JVM的GC簡介和實例

本文是一次內部分享中總結了jvm gc的分類和一些實例, 內容是introduction級別的,供初學人士參考.
成文倉促,難免有些錯誤,如果有大牛發現,請留言,我一定及時更正,謝謝!
JVM內存佈局主要包含下面幾個部分:

Java Virtual Machine Stack: 也就是我們常見的局部變量棧,線程私有,保存線程執行的局部變量表、操作棧、動態連接等。
Java Heap:我們最常打交道的內存區域,幾乎所有對象的實例都在這個區域分配。所謂的GC基本上也就是跟這個區域打交道。
Method Area:包含被虛擬機加載的類、常量、靜態變量等數據。
Hotspot虛擬機使用分代收集算法,將Java Heap根據對象的存活週期分爲多個區域:新生代、老生代和永生代。

新生代和老生代位於Java heap中,是垃圾收集器主要處理的內存區域。

永生代則基本上等價於Method Area,也就是說其中包含的數據在jvm進程存活期間會一直存在,一般不會發生變化。

java堆內存的佈局如下圖所示:

jvm堆佈局

使用jstat可以查看某個java進程的內存狀況:




 chendeMacBook-Air:~ eleforest$ jstat -gc 16136
 S0C    S1C    S0U    S1U      EC       EU        OC         
 OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   2867.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
其中各個指標介紹如下:(單位爲KB)

S0C,S1C,S0U,S1U: 0/1倖存區(survivor)容量(C:Capacity)/使用量(U:Used)。
EC,EU: Eden(伊甸)區容量/用量。Eden和survivor兩個區域位於新生代,由於新生代GC一般是使用複製算法進行清理,因此按照複製算法的原理將新生代分成了3個區域:Eden、Survivor0、Survivor1。Hotspot虛擬機的3個空間缺省配比爲:8:1:1,jvm只會使用eden和1個survivor作爲新生代空間.當新生代空間不足時發生minor gc,此時根據複製算法, jvm會首先 1)將eden和from survivor中存活的對象拷貝到to survior中,然後2)釋放eden和from中的所有需要回收對象,最後3)調換from/to survior,jvm將eden和新的from survior作爲新生代。當然上述minor gc順利執行還取決於很多因素,這裏只描述了最理想化的狀態。
OC,OU: Old(老生代)容量/用量。老生代常用的垃圾收集器有CMS、Serial Old、Parallel Old等
PC,PU: Perm(永生代)容量/用量。
YGC/YGCT: Young GC次數和總耗費時間。Young GC也就是Minor GC,新生代中內存不夠時觸發,通常採用複製算法進行,回收速度較快,對系統的影響較小。
FGC/FGCT:Full GC次數和總耗費時間。Full GC是在java heap空間不足(包括New和Old區域)時觸發,會分別清理新生代、老生代,通常耗時較長,對系統有較大影響,應該儘量避免。
GCT:GC總耗時。
常用的垃圾收集器包括下面幾個

Serial:最基本,歷史最悠久的收集器,單線程收集垃圾內存,在新生代採用複製算法,在老生代使用標記-整理算法
ParNew:Serial的多線程版本,主要用於新生代收集。與CMS收集器配合成爲現在最常用的server收集器
Parallel Scavenge:也是一個並行收集器,使用與ParNew完全不同的收集策略,具體的差別還在研究中
CMS:Concurrent Mark Sweep收集器,大名鼎鼎,其目標是獲取最短回收停頓時間,是server模式下最常用的收集器
G1:最新的收集器,木有用過啊
下面將會用一段簡單的程序演示jvm在配置使用不同的收集器情況下,GC行爲的不同點,通過GC的行爲能夠瞭解到不同收集器的收集策略和行爲。代碼非常簡單:

//jvm basic args:-Xmx20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
public class Main {
    public static void main(String[] args) throws Exception {
    byte[] alloc1,alloc2,alloc3,alloc4;
    alloc1 = new byte[2*1024*1024];
    Thread.sleep(2000);
    alloc2 = new byte[2*1024*1024];
    Thread.sleep(2000);
    alloc3 = new byte[2*1024*1024];
    Thread.sleep(2000);
    alloc4 = new byte[2*1024*1024];
    Thread.sleep(2000);
    }
}
其中上例中的jvm參數解釋如下:

