前段時(shí)間在一個(gè)項(xiàng)目的性能測(cè)試中又發(fā)生了一次OOM(Out of swap sapce),情形和以前網(wǎng)店版的那次差不多,比上次更奇怪的是,此次搞了幾天之后啥都沒調(diào)整系統(tǒng)就自動(dòng)好了,死活沒法再重現(xiàn)之前的OOM了!問題雖然蹊蹺,但也趁此機(jī)會(huì)再次對(duì)JVM堆模型、GC垃圾算法等進(jìn)行了一次系統(tǒng)梳理;
基本概念
堆/Heap
JVM管理的內(nèi)存叫堆;在32Bit操作系統(tǒng)上有4G的限制,一般來說Windows下為2G,而Linux下為3G;64Bit的就沒有這個(gè)限制。
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就會(huì)增大堆直到-Xmx的最大限制,可以由-XX:MinHeapFreeRatio=指定。
默認(rèn)空余堆內(nèi)存大于70%時(shí),JVM會(huì)減少堆直到-Xms的最小限制,可以由-XX:MaxHeapFreeRatio=指定。
服務(wù)器一般設(shè)置-Xms、-Xmx相等以避免在每次GC后調(diào)整堆的大小,所以上面的兩個(gè)參數(shù)沒啥用。
分代/堆模型
分代是Java垃圾收集的一大亮點(diǎn),根據(jù)對(duì)象的生命周期長(zhǎng)短,把堆分為3個(gè)代:Young,Old和Permanent,根據(jù)不同代的特點(diǎn)采用不同的收集算法,可以揚(yáng)長(zhǎng)避短??蓞⒖既缦碌哪P蛨D:
Young(Nursery):年輕代
研究表明大部分對(duì)象都是朝生暮死,隨生隨滅的。所以對(duì)于年輕代在GC時(shí)都采取復(fù)制收集算法,具體算法參考下面的描述;
Young的默認(rèn)值為4M,隨堆內(nèi)存增大,約為1/15,JVM會(huì)根據(jù)情況動(dòng)態(tài)管理其大小變化。
Young里面又分為3個(gè)區(qū)域,一個(gè)Eden,所有新建對(duì)象都會(huì)存在于該區(qū),兩個(gè)Survivor區(qū),用來實(shí)施復(fù)制算法。
-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。
-XX:SurvivorRatio= 參數(shù)可以設(shè)置Eden與Survivor的比例,默認(rèn)為32。Survivio大了會(huì)浪費(fèi),小了的話,會(huì)使一些年輕對(duì)象潛逃到老人區(qū),引起老人區(qū)的不安,但這個(gè)參數(shù)對(duì)性能并不太重要。
Old(Tenured):年老代
年輕代的對(duì)象如果能夠挺過數(shù)次收集,就會(huì)進(jìn)入老人區(qū)。老人區(qū)使用標(biāo)記整理算法。因?yàn)槔先藚^(qū)的對(duì)象都沒那么容易死的,采用復(fù)制算法就要反復(fù)的復(fù)制對(duì)象,很不合算,只好采用標(biāo)記清理算法,但標(biāo)記清理算法其實(shí)也不輕松,每次都要遍歷區(qū)域內(nèi)所有對(duì)象,所以還是沒有免費(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=,否則它滿了之后會(huì)引起fullgc()或Out of Memory。 注意Spring,Hibernate這類喜歡AOP動(dòng)態(tài)生成類的框架需要更多的持久代內(nèi)存。一般情況下,持久代是不會(huì)進(jìn)行GC的,除非通過-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled進(jìn)行強(qiáng)制設(shè)置。
GC的類型
當(dāng)每個(gè)代滿了之后都會(huì)自動(dòng)促發(fā)collection,各收集器觸發(fā)的條件不一樣,當(dāng)然也可以通過一些參數(shù)進(jìn)行強(qiáng)制設(shè)定。主要分為兩種類型:
- Minor Collection:GC用較高的頻率對(duì)young進(jìn)行掃描和回收,采用復(fù)制算法。
- Major Collection:同時(shí)對(duì)Young和Old進(jìn)行內(nèi)存收集,也叫Full GC;因?yàn)槌杀娟P(guān)系對(duì)Old的檢查回收頻率要比Young低很多,采用標(biāo)記清除/標(biāo)記整理算法。可以通過調(diào)用代碼System.gc()引發(fā)major collection,使用-XX:+DisableExplicitGC禁止它,或設(shè)為CMS并發(fā)-XX:+ExplicitGCInvokesConcurrent。
更為具體的闡述如下:
由于年輕代進(jìn)進(jìn)出出的人多而頻繁,所以年輕代的GC也就頻繁一點(diǎn),但涉及范圍也就年輕代這點(diǎn)彈丸之地內(nèi)的對(duì)象,其特點(diǎn)就是少量,多次,但快速,稱之為Minor Collection。當(dāng)年輕代的內(nèi)存使用達(dá)到一定的閥值時(shí),Minor Collection就被觸發(fā),Eden及某一Survior space(from space)之內(nèi)存活的的對(duì)象被移到另一個(gè)空的Survior space(to space)中,然后from space和to space角色對(duì)調(diào)。當(dāng)一個(gè)對(duì)象在兩個(gè)survivor space之間移動(dòng)過一定次數(shù)(達(dá)到預(yù)設(shè)的閥值)時(shí),它就足夠old了,夠資格呆在年老代了。當(dāng)然,如果survivor
space比較小不足以容下所有l(wèi)ive objects時(shí),部分live objects也會(huì)直接晉升到年老代。
Survior spaces可以看作是Eden和年老代之間的緩沖,通過該緩沖可以檢驗(yàn)一個(gè)對(duì)象生命周期是否足夠的長(zhǎng),因?yàn)槟承?duì)象雖然逃過了一次Minor Collection,并不能說明其生命周期足夠長(zhǎng),說不定在下一次Minor Collection之前就掛了。這樣一定程度上確保了進(jìn)入年老代的對(duì)象是貨真價(jià)實(shí)的,減少了年老代空間使用的增長(zhǎng)速度,也就降低年老代GC的頻率。
當(dāng)年老代或者永久代的內(nèi)存使用達(dá)到一定閥值時(shí),一次基于所有代的GC就觸發(fā)了,其特定是涉及范圍廣(量大),耗費(fèi)的時(shí)間相對(duì)較長(zhǎng)(較慢),但是頻率比較低(次數(shù)少),稱之為Major Collection(Full Collection)。通常,首先使用針對(duì)年輕代的GC算法進(jìn)行年輕代的GC,然后使用針對(duì)年老代的GC算法對(duì)年老代和永久代進(jìn)行GC。
基本GC收集算法
- 復(fù)制(copying):將堆內(nèi)分成兩個(gè)相同空間,從根(ThreadLocal的對(duì)象,靜態(tài)對(duì)象)開始訪問每一個(gè)關(guān)聯(lián)的活躍對(duì)象,將空間A的活躍對(duì)象全部復(fù)制到空間B,然后一次性回收整個(gè)空間A。
因?yàn)橹辉L問活躍對(duì)象,將所有活動(dòng)對(duì)象復(fù)制走之后就清空整個(gè)空間,不用去訪問死對(duì)象,所以遍歷空間的成本較小,但需要巨大的復(fù)制成本和較多的內(nèi)存。可參考如下的示例圖:

