一、參考資料:
- Tuning Garbage Collection with the 5.0 Java Virtual Machine 官方指南。
- Hotspot memory management whitepaper 官方白皮書。
- Java Tuning White Paper 官方文檔。
- FAQ about Garbage Collection in the Hotspot 官方FAQ,JVM1.4.2。
- Java HotSpot 虛擬機(jī)中的垃圾收集 JavaOne2004上的中文ppt
- A Collection of JVM Options JVM選項(xiàng)的超完整收集。
二、基本概念
1、堆(Heap)
JVM管理的內(nèi)存叫堆。在32Bit操作系統(tǒng)上有1.5G-2G的限制,而64Bit的就沒有。
JVM初始分配的內(nèi)存由-Xms指定,默認(rèn)是物理內(nèi)存的1/64但小于1G。
JVM最大分配的內(nèi)存由-Xmx指定,默認(rèn)是物理內(nèi)存的1/4但小于1G。
默認(rèn)空余堆內(nèi)存小于40%時(shí),JVM就會增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指定。
默認(rèn)空余堆內(nèi)存大于70%時(shí),JVM會減少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。
服務(wù)器一般設(shè)置-Xms、-Xmx相等以避免在每次GC 后調(diào)整堆的大小,所以上面的兩個(gè)參數(shù)沒啥用。
2.基本收集算法
- 復(fù)制:將堆內(nèi)分成兩個(gè)相同空間,從根(ThreadLocal的對象,靜態(tài)對象)開始訪問每一個(gè)關(guān)聯(lián)的活躍對象,將空間A的活躍對象全部復(fù)制到空間B,然后一次性回收整個(gè)空間A。
因?yàn)橹辉L問活躍對象,將所有活動(dòng)對象復(fù)制走之后就清空整個(gè)空間,不用去訪問死對象,所以遍歷空間的成本較小,但需要巨大的復(fù)制成本和較多的內(nèi)存。
- 標(biāo)記清除(mark-sweep):收集器先從根開始訪問所有活躍對象,標(biāo)記為活躍對象。然后再遍歷一次整個(gè)內(nèi)存區(qū)域,把所有沒有標(biāo)記活躍的對象進(jìn)行回收處理。該算法遍歷整個(gè)空間的成本較大暫停時(shí)間隨空間大小線性增大,而且整理后堆里的碎片很多。
- 標(biāo)記整理(mark-sweep-compact):綜合了上述兩者的做法和優(yōu)點(diǎn),先標(biāo)記活躍對象,然后將其合并成較大的內(nèi)存塊。
可見,沒有免費(fèi)的午餐,無論采用復(fù)制還是標(biāo)記清除算法,自動(dòng)的東西都要付出很大的性能代價(jià)。
3.分代
分代是Java垃圾收集的一大亮點(diǎn),根據(jù)對象的生命周期長短,把堆分為3個(gè)代:Young,Old和Permanent,根據(jù)不同代的特點(diǎn)采用不同的收集算法,揚(yáng)長避短也。
Young(Nursery),年輕代。研究表明大部分對象都是朝生暮死,隨生隨滅的。因此所有收集器都為年輕代選擇了復(fù)制算法。
復(fù)制算法優(yōu)點(diǎn)是只訪問活躍對象,缺點(diǎn)是復(fù)制成本高。因?yàn)槟贻p代只有少量的對象能熬到垃圾收集,因此只需少量的復(fù)制成本。而且復(fù)制收集器只訪問活躍對象,對那些占了最大比率的死對象視而不見,充分發(fā)揮了它遍歷空間成本低的優(yōu)點(diǎn)。
Young的默認(rèn)值為4M,隨堆內(nèi)存增大,約為1/15,JVM會根據(jù)情況動(dòng)態(tài)管理其大小變化。
-XX:NewRatio= 參數(shù)可以設(shè)置Young與Old的大小比例,-server時(shí)默認(rèn)為1:2,但實(shí)際上young啟動(dòng)時(shí)遠(yuǎn)低于這個(gè)比率?如果信不過JVM,也可以用-Xmn硬性規(guī)定其大小,有文檔推薦設(shè)為Heap總大小的1/4。
Young的大小非常非常重要,見“后面暫停時(shí)間優(yōu)先收集器”的論述。
Young里面又分為3個(gè)區(qū)域,一個(gè)Eden,所有新建對象都會存在于該區(qū),兩個(gè)Survivor區(qū),用來實(shí)施復(fù)制算法。每次復(fù)制就是將Eden和第一塊Survior的活對象復(fù)制到第2塊,然后清空Eden與第一塊Survior。Eden與Survivor的比例由-XX:SurvivorRatio=設(shè)置,默認(rèn)為32。Survivio大了會浪費(fèi),小了的話,會使一些年輕對象潛逃到老人區(qū),引起老人區(qū)的不安,但這個(gè)參數(shù)對性能并不重要。
Old(Tenured),年老代。年輕代的對象如果能夠挺過數(shù)次收集,就會進(jìn)入老人區(qū)。老人區(qū)使用標(biāo)記整理算法。因?yàn)槔先藚^(qū)的對象都沒那么容易死的,采用復(fù)制算法就要反復(fù)的復(fù)制對象,很不合算,只好采用標(biāo)記清理算法,但標(biāo)記清理算法其實(shí)也不輕松,每次都要遍歷區(qū)域內(nèi)所有對象,所以還是沒有免費(fèi)的午餐啊。
-XX:MaxTenuringThreshold=設(shè)置熬過年輕代多少次收集后移入老人區(qū),CMS中默認(rèn)為0,熬過第一次GC就轉(zhuǎn)入,可以用-XX:+PrintTenuringDistribution查看。
Permanent,持久代。裝載Class信息等基礎(chǔ)數(shù)據(jù),默認(rèn)64M,如果是類很多很多的服務(wù)程序,需要加大其設(shè)置-XX:MaxPermSize=,否則它滿了之后會引起fullgc()或Out of Memory。 注意Spring,Hibernate這類喜歡AOP動(dòng)態(tài)生成類的框架需要更多的持久代內(nèi)存。
4.minor/major collection
每個(gè)代滿了之后都會促發(fā)collection,(另外Concurrent Low Pause Collector默認(rèn)在老人區(qū)68%的時(shí)候促發(fā))。GC用較高的頻率對young進(jìn)行掃描和回收,這種叫做minor collection。
而因?yàn)槌杀娟P(guān)系對Old的檢查回收頻率要低很多,同時(shí)對Young和Old的收集稱為major collection。
System.gc()會引發(fā)major collection,使用-XX:+DisableExplicitGC禁止它,或設(shè)為CMS并發(fā)-XX:+ExplicitGCInvokesConcurrent。
5.小結(jié)
Young -- minor collection -- 復(fù)制算法
Old(Tenured) -- major colletion -- 標(biāo)記清除/標(biāo)記整理算法
三、收集器
1.古老的串行收集器(Serial Collector)
使用 -XX:+UseSerialGC,策略為年輕代串行復(fù)制,年老代串行標(biāo)記整理。
2.吞吐量優(yōu)先的并行收集器(Throughput Collector)
使用 -XX:+UseParallelGC ,也是JDK5 -server的默認(rèn)值。策略為:
1.年輕代暫停應(yīng)用程序,多個(gè)垃圾收集線程并行的復(fù)制收集,線程數(shù)默認(rèn)為CPU個(gè)數(shù),CPU很多時(shí),可用–XX:ParallelGCThreads=減少線程數(shù)。
2.年老代暫停應(yīng)用程序,與串行收集器一樣,單垃圾收集線程標(biāo)記整理。
所以需要2+的CPU時(shí)才會優(yōu)于串行收集器,適用于后臺處理,科學(xué)計(jì)算。
可以使用-XX:MaxGCPauseMillis= 和 -XX:GCTimeRatio 來調(diào)整GC的時(shí)間。
3.暫停時(shí)間優(yōu)先的并發(fā)收集器(Concurrent Low Pause Collector-CMS)
前面說了這么多,都是為了這節(jié)做鋪墊......
使用-XX:+UseConcMarkSweepGC,策略為:
1.年輕代同樣是暫停應(yīng)用程序,多個(gè)垃圾收集線程并行的復(fù)制收集。
2.年老代則只有兩次短暫停,其他時(shí)間應(yīng)用程序與收集線程并發(fā)的清除。
3.1 年老代詳述
并行(Parallel)與并發(fā)(Concurrent)僅一字之差,并行指多條垃圾收集線程并行,并發(fā)指用戶線程與垃圾收集線程并發(fā),程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)個(gè)CPU上。
并發(fā)收集一開始會很短暫的停止一次所有線程來開始初始標(biāo)記根對象,然后標(biāo)記線程與應(yīng)用線程一起并發(fā)運(yùn)行,最后又很短的暫停一次,多線程并行的重新標(biāo)記之前可能因?yàn)椴l(fā)而漏掉的對象,然后就開始與應(yīng)用程序并發(fā)的清除過程。可見,最長的兩個(gè)遍歷過程都是與應(yīng)用程序并發(fā)執(zhí)行的,比以前的串行算法改進(jìn)太多太多了!!!
串行標(biāo)記清除是等年老代滿了再開始收集的,而并發(fā)收集因?yàn)橐c應(yīng)用程序一起運(yùn)行,如果滿了才收集,應(yīng)用程序就無內(nèi)存可用,所以系統(tǒng)默認(rèn)68%滿的時(shí)候就開始收集。內(nèi)存已設(shè)得較大,吃內(nèi)存又沒有這么快的時(shí)候,可以用-XX:CMSInitiatingOccupancyFraction=恰當(dāng)增大該比率。
3.2 年輕代詳述
可惜對年輕代的復(fù)制收集,依然必須停止所有應(yīng)用程序線程,原理如此,只能靠多CPU,多收集線程并發(fā)來提高收集速度,但除非你的Server獨(dú)占整臺服務(wù)器,否則如果服務(wù)器上本身還有很多其他線程時(shí),切換起來速度就..... 所以,搞到最后,暫停時(shí)間的瓶頸就落在了年輕代的復(fù)制算法上。
因此Young的大小設(shè)置挺重要的,大點(diǎn)就不用頻繁GC,而且增大GC的間隔后,可以讓多點(diǎn)對象自己死掉而不用復(fù)制了。但Young增大時(shí),GC造成的停頓時(shí)間攀升得非常恐怖,比如在我的機(jī)器上,默認(rèn)8M的Young,只需要幾毫秒的時(shí)間,64M就升到90毫秒,而升到256M時(shí),就要到300毫秒了,峰值還會攀到恐怖的800ms。誰叫復(fù)制算法,要等Young滿了才開始收集,開始收集就要停止所有線程呢。
3.3 持久代
可設(shè)置-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled,使CMS收集持久代的類,而不是fullgc,netbeans5.5 performance文檔的推薦。
4.增量(train算法)收集器(Incremental Collector)
已停止維護(hù),–Xincgc選項(xiàng)默認(rèn)轉(zhuǎn)為并發(fā)收集器。
四、暫停時(shí)間顯示
加入下列參數(shù) (請將PrintGC和Details中間的空格去掉,CSDN很怪的認(rèn)為是禁止字句)
-verbose:gc -XX:+PrintGC Details -XX:+PrintGCTimeStamps
會程序運(yùn)行過程中將顯示如下輸出
9.211: [GC 9.211: [ParNew: 7994K->0K(8128K), 0.0123935 secs] 427172K->419977K(524224K), 0.0125728 secs]
顯示在程序運(yùn)行的9.211秒發(fā)生了Minor的垃圾收集,前一段數(shù)據(jù)針對新生區(qū),從7994k整理為0k,新生區(qū)總大小為8128k,程序暫停了12ms,而后一段數(shù)據(jù)針對整個(gè)堆。
對于年老代的收集,暫停發(fā)生在下面兩個(gè)階段,CMS-remark的中斷是17毫秒:
[GC [1 CMS-initial-mark: 80168K(196608K)] 81144K(261184K), 0.0059036 secs]
[1 CMS-remark: 80168K(196608K)] 82493K(261184K),0.0168943 secs]
再加兩個(gè)參數(shù) -XX:+PrintGCApplicationConcurrentTime -XX:+PrintGCApplicationStoppedTime對暫停時(shí)間看得更清晰。
五、真正不停的BEA JRockit 與Sun RTS2.0
Bea的JRockit 5.0 R27 的特色之一是動(dòng)態(tài)決定的垃圾收集策略,用戶可以決定自己關(guān)心的是吞吐量,暫停時(shí)間還是確定的暫停時(shí)間,再由JVM在運(yùn)行時(shí)動(dòng)態(tài)決定、改變改變垃圾收集策略。
它的Deterministic GC的選項(xiàng)是-Xgcprio: deterministic,號稱可以把暫停可以控制在10-30毫秒,非常的牛,一句Deterministic道盡了RealTime的真諦。 不過細(xì)看一下文檔,30ms的測試環(huán)境是1 GB heap 和 平均 30% 的活躍對象(也就是300M)活動(dòng)對象,2 個(gè) Xeon 3.6 GHz 4G內(nèi)存 ,或者是4 個(gè)Xeon 2.0 GHz,8G內(nèi)存。
最可惜JRockt的license很奇怪,雖然平時(shí)使用免費(fèi),但這個(gè)30ms的選項(xiàng)就需要購買整個(gè)Weblogic Real Time Server的license。
其他免費(fèi)選項(xiàng),有:
- -Xgcprio:pausetime -Xpausetarget=210ms
因?yàn)槊赓M(fèi),所以最低只能設(shè)置到200ms pause target。 200ms是Sun認(rèn)為Real-Time的分界線。
- -Xgc:gencon
普通的并發(fā)做法,效率也不錯(cuò)。
JavaOne2007上有Sun的 Java Real-Time System 2.0 的介紹,RTS2.0基于JDK1.5,在Real-Time Garbage Collctor上又有改進(jìn),但還在beta版狀態(tài),只供給OEM,更怪。
六、JDK 6.0的改進(jìn)
因?yàn)镴DK5.0在Young較大時(shí)的表現(xiàn)還是不夠讓人滿意,又繼續(xù)看JDK6.0的改進(jìn),結(jié)果稍稍失望,不涉及我最頭痛的年輕代復(fù)制收集改良。
1.年老代的標(biāo)識-清除收集,并行執(zhí)行標(biāo)識
JDK5.0只開了一條收集進(jìn)程與應(yīng)用線程并發(fā)標(biāo)識,而6.0可以開多條收集線程來做標(biāo)識,縮短標(biāo)識老人區(qū)所有活動(dòng)對象的時(shí)間。
2.加大了Young區(qū)的默認(rèn)大小
默認(rèn)大小從4M加到16M,從堆內(nèi)存的1/15增加到1/7
3.System.gc()可以與應(yīng)用程序并發(fā)執(zhí)行
使用-XX:+ExplicitGCInvokesConcurrent 設(shè)置
七、小結(jié)
1. JDK5.0/6.0
對于服務(wù)器應(yīng)用,我們使用Concurrent Low Pause Collector,對年輕代,暫停時(shí)多線程并行復(fù)制收集;對年老代,收集器與應(yīng)用程序并行標(biāo)記--整理收集,以達(dá)到盡量短的垃圾收集時(shí)間。
本著沒有深刻測試前不要胡亂優(yōu)化的宗旨,命令行屬性只需簡單寫為:
-server -Xms<heapsize>M -Xmx<heapsize>M -XX:+UseConcMarkSweepGC -XX:+PrintGC Details -XX:+PrintGCTimeStamps
然后要根據(jù)應(yīng)用的情況,在測試軟件輔助可以下看看有沒有JVM的默認(rèn)值和自動(dòng)管理做的不夠的地方可以調(diào)整,如-xmn 設(shè)Young的大小,-XX:MaxPermSize設(shè)持久代大小等。
2. JRockit 6.0 R27.2
但因?yàn)镴DK5的測試結(jié)果實(shí)在不能滿意,后來又嘗試了JRockit,總體效果要好些。
JRockit的特點(diǎn)是動(dòng)態(tài)垃圾收集器是根據(jù)用戶關(guān)心的特征動(dòng)態(tài)決定收集算法的,參數(shù)如下

-Xms<heapsize>M -Xmx<heapsize>M
-Xgcprio:pausetime -Xpausetarget=200ms -XgcReport -XgcPause -Xverbose:memory