http://blog.csdn.net/historyasamirror/article/details/6233007(轉(zhuǎn))
正文之前,先介紹一人:Jon Masamitsu。此人背景不詳,不過(guò)他在SUN做的就是JVM,所以他的blog我認(rèn)為是每一個(gè)想對(duì)JVM調(diào)優(yōu)的人都應(yīng)該讀一讀的。本文的很多觀點(diǎn)和一些圖也是取自他的blog。
blog link:http://blogs.sun.com/jonthecollector/
在他的一篇blog【1】中,寫到了GC調(diào)優(yōu)的最重要的三個(gè)選項(xiàng):
排在第三位的是young generation在整個(gè)JVM heap中所占的比重;
排在第二位的是整個(gè)JVM heap的大??;
排在第一位的就是選擇合適的GC collector,這也是本文的內(nèi)容所在。
基本概念
先科普一些基本知識(shí)。JVM Heap在實(shí)現(xiàn)中被切分成了不同的generation(很多中文翻譯成‘代’),比如生命周期短的對(duì)象會(huì)放在young generation,而生命周期長(zhǎng)的對(duì)象放在tenured generation中,如下圖(摘自【2】)。
當(dāng)GC只發(fā)生在young generation中,回收young generation中的對(duì)象,稱為Minor GC;當(dāng)GC發(fā)生在tenured generation時(shí)則稱為Major GC或者Full GC。一般的,Minor GC的發(fā)生頻率要比Major GC高很多。
關(guān)于generation的知識(shí),這里不多談了,感興趣的參見【2】,或者很多網(wǎng)上的文章。
上圖(摘自【3】)很清楚的列出了JVM提供的幾種GC collector。
其中負(fù)責(zé)Young Generation的collector有三種:
Serial :最簡(jiǎn)單的collector,只有一個(gè)thread負(fù)責(zé)GC,并且,在執(zhí)行GC的時(shí)候,會(huì)暫停整個(gè)程序(所謂的“stop-the-world”),如下圖所示;
Parallel Scavenge:
和Serial相比,它的特點(diǎn)在于使用multi-thread來(lái)處理GC,當(dāng)然,在執(zhí)行的時(shí)候,仍然會(huì)“stop-the-world”,好處在于,暫停的時(shí)間也許更短;
ParNew:
它基本上和Parallel Scavenge非常相似,唯一的區(qū)別,在于它做了強(qiáng)化能夠和CMS一起使用;
負(fù)責(zé)Tenured Generation的collector也有三種:
Serial Old:
單線程,采用的是mark-sweep-compact回收方法(好吧,我承認(rèn)我不知道什么是mark-sweep-compact,對(duì)我來(lái)說(shuō),只記住了它是單線程的),圖示和Serial類似;
Parallel Old:
同理,多線程的GC collector;
CMS:
全稱“concurrent-mark-sweep”,它是最并發(fā),暫停時(shí)間最低的collector,之所以稱為concurrent,是因?yàn)樗趫?zhí)行GC任務(wù)的時(shí)候,GC thread是和application thread一起工作的,基本上不需要暫停application thread,如下圖所示;
6種collector介紹完了。不過(guò),在設(shè)定JVM參數(shù)的時(shí)候,很少有人去分別制定young generation和tenured generation的collector,而是提供了幾套選擇方案:
-XX:+UseSerialGC:
相當(dāng)于”Serial” + “SerialOld”,這個(gè)方案直觀上就應(yīng)該是性能最差的,我的實(shí)驗(yàn)證明也確實(shí)如此;
-XX:+UseParallelGC:
相當(dāng)于” Parallel Scavenge” + “SerialOld”,也就是說(shuō),在young generation中是多線程處理,但是在tenured generation中則是單線程;
-XX:+UseParallelOldGC:
相當(dāng)于” Parallel Scavenge” + “ParallelOld”,都是多線程并行處理;
-XX:+UseConcMarkSweepGC:
相當(dāng)于"ParNew" + "CMS" + "Serial Old",即在young generation中采用ParNew,多線程處理;在tenured generation中使用CMS,以求得到最低的暫停時(shí)間,但是,采用CMS有可能出現(xiàn)”Concurrent Mode Failure”(這個(gè)后面再說(shuō)),如果出現(xiàn)了,就只能采用”SerialOld”模式了;
【3】中還提到了一個(gè)方案:UseParNewGC,不過(guò)我在其它地方很少看見有人用它,就不介紹了。
實(shí)驗(yàn)和結(jié)果
說(shuō)了這么多,還是用實(shí)驗(yàn)驗(yàn)證一下吧。
被實(shí)驗(yàn)的系統(tǒng)可以看做是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),所有的數(shù)據(jù)和查詢都是在內(nèi)存中進(jìn)行,測(cè)試用的workload即包含將數(shù)據(jù)寫入到內(nèi)存中,也包含各式各樣的查詢。這樣一個(gè)系統(tǒng)和測(cè)試數(shù)據(jù),在運(yùn)行過(guò)程中,由于處理大量的查詢,所以肯定會(huì)隨時(shí)產(chǎn)生很多的短周期對(duì)象,所以young generation對(duì)應(yīng)的Minor GC會(huì)比較頻繁,同時(shí),由于不斷有新的數(shù)據(jù)寫入到數(shù)據(jù)庫(kù),而這些數(shù)據(jù)都屬于長(zhǎng)周期對(duì)象,所以tenured generation的使用率是不斷增長(zhǎng)。
實(shí)驗(yàn)使用的是一臺(tái)DELL的服務(wù)器,有48G內(nèi)存,有2個(gè)CPU,各4個(gè)core,所以總共8個(gè)core。為了實(shí)驗(yàn)的方便,我只是用了大概16G內(nèi)存給JVM使用。
SerialGC
先看第一張圖:
圖中的采樣點(diǎn)有些密集,截取其中的一小段放大如下:
圖的縱軸表示的是系統(tǒng)的throughput,單位是request/second。我設(shè)定的計(jì)算throughput的時(shí)間間隔是5秒,也就是說(shuō),圖中的每一個(gè)點(diǎn),都是一個(gè)5秒時(shí)間區(qū)間內(nèi)系統(tǒng)throughput的平均值。
圖的橫軸表示的是時(shí)間維度。
后面幾張圖的設(shè)定和這個(gè)圖是一樣的。
這張圖采用的是SerialGC ,整個(gè)JVM的參數(shù)設(shè)置如下:
Java -jar -Xms10g -Xmx15g -XX:+UseSerialGC -XX:NewSize=6g -XX:MaxNewSize=6g -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:./log/gc.log Slaver.jar
其中,-XX:NewSize=6g -XX:MaxNewSize=6g 表示的是將young generation的初始化size和最大size都設(shè)置成了6G,也就是說(shuō)在整個(gè)系統(tǒng)運(yùn)行過(guò)程中,young generation的size是不會(huì)變的。這是因?yàn)镴VM會(huì)根據(jù)實(shí)際的運(yùn)行情況動(dòng)態(tài)的調(diào)節(jié)young generation和tenured generation的比例,而這樣的設(shè)置能夠避免這種動(dòng)態(tài)的調(diào)整,便于觀察實(shí)驗(yàn)結(jié)果。
-Xms10g -Xmx15g 表示將JVM Heap的初始Size設(shè)置為10G,而它的最大Size設(shè)置為15G。相當(dāng)于初始的時(shí)候tenured generation的大小約等于(10-6)G,而它可以增長(zhǎng)為(15-6)G (只是近似計(jì)算,忽略了perm generation的size)。
-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:./log/gc.log 這些參數(shù),是將GC的細(xì)節(jié)和Heap的變化記錄到gc.log中。
Slaver.jar 是俺的程序....
對(duì)于后面的實(shí)驗(yàn),這些參數(shù)都不會(huì)變動(dòng)。唯一變動(dòng)的就只是選擇不同的Collector。
OK,回到實(shí)驗(yàn)結(jié)果圖。
這張圖有一個(gè)很有趣的現(xiàn)象是它的抖動(dòng)非常大,幾乎總是一個(gè)點(diǎn)高,緊挨著一個(gè)點(diǎn)就會(huì)低。這意味著系統(tǒng)的throughput在不斷的震蕩。這個(gè)現(xiàn)象的原因是實(shí)驗(yàn)過(guò)程中,Minor GC的發(fā)生頻率基本上是7-9秒一次(通過(guò)log觀察到的),而前面提到了我們計(jì)算throughput的時(shí)間區(qū)間是5秒,所以,基本上每隔一個(gè)時(shí)間區(qū)間就會(huì)發(fā)生一次Minor GC,而發(fā)生Minor GC的時(shí)間區(qū)間它的平均throughput就會(huì)降低。
為了證明這一點(diǎn),我隨意找了一段Minor GC的log:
900.692: [GC 900.692: [DefNew: 5334222K->300781K(5662336K), 0.8988610 secs] 7654628K->2641401K(9856640K), 0.8990190 secs] [Times: user=0.86 sys=0.03, real=0.89 secs ]
解釋一下這段log:
900.692 表明這次GC發(fā)生在系統(tǒng)啟動(dòng)后的900.062秒這一時(shí)刻;
5334222K->300781K(5662336K) 表明young generation在這次GC中從5334222K降低到了300781K,而young generation的size是5662336K(注意,這個(gè)值近似于我們之前設(shè)定的6G),這個(gè)過(guò)程花費(fèi)了0.8988610秒;
7654628K->2641401K(9856640K) 表明這個(gè)JVM Heap從7654628K降低到了2641401K(這兩個(gè)值之差應(yīng)該和上面的那兩個(gè)值之差幾乎相等,因?yàn)檫@是Minor GC,所以整個(gè)Heap清理出的空間其實(shí)就是young generation清理出的空間);
user=0.86 sys=0.03, real=0.89 secs 表明這次GC的user time是0.86,而real time是0.89秒 (不理解user/sys/real的見【4】)
我們之前提到過(guò),SerialGC是需要將整個(gè)application暫停的,所以,這次GC將整個(gè)application暫停了0.89秒,就是這個(gè)暫停導(dǎo)致了系統(tǒng)throughput的下降;
圖中更有意思的現(xiàn)象是在橫軸1700附近,系統(tǒng)的throughput直線下降到幾乎為0。直觀的猜測(cè),這里應(yīng)該是發(fā)生了Full GC。果然,在log中找到了它:
1700.750: [GC 1700.750: [DefNew: 5335802K->302541K(5662336K), 0.9367800 secs ]1701.687: [Tenured: 4210828K->4211008K(4211008K), 17.6799010 secs ] 9526754K->4513370K(9873344K), [Perm : 11048K->11048K(21248K)], 18.6220490 secs] [Times: user=18.61 sys=0.01, real=18.62 secs ].
這里最重要的是這句:[Tenured: 4210828K->4211008K(4211008K), 17.6799010 secs] ,表示對(duì)tenured generation進(jìn)行了GC,花費(fèi)了17.6799010秒(由于我寫入的數(shù)據(jù)是要一直保存的,所以這次GC幾乎沒(méi)有在tenured generation中清除任何的dead object,所以下降幅度不大4210828K->4211008K)。
而這次GC總的花費(fèi)了18.62 secs,意味著系統(tǒng)在這18秒內(nèi)都被暫停了,在這18秒內(nèi)系統(tǒng)幾乎沒(méi)有任何的throughput。所以,采用Seral GC,意味著系統(tǒng)在發(fā)生Full GC的時(shí)候,將會(huì)有大概十幾秒的時(shí)間對(duì)外界的請(qǐng)求沒(méi)有響應(yīng),如果是一個(gè)Web Server的話,意味著這十幾秒用戶沒(méi)法瀏覽網(wǎng)頁(yè)。這個(gè)感覺可不好。
此外,log還記錄了Full GC發(fā)生前后Heap的情況:
{Heap before GC invocations=204 (full 0):
...........................................
tenured generation total 4194304K , used 4190951K [0x00007fe2c82e0000, 0x00007fe3c82e0000, 0x00007fe5082e0000)
the space 4194304K, 99% used [0x00007fe2c82e0000, 0x00007fe3c7f99dd8, 0x00007fe3c7f99e00, 0x00007fe3c82e0000)
........................................
........................................
Heap after GC invocations=205 (full 1):
....................................................................
tenured generation total 7018348K , used 4211008K [0x00007fe2c82e0000, 0x00007fe4748bb000, 0x00007fe5082e0000)
the space 7018348K, 59% used [0x00007fe2c82e0000, 0x00007fe3c9330000, 0x00007fe3c9330000, 0x00007fe4748bb000)
......................................................
}
從log可以看到,在GC發(fā)生之前,Heap里面的tenured generation的占用率已經(jīng)到了99%,意味著tenured generation已經(jīng)滿了。所以,可以判斷出,F(xiàn)ull GC發(fā)生的條件就是tenured generation已經(jīng)滿了。
而在GC發(fā)生之后,發(fā)現(xiàn)total space從4194304K增長(zhǎng)到了7018348K。還記得嗎,我們?cè)谠O(shè)置JVM的時(shí)候,初始Heap的Size是10G,最大的Size是15G,那多出的5G就是用于generation的增長(zhǎng)的。
最后,你能夠發(fā)現(xiàn),無(wú)論是Minor GC還是Major GC,它的user time和real time的值相差不大,如果你了解user time和real time的意義,就能夠知道這意味著GC這個(gè)任務(wù)是有單線程執(zhí)行的,才會(huì)出現(xiàn)這種情況。這也和SerialGC的概念相吻合了。
(未完待續(xù))
Reference:
【1】The Second Most Important GC Tuning Knob
【2】Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
【3】Our Collectors
【4】 user / sys /real time
聯(lián)系客服