Xmx 最大堆容量,包含了新生代和老生代的堆容量
Xms 最小堆容量,此時配置與Xmx一樣,避免了申請空間時的堆擴展
Xmn 新生代容量,包含eden,survivor1,survivor2三個區域
PrintGCDetails  讓jvm在每次發生gc的時候打印日誌,利於分析gc的原因和狀況
SurvivorRatio   新生代中eden的比例,如果設置爲8,意味着新生代中eden佔據80%的空間,兩個survivor分別佔據10%
測試環境爲mac os 10.8,jdk版本如下:

chendeMacBook-Air:~ eleforest$ java -version
java version "1.7.0_09"
Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)
示例1:讓jvm自動選擇收集器
直接運行上述代碼,用jstat觀察gc情況如下:


chendeMacBook-Air:~ eleforest$ jstat -gc 21729 1000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   819.9    10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   2867.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   2867.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   4915.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   4915.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   6963.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   6963.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0   292.9   8192.0   2375.9   10240.0     6144.0   21248.0 2640.3      1    0.007   0      0.000    0.007
1024.0 1024.0  0.0   292.9   8192.0   2375.9   10240.0     6144.0   21248.0 2640.3      1    0.007   0      0.000    0.007
由上述的結果可見,程序啓動時,Eden使用了819.9K的空間(我現在還不知道819k是什麼東西的開銷),S1、S2、老生代均沒有佔用,永生代則使用了2.6MB空間,其中包含了包含被虛擬機加載的類、常量、靜態變量等數據。
隨後連續三次申請了2MB的空間,這些數據都被放到了Eden區域,這就是jvm內存分配的第一個原則:對象優先在Eden分配,這個原則只在Eden空間足夠,且申請的內存小於jvm參數PretenureSizeThreshold設置值時生效(根據採用的收集器不同,還會有很多不同情況)
注意看第四次申請2MB空間,此時由於Eden空間無法容納新的數組,因此發生了一次Minor GC,具體的GC log如下所示:

[GC [DefNew: 6963K->292K(9216K), 0.0065350 secs] 6963K->6436K(19456K), 0.0065940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
Heap
 def new generation   total 9216K, used 2832K [0x0000000112230000, 0x0000000112c30000, 0x0000000112c30000)
  eden space 8192K,  31% used [0x0000000112230000, 0x00000001124aaf60, 0x0000000112a30000)
  from space 1024K,  28% used [0x0000000112b30000, 0x0000000112b793b0, 0x0000000112c30000)
  to   space 1024K,   0% used [0x0000000112a30000, 0x0000000112a30000, 0x0000000112b30000)
 tenured generation   total 10240K, used 6144K [0x0000000112c30000, 0x0000000113630000, 0x0000000113630000)
   the space 10240K,  60% used [0x0000000112c30000, 0x0000000113230030, 0x0000000113230200, 0x0000000113630000)
 compacting perm gen  total 21248K, used 2647K [0x0000000113630000, 0x0000000114af0000, 0x0000000118830000)
   the space 21248K,  12% used [0x0000000113630000, 0x00000001138c5ec0, 0x00000001138c6000, 0x0000000114af0000)
No shared spaces configured.
其中第一行中的"DefNew"代表使用的收集器是Serial收集器,這次Minor GC使用copy算法,做了下面幾件事情:

檢索heap中的對象,將還能通過GC roots能夠遍歷到的對象copy到to區中
如果需要copy的對象沒法進入from區中,則將其晉升到老年代,本例中即發生了這種情況,32MB的數組全部晉升到老生代(OU:6144)
清理eden和from中無用的垃圾
互換from和to空間
比較有意思的是,在我的機器上重新再跑一次示例程序,發生了不一致的gc行爲:

[GC [PSYoungGen: 6963K->384K(9216K)] 6963K->6528K(19456K), 0.0052500 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC [PSYoungGen: 384K->0K(9216K)] [ParOldGen: 6144K->6436K(10240K)] 6528K->6436K(19456K) [PSPermGen: 2637K->2635K(21248K)], 0.0157270 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]
HeapA
 PSYoungGen      total 9216K, used 2539K [0x00000001106d0000, 0x00000001110d0000, 0x00000001110d0000)
  eden space 8192K, 31% used [0x00000001106d0000,0x000000011094af60,0x0000000110ed0000)
  from space 1024K, 0% used [0x0000000110ed0000,0x0000000110ed0000,0x0000000110fd0000)
  to   space 1024K, 0% used [0x0000000110fd0000,0x0000000110fd0000,0x00000001110d0000)
 ParOldGen       total 10240K, used 6436K [0x000000010fcd0000, 0x00000001106d0000, 0x00000001106d0000)
  object space 10240K, 62% used [0x000000010fcd0000,0x0000000110319278,0x00000001106d0000)
 PSPermGen       total 21248K, used 2645K [0x000000010aad0000, 0x000000010bf90000, 0x000000010fcd0000)
  object space 21248K, 12% used [0x000000010aad0000,0x000000010ad65688,0x000000010bf90000)
GC log第一行的PSYoungGen意味着這次運行中jvm自動選擇了Parallel Scavenge收集器,GC行爲發生了變化,同樣的內存請求,PS收集器除了一次Minor GC以外,還發生了一次Full GC。PS收集器的實現與serial不一致,其行爲模式還需要進一步研究.
比較弔詭的是jvm的自動選擇行爲,我閱讀了openjdk的源碼,版本爲:openjdk-7-fcs-src-b147-27_jun_2011
其中關於jvm自動選擇gc的代碼如下:


if (os::is_server_class_machine() && !force_client_mode ) {
  // If no other collector is requested explicitly,
  // let the VM select the collector based on
  // machine class and automatic selection policy.
  if (!UseSerialGC &&
      !UseConcMarkSweepGC &&
      !UseG1GC &&
      !UseParNewGC &&
      !DumpSharedSpaces &&
      FLAG_IS_DEFAULT(UseParallelGC)) {
    if (should_auto_select_low_pause_collector()) {//如果需要低時延收集器,選擇cms
      FLAG_SET_ERGO(bool, UseConcMarkSweepGC, true);
    } else {//否則缺省使用ps收集器
      FLAG_SET_ERGO(bool, UseParallelGC, true);
    }
    no_shared_spaces();
  }
}
如上所示,jvm在沒有明確設置gc時會採用parallel scavenge作爲缺省收集器。因此我機器上jvm自動選擇gc的行爲還需要進一步研究。

示例2:使用ParNew收集器
調整jvm的參數,添加-XX:+UseParNewGC,告訴jvm選擇使用ParNew收集器,此時執行的結果與示例1中使用serial收集器的行爲完全一樣。這裏不再贅述

示例3:使用CMS收集器
調整jvm參數爲:


-Xmx20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC</pre>
此時啓動示例程序,我們會看到如下的結果:
<pre class="brush:shell">chendeMacBook-Air:~ eleforest$ jstat -gc 21729 1000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   820.2     8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   2868.2    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   2868.2    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   4916.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   4916.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   6964.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0    0.0    8192.0   6964.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   2      0.001    0.010
1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   2      0.001    0.010
1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   4      0.002    0.011
1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   4      0.002    0.011
1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   6      0.003    0.012
也就是說到第四個2MB申請,老生代裏使用6MB的數據之後,jvm還進行了6次full gc,這是由於cms特殊性導致的:cms爲了保證進行gc時應用的低時延,要求在老生代中剩餘充足的空間以備應用使用。這個特性可以用下列參數進行調整和限制


-XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly
其中CMSInitiatingOccupancyFraction的缺省爲68%。在我們的示例中,OU已經超過了這個限制,jvm試圖去清理老生代,因此發生了多次full gc。
通過修改CMSInitiatingOccupancyFraction爲80或者更高值,再次執行示例程序後不會再發生fullGC。
爲了使應用平順,CMS收集器的使用需要小心的調整堆空間的大小,太小的老生代可能會起到相反的效果,過高的CMSInitiatingOccupancyFraction也會導致回收數據時使應用無法正常工作。

以上便是我在這篇博客中想要分享的內容,做一些記錄,也分享出來。
但是如分享中所說的,還有以下問題還沒有搞清楚:

PS收集器的行爲,觸發full gc的條件
jvm自動選擇收集器的策略
G1收集器的使用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章