本文是一次內部分享中總結了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區中,則將其晉升到老年代,本例中即發生了這種情況,3個2MB的數組全部晉升到老生代(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收集器的使用
JVM的GC簡介和實例
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.