JVM全面分析之垃圾回收的相關概念
目錄
System.gc()的理解
在預設情況下,通過System.gc()或者Runtime.getRuntime().gc()的呼叫,會顯示的觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件佔用的記憶體。
然而System.gc()呼叫附帶一個免責宣告,無法保證對垃圾收集器的呼叫。即有可能不會執行垃圾回收或者延遲執行垃圾回收。
JVM實現者可以通過System.gc()呼叫來決定JVM的GC行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過於麻煩了
案例
public class GCTest01 { public void localvarGC1() { byte[] buffer = new byte[10 * 1024 * 1024]; System.gc(); } public void localvarGC2() { byte[] buffer = new byte[10 * 1024 * 1024]; buffer = null; System.gc(); } public void localvarGC3() { { byte[] buffer = new byte[10 * 1024 * 1024]; } System.gc(); } public void localvarGC4() { { byte[] buffer = new byte[10 * 1024 * 1024]; } int value = 10; System.gc(); } public void localvarGC5() { localvarGC1(); System.gc(); } public static void main(String[] args) { GCTest01 test = new GCTest01(); test.localvarGC1(); } }
執行程式之前新增如下命令:-XX:+PrintGCDetails
當執行第一個函式時:
[GC (System.gc()) [PSYoungGen: 31211K->11817K(305664K)] 31211K->11825K(1005056K), 0.0129885 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [Full GC (System.gc()) [PSYoungGen: 11817K->0K(305664K)] [ParOldGen: 8K->11691K(699392K)] 11825K->11691K(1005056K), [Metaspace: 3134K->3134K(1056768K)], 0.0147335 secs] [Times: user=0.02 sys=0.01, real=0.01 secs]
可見buffer沒有被回收,進行Full GC時被放入老年代.
當執行第二個函式時:
[GC (System.gc()) [PSYoungGen: 31211K->1549K(305664K)] 31211K->1557K(1005056K), 0.0026331 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 1549K->0K(305664K)] [ParOldGen: 8K->1451K(699392K)] 1557K->1451K(1005056K), [Metaspace: 3134K->3134K(1056768K)], 0.0086842 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
發現buffer被回收了。
當執行第三個函式時
[GC (System.gc()) [PSYoungGen: 31211K->11821K(305664K)] 31211K->11829K(1005056K), 0.0162805 secs] [Times: user=0.01 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 11821K->0K(305664K)] [ParOldGen: 8K->11691K(699392K)] 11829K->11691K(1005056K), [Metaspace: 3134K->3134K(1056768K)], 0.0181306 secs] [Times: user=0.02 sys=0.01, real=0.02 secs]
發現buffer沒被回收,不是說程式碼塊中的物件作用域只在程式碼塊裡面嗎,出來程式碼塊就會被回收嗎?而且檢視.class檔案也發現區域性變數數量是2,但是卻只有一個現實出來,不是說會被重複利用嗎?
原因是這樣的:方法3中除了this變數外,只有buffer變數,如果在宣告另一個變數,才會佔用buffer的位置,這就是方法4,
方法4執行結果
[GC (System.gc()) [PSYoungGen: 31211K->1565K(305664K)] 31211K->1573K(1005056K), 0.0031814 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 1565K->0K(305664K)] [ParOldGen: 8K->1451K(699392K)] 1573K->1451K(1005056K), [Metaspace: 3135K->3135K(1056768K)], 0.0085616 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
方法5結果:
[GC (System.gc()) [PSYoungGen: 31211K->11821K(305664K)] 31211K->11829K(1005056K), 0.0117181 secs] [Times: user=0.02 sys=0.01, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 11821K->0K(305664K)] [ParOldGen: 8K->11691K(699392K)] 11829K->11691K(1005056K), [Metaspace: 3134K->3134K(1056768K)], 0.0138071 secs] [Times: user=0.02 sys=0.01, real=0.01 secs]
[GC (System.gc()) [PSYoungGen: 0K->0K(305664K)] 11691K->11691K(1005056K), 0.0004702 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 0K->0K(305664K)] [ParOldGen: 11691K->1451K(699392K)] 11691K->1451K(1005056K), [Metaspace: 3134K->3134K(1056768K)], 0.0071666 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
方法5運行了兩次GC,因為第一次gc沒有把區域性變數消除掉。
記憶體溢位與記憶體洩漏
記憶體溢位
- Java虛擬機器的堆記憶體設定不夠
比如:可能存在記憶體洩漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的資料量,但是沒有顯示指定JVM堆大小或者指定數值偏小。我們可以通過引數-Xms、-Xmx來調整。 - 程式碼中建立了大量的大物件,並且長時間不能被垃圾收集器收集(存在被引用)
對於老闆的Oracle JDK,因為永久代的大小是有限的,並且JVM對永久代垃圾回收(如:常量池回收、解除安裝不再需要的型別)非常不積極,所以當我們不斷新增新型別的時候,永久代出現OutOfMemoryError也非常多見,尤其是在執行時存在大量動態型別生成的場合;類似intern字串快取佔用太多空間,也會導致OOM問題。對應的異常資訊,會標記出來和永久代相關:”java.lang.OutOfMemoryError:PermGen space“。
隨著元資料區的引入,方法區記憶體已經不再那麼窘迫,所以相應的OOM有所改觀,出現OOM,異常資訊則變成了:”java.lang.OutOfmemoryError:Metaspace“。直接記憶體不足,也會導致OOM。
- 這裡隱含一層意思是,在丟擲OutOfMemoryError之前,通常垃圾收集器會被觸發,盡其可能去清理出空間。
&emsp * 例如L在引用機制分析中,涉及到JVM會去嘗試回收軟引用指向的物件等
* 在java.nio.BIts.reserveMemory()方法中,我們能清除的看到,System.gc()會被呼叫,以清理空間。 - 當然,也不是在任何情況下垃圾收集器都會觸發的
* 比如,我們去分配一個超大的物件,類似一個超大陣列超過堆的最大值,JVM可以判斷出垃圾收集並不能解決這個問題,所以直接丟擲OutOfMemoryError。
記憶體洩漏(Memory Leak)
也稱作”儲存滲漏“。嚴格來說,只有物件不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體洩漏。
但實際情況很多時候一些不太好的實踐或疏忽會導致物件的宣告週期變得很長甚至導致OOM,也可以叫做寬泛意義上的”記憶體洩漏“
Stop The World
Stop-the-world,簡稱STW,指的是GC事件發生過程中,會產生應用程式的停頓,停頓產生時整個應用程式執行緒都會暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。
* 可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。
* 分析工作必須在一個能確保一致性的快照中進行
* 一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上
* 如果出現分析過程中物件引用關係還在不斷變化,則分析結果的準確性無法保證
- STW事件和採用哪款GC無關,所有的GC都有這個事件。
- 哪怕G1也不能完全避免Stop-the-world情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,儘可能地縮短了暫停時間
- STW是JVM在後臺自動發起和自動完成的,在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉。
垃圾回收的並行與併發
- 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍處於等待狀態。
如:ParNew、Parallel Scavenge、Parallel Old; - 序列(Serial)
相較於並行的概念,單執行緒執行。
如果記憶體不夠,則程式暫停,啟動JVM垃圾回收器進行垃圾回收。回收完,再啟動程式的執行緒。
併發:
指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓使用者程式的執行。
* 使用者程式在繼續執行,而垃圾收集程式執行緒運行於另一個CPU上;
* 如CMS、G1
安全點與安全區域
安全點
程式執行時並非在所有地方都能停頓下來開始GC,只有在特定的位置才能停頓下來開始GC,這些位置稱為”安全點(safepoint)“
safepoint的選擇很重要,如果太少可能導致GC等待時間太長,如果太頻繁導致執行時效能問題。大部分指令的執行時間都非常短暫,通常會根據”是否具有讓程式長時間執行的特徵“為標準。比如:選擇一些執行時間較長的指令作為Safe Point,如方法呼叫、迴圈跳轉和異常跳轉等。
如何在GC發生時,檢查所有執行緒都跑到最近的安全點停頓下來呢?
-
搶先式中斷:(目前沒有虛擬機器採用了)
首先中斷所有執行緒。如果還有執行緒不在安全點,就恢復執行緒,讓執行緒跑到安全點。 -
主動式終端
設定一個終端標誌,各個執行緒執行到safe point的時候主動輪詢這個標誌,如果終端標誌為真,則將自己進行中斷掛起。
安全區域(Safe Region)
Safe point機制保證了程式執行時,在不太長的時間就會遇到可進入GC的safepoint。但是,程式”不執行“的時候呢?例如執行緒處於Sleep狀態或Blocked狀態,這時候執行緒無法響應JVM的中斷請求,”走“到安全點去中斷掛起,JVM也不太可能等待執行緒被喚醒。對於這種情況,就需要安全區域(safe region)來解決。
安全區域是指在一段程式碼片段中,物件的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。我們也可以把safe region看做是被擴充套件了的safe point.
實際執行時:
- 當執行緒執行到safe region的程式碼時,首先標識已經進入了safe region,如果這段時間內發生GC,JVM會忽略標識為safe region狀態的執行緒;
- 當執行緒即將離開safe region時,會檢查JVM是否已經完成GC,如果完成了,則繼續執行,否則執行緒必須等待直到收到可以安全離開啊safe region的訊號位置。
再談引用:強引用
概述
-
強引用(StrongReference):最傳統的“引用”的定義。無論任何情況下只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件
-
軟引用(SoftReference):在系統將要發生記憶體溢位之前,將會把這些物件列入回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
-
弱引用(WeakReference):被弱引用關聯的物件只能生存到下一次垃圾回收之前。當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收掉被弱引用關聯的物件。
-
虛引用(PhantomReference):一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個物件的例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
-
強引用所指向的物件在任何時候都不會被系統回收,虛擬機器寧願丟擲OOM異常,也不會回收強引用所指向的物件
-
強引用也是造成Java記憶體洩漏的主要原因之一。
再談引用:軟引用
軟引用是用來描述一些還有用,但非必須的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
軟引用通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟引用。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同事,不會耗盡記憶體。
垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟引用,並可選地把引用存放到一個引用佇列(Reference Queue)。
類似弱引用,只不過Java虛擬機器會盡量讓軟引用的存活時間長一些,迫不得已才清理。
再談引用:弱引用
在談引用:虛引用
也稱為”幽靈引用“或者”幻影引用“,是所有引用型別中最弱的一個。
一個物件是否有虛引用的存在,完全不會決定物件的宣告週期。如果一個物件僅持有虛引用,那麼它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。
它不能單獨使用,也無法通過虛引用來獲取被引用的物件。當試圖通過虛引用的get()方法取得物件時,總是null。
為一個物件設定虛引用關聯的唯一目的在於跟蹤垃圾回收過程。比如:能在這個物件被收集器回收時收到一個系統通知。
虛引用和引用佇列一起使用。虛引用在建立時必須提供一個引用佇列作為引數。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件後,將這個虛引用加入引用佇列,以通知應用程式物件的回收情況。
由於虛引用可以跟蹤物件的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