- 標(biāo)記清除(mark-sweep):收集器先從根開始訪問所有活躍對(duì)象,標(biāo)記為活躍對(duì)象。然后再遍歷一次整個(gè)內(nèi)存區(qū)域,把所有沒有標(biāo)記活躍的對(duì)象進(jìn)行回收處理。該算法遍歷整個(gè)空間的成本較大暫停時(shí)間隨空間大小線性增大,而且整理后堆里的碎片很多。可參考如下的示例圖:

- 標(biāo)記整理(mark-sweep-compact):綜合了上述兩者的做法和優(yōu)點(diǎn),先標(biāo)記活躍對(duì)象,然后將其合并成較大的內(nèi)存塊。可參考如下的示例圖:

GC收集器類型
- 古老的串行收集器(Serial Collector)
-XX:+UseSerialGC:策略為年輕代串行復(fù)制,年老代串行標(biāo)記整理??蓞⒖既缦碌氖纠龍D:
- 吞吐量?jī)?yōu)先的并行收集器(Throughput Collector)
-XX:+UseParallelGC:這是JDK5 -server的默認(rèn)值。策略為:
年輕代:暫停應(yīng)用程序,多個(gè)垃圾收集線程并行的復(fù)制收集,線程數(shù)默認(rèn)為CPU個(gè)數(shù),CPU很多時(shí),可用-XX:ParallelGCThreads= 設(shè)定線程數(shù)。
年老代:暫停應(yīng)用程序,與串行收集器一樣,單垃圾收集線程標(biāo)記整理。
如上可知該收集器需要2+的CPU時(shí)才會(huì)優(yōu)于串行收集器,適用于后臺(tái)處理,科學(xué)計(jì)算。
可以使用-XX:MaxGCPauseMillis= 和 -XX:GCTimeRatio 來調(diào)整GC的時(shí)間??蓞⒖既缦碌氖纠龍D:
- 暫停時(shí)間優(yōu)先的并發(fā)收集器(Concurrent Low Pause Collector-CMS)
-XX:+UseConcMarkSweepGC:這是以上兩種策略的升級(jí)版,策略為:
年輕代:同樣是暫停應(yīng)用程序,多個(gè)垃圾收集線程并行的復(fù)制收集。
年老代:則只有兩次短暫停,其他時(shí)間應(yīng)用程序與收集線程并發(fā)的清除。
若要采用標(biāo)記整理算法,則可以通過設(shè)置參數(shù)實(shí)現(xiàn);可參考如下的示例圖:
- 增量并發(fā)收集器(Incremental Concurrent-Mark-Sweep/i-CMS):雖然CMS收集算法在最為耗時(shí)的內(nèi)存區(qū)域遍歷時(shí)采用多線程并發(fā)操作,但對(duì)于服務(wù)器CPU資源不夠的情況下,其實(shí)對(duì)性能是沒有提升的,反而會(huì)導(dǎo)致系統(tǒng)吞吐量的下降,為了盡量避免這種情況的出現(xiàn),就有了增量CMS收集算法,就是在并發(fā)標(biāo)記、清理的時(shí)候讓GC線程、用戶線程交叉運(yùn)行,盡量減少GC線程的全程獨(dú)占式執(zhí)行;可參考如下的示例圖:

