Full GC觸發原理和日誌分析

1、Java GC的工作原理

GC(garbage collection)是指垃圾回收機制,當一個對象不能再被後續程序所引用到時,這個對象所佔用的內存空間就沒有存在的意義了,java虛擬機會不定時的去檢測內存中這樣的對象,然後回收這塊內存空間。當可用內存不能滿足內存請求時,GC會自動進行。
所有通過new創建的對象的內存都在堆中分配,其大小可以通過-Xms(程序的初始化內存大小)和-Xmx(程序佔用的最大內存)來控制。堆被劃分爲新生代和舊生代,新生代又被進一步劃分爲Eden和Survivor區,最後Survivor由FromSpace和ToSpace組成,結構圖如下所示:
Heap的結構圖

  • 新生代:
    新建的對象都是用新生代分配內存,Eden空間不足的時候,會把存活的對象轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例舊生代。用於存放新生代中經過多次垃圾回收仍然存活的對象。
    新生代的GC:
    新生代通常存活時間較短,因此基於Copying算法來進行回收,所謂Copying算法就是掃描出存活的對象,並複製到一塊新的完全未使用的空間中,對應於新生代,就是在Eden和FromSpace或ToSpace之間copy。新生代採用空閒指針的方式來控制GC觸發,指針保持最後一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發GC。當連續分配對象時,對象會逐漸從eden到survivor,最後到舊生代
  • 舊生代:
    舊生代與新生代不同,對象存活的時間比較長,比較穩定,因此採用標記(Mark)算法來進行回收,所謂標記就是掃描出存活的對象,然後再進行回收未被標記的對象,回收後對用空出的空間要麼進行合併,要麼標記出來便於下次進行分配,總之就是要減少內存碎片帶來的效率損耗。
    在GC被觸發的過程中,會導致服務暫停,因此關注FULLGC的總次數,計算服務暫停時間和頻率,就是爲了分析服務器的性能,從而根據具體情況進行性能優化。

2、觸發Full GC執行的情況

一般Young區觸發gc的條件是Eden區滿。

除直接調用System.gc外,觸發Full GC執行的情況有如下四種
1) 新生代對象轉入及創建爲大對象、大數組時導致舊生代空間出現不足的現象,爲避免以上狀況引起的Full GC,調優時應儘量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組

2) Permanet Generation中存放的爲一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的情況下會執行Full GC。爲避免Perm Gen佔滿造成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。
CMS:ConcurrentMarkSweep收集器,其目標是獲取最短回收停頓時間,是server模式下最常用的收集器。

3) 對於採用CMS進行舊生代GC的程序,當GC日誌中有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。promotion failed是在進行Minor GC時,survivor space放不下、對象只能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入舊生代,而此時舊生代空間不足造成的。應對措施:增大survivor space、舊生代空間或調低觸發併發GC的比率。

4) 還有一個較爲複雜的觸發情況,Hotspot爲了避免由於新生代對象晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。
例如程序第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。
當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。
除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認情況下會一小時執行一次Full GC。可通過在啓動時通過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

3、GC日誌分析

發生GC時輸出的日誌就類似於下面這種格式(爲了顯示方便,已手工折行):

2015-05-26T14:45:37.987-0200: 151.126: 
  [GC (Allocation Failure) 151.126:
    [DefNew: 629119K->69888K(629120K), 0.0584157 secs]
    1619346K->1273247K(2027264K), 0.0585007 secs] 
  [Times: user=0.06 sys=0.00, real=0.06 secs]

2015-05-26T14:45:59.690-0200: 172.829: 
  [GC (Allocation Failure) 172.829: 
    [DefNew: 629120K->629120K(629120K), 0.0000372 secs]
    172.829: [Tenured: 1203359K->755802K(1398144K), 0.1855567 secs]
    1832479K->755802K(2027264K),
    [Metaspace: 6741K->6741K(1056768K)], 0.1856954 secs]
  [Times: user=0.18 sys=0.00, real=0.18 secs]
12345678910111213

上面的GC日誌暴露了JVM中的一些信息。事實上,這個日誌片段中發生了 2 次垃圾回收事件(Garbage Collection events)。其中一次清理的是年輕代(Young generation), 而第二次處理的是整個堆內存。下面我們來看,如何解讀第一次GC事件,發生在年輕代中的小型GC(Minor GC):

2015-05-26T14:45:37.987-0200:151.126:

[GC(Allocation Failure)151.126: 

[DefNew:629119K->69888K(629120K), 0.0584157 secs]
1619346K->1273247K(2027264K),0.0585007 secs]
[Times: user=0.06 sys=0.00, real=0.06 secs]
1. 2015-05-26T14:45:37.987-0200 – GC事件(GC event)開始的時間點.
 2. 151.126 – GC事件的開始時間,相對於JVM的啓動時間,單位是秒(Measured in seconds).
 3. GC – 用來區分(distinguish)是 Minor GC 還是 Full GC 的標誌(Flag). 這裏的 GC
    表明本次發生的是 Minor GC.
 4. Allocation Failure – 引起垃圾回收的原因.
    本次GC是因爲年輕代中沒有任何合適的區域能夠存放需要分配的數據結構而觸發的.
 5. DefNew – 使用的垃圾收集器的名字. DefNew 這個名字代表的是: 單線程(single-threaded),
    採用標記複製(mark-copy)算法的, 使整個JVM暫停運行(stop-the-world)的年輕代(Young
    generation) 垃圾收集器(garbage collector).
 6. 629119K->69888K – 在本次垃圾收集之前和之後的年輕代內存使用情況(Usage).
 7. (629120K) – 年輕代的總的大小(Total size).
 8. 1619346K->1273247K – 在本次垃圾收集之前和之後整個堆內存的使用情況(Total used heap).
 9. (2027264K) – 總的可用的堆內存(Total available heap).
 10. 0.0585007 secs – GC事件的持續時間(Duration),單位是秒.
 11. [Times: user=0.06 sys=0.00, real=0.06 secs]GC事件的持續時間,通過多種分類來進行衡量: user – 此次垃圾回收, 垃圾收集線程消耗的所有CPU時間(Total CPU
     time). sys – 操作系統調用(OS call) 以及等待系統事件的時間(waiting for system event)
     real – 應用程序暫停的時間(Clock time). 由於串行垃圾收集器(Serial Garbage
     Collector)只會使用單個線程, 所以 real time 等於 user 以及 system time 的總和.

通過上面的分析, 我們可以計算出在垃圾收集期間, JVM 中的內存使用情況。在垃圾收集之前, 堆內存總的使用了 1.54G (1,619,346K)。其中, 年輕代使用了 614M(629,119k)。可以算出老年代使用的內存爲: 967M(990,227K)。

下一組數據( -> 右邊)中蘊含了更重要的結論, 年輕代的內存使用在垃圾回收後下降了 546M(559,231k), 但總的堆內存使用(total heap usage)只減少了 337M(346,099k). 通過這一點,我們可以計算出, 有 208M(213,132K) 的年輕代對象被提升到老年代(Old)中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章