對(duì)于以上的GC收集器的詳細(xì)設(shè)置參數(shù),可以參考JVM選項(xiàng)的超完整收集《A Collection of JVM Options》,這里就不一一詳述了。
并行、并發(fā)的區(qū)別
并行(Parallel)與并發(fā)(Concurrent)僅一字之差,但體現(xiàn)的意思卻完全不同,這可能也是很多同學(xué)非常困惑的地方,要想深刻體會(huì)這其中的差別,可以多揣摩下上面關(guān)于GC收集器的示例圖;
- 并行:指多條垃圾收集線程并行,此時(shí)用戶線程是沒有運(yùn)行的;
- 并發(fā):指用戶線程與垃圾收集線程并發(fā)執(zhí)行,程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)個(gè)CPU上。
并發(fā)收集一開始會(huì)很短暫的停止一次所有線程來開始初始標(biāo)記根對(duì)象,然后標(biāo)記線程與應(yīng)用線程一起并發(fā)運(yùn)行,最后又很短的暫停一次,多線程并行的重新標(biāo)記之前可能因?yàn)椴l(fā)而漏掉的對(duì)象,然后就開始與應(yīng)用程序并發(fā)的清除過程。可見,最長(zhǎng)的兩個(gè)遍歷過程都是與應(yīng)用程序并發(fā)執(zhí)行的,比以前的串行算法改進(jìn)太多太多了?。?!
串行標(biāo)記清除是等年老代滿了再開始收集的,而并發(fā)收集因?yàn)橐c應(yīng)用程序一起運(yùn)行,如果滿了才收集,應(yīng)用程序就無(wú)內(nèi)存可用,所以系統(tǒng)默認(rèn)68%滿的時(shí)候就開始收集。內(nèi)存已設(shè)得較大,吃內(nèi)存又沒有這么快的時(shí)候,可以用-XX:CMSInitiatingOccupancyFraction=恰當(dāng)增大該比率。
年輕代的痛
由于對(duì)年輕代的復(fù)制收集,依然必須停止所有應(yīng)用程序線程,原理如此,只能靠多CPU,多收集線程并發(fā)來提高收集速度,但除非你的Server獨(dú)占整臺(tái)服務(wù)器,否則如果服務(wù)器上本身還有很多其他線程時(shí),切換起來速度就..... 所以,搞到最后,暫停時(shí)間的瓶頸就落在了年輕代的復(fù)制算法上。
因此Young的大小設(shè)置挺重要的,大點(diǎn)就不用頻繁GC,而且增大GC的間隔后,可以讓多點(diǎn)對(duì)象自己死掉而不用復(fù)制了。但Young增大時(shí),GC造成的停頓時(shí)間攀升得非??植溃瑩?jù)某人的測(cè)試結(jié)果顯示:默認(rèn)8M的Young,只需要幾毫秒的時(shí)間,64M就升到90毫秒,而升到256M時(shí),就要到300毫秒了,峰值還會(huì)攀到恐怖的800ms。誰(shuí)叫復(fù)制算法,要等Young滿了才開始收集,開始收集就要停止所有線程呢。
參考資料
主要參考:
官方指南:Tuning Garbage Collection with the 5.0 Java Virtual Machine