JVM分代垃圾回收機(jī)制和垃圾回收算法
一、什么是GC
GC (Garbage Collection)垃圾回收,顧名思義就是專(zhuān)門(mén)回收垃圾的。,在C/C++中,我們需要用到內(nèi)存的時(shí)候,需要先手動(dòng)申明一下,使用完后又需要在手動(dòng)回收一下,這兩部非常麻煩而且還經(jīng)常會(huì)出這個(gè)方面的問(wèn)題。而這一切在Java中就已經(jīng)被自動(dòng)執(zhí)行掉了,所以我們寫(xiě)代碼的時(shí)候都不用再管這些無(wú)效的數(shù)據(jù)。
二、GC分類(lèi)
在目前主流的虛擬機(jī)中,大多都是根據(jù)分代收集的理論來(lái)進(jìn)行設(shè)計(jì)的。因?yàn)樵谔摂M機(jī)中絕大部分的對(duì)象都是朝生夕死的,而熬過(guò)了多次的垃圾回收后的對(duì)象就越難被回收。所以前面的理論堆就被劃分成了兩個(gè)區(qū)域,新生代和老年代,前者主要存儲(chǔ)那些朝生夕死的對(duì)象,后者存放難死的對(duì)象。
1、 新生代回收(Minor GC/Young GC):指只是進(jìn)行新生代的回收。
2、老年代回收(Major GC/Old GC):指只是進(jìn)行老年代的回收。目前只有 CMS 垃圾回收器會(huì)有這個(gè)單獨(dú)的回收老年代的行為。 (Major GC 定義是比較混亂,有說(shuō)指是老年代,有的說(shuō)是做整個(gè)堆的收集,這個(gè)需要你根據(jù)別人的場(chǎng)景來(lái)定,沒(méi)有固定的說(shuō)法)
3、整堆回收(Full GC):收集整個(gè) Java 堆和方法區(qū)(注意包含方法區(qū))
三、垃圾回收算法
1、復(fù)制算法(Copying)
將一塊內(nèi)存區(qū)域進(jìn)行對(duì)半分,當(dāng)有一半的內(nèi)存使用完時(shí)將還存活的對(duì)象放到另一半內(nèi)存區(qū)域中,原來(lái)的內(nèi)存區(qū)域進(jìn)行回收,不用考慮內(nèi)存碎片區(qū)域,只要按順序分配內(nèi)存就行。實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。
但是這樣也有個(gè)缺點(diǎn)就是對(duì)內(nèi)存的利用率只有50%,于是在JVM中就有了以下的解決辦法:
Appel式回收
Eden區(qū)的添加,一般來(lái)說(shuō)的內(nèi)存區(qū)域的分配為:Eden:80%,Survivor:20%(From 10%,To 10%),當(dāng)Survivor區(qū)不夠用的時(shí)候,就需要老年代進(jìn)行分配擔(dān)保。
2、標(biāo)記-清除法(Mark-Sweep)
算法分為“標(biāo)記”和“清理”兩個(gè)階段:第一步掃描需要標(biāo)記所有可以被回收的對(duì)象,第二遍掃描需要清理被第一步標(biāo)記的對(duì)象,效率略低。因?yàn)樾枰罅康臉?biāo)記對(duì)象和清除所以回收效率是不復(fù)制算法的,如果大部分的對(duì)象是朝生夕死的那么標(biāo)記的對(duì)象就會(huì)更多,效率會(huì)更低。
它還有個(gè)主要問(wèn)題就是會(huì)產(chǎn)生大量的內(nèi)存碎片導(dǎo)致大對(duì)象無(wú)法進(jìn)行存儲(chǔ),從而不得不提前觸發(fā)其他的垃圾回收。
3、標(biāo)記-整理法(Mark-Compact )
步驟與清除法步驟一致但是,它的第二步是整理標(biāo)記之外的所有對(duì)象,將所有對(duì)象向前移動(dòng)之后直接清除掉這些對(duì)象所在之外的內(nèi)存區(qū)域。標(biāo)記法不會(huì)存在內(nèi)存碎片,但是效率是遍低的。
整理法和清除法的主要區(qū)別就是一個(gè)是回收對(duì)象,一個(gè)整理對(duì)象,而移動(dòng)對(duì)象還會(huì)需要暫停所有的業(yè)務(wù)線(xiàn)程后更新所有對(duì)象的引用(直接指針需要調(diào)整)。
四、JVM垃圾回收器
1、Serial/Serial Old
JVM誕生初期所采用的垃圾回收器,單線(xiàn)程,獨(dú)占式,適合單CPU。
它只適合堆內(nèi)存幾十兆到幾百兆,如果超過(guò)的這個(gè)內(nèi)存的大小則會(huì)大大的降低回收效率,所以在目前很雞肋。
Stop The World(STW)
單線(xiàn)程進(jìn)行垃圾回收時(shí),必須暫停所有的工作線(xiàn)程,直到它回收結(jié)束。這個(gè)暫停稱(chēng)之為“Stop The World”,但是這種 STW 帶來(lái)了惡劣的用戶(hù)體驗(yàn),例如:應(yīng)用每運(yùn)行一個(gè)小時(shí)就需要暫停響應(yīng) 5 分。這個(gè)也是早期 JVM 和 java 被 C/C++ 語(yǔ)言詬病性能差的一個(gè)重要原因。所以 JVM 開(kāi)發(fā)團(tuán)隊(duì)一直努力消除或降低 STW 的時(shí)間。
2、Parallel/Parallel Old
為了提高JVM的回收效率,從JDK 1.3開(kāi)始,JVM使用了多線(xiàn)程的垃圾回收器,關(guān)注吞吐量的垃圾回收器,可以更高效的利用CPU時(shí)間,從而盡快完成程序的運(yùn)算任務(wù)。
所謂吞吐量就是 CPU 用于運(yùn)行用戶(hù)代碼的時(shí)間與 CPU 總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶(hù)代碼時(shí)間/(運(yùn)行用戶(hù)代碼時(shí)間+垃圾收集時(shí)間),虛擬機(jī)總 共運(yùn)行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是 99%。
該垃圾回收器適合回收堆空間上百兆~幾個(gè)G。
JVM參數(shù)設(shè)置
JDK1.8 默認(rèn)就是以下組合
-XX:+UseParallelGC 新生代使用 Parallel Scavenge,老年代使用 Parallel Old
-XX:MaxGCPauseMillis
不過(guò)不要異想天開(kāi)地認(rèn)為如果把這個(gè)參數(shù)的值設(shè)置得更小一點(diǎn)就能使得系統(tǒng)的垃圾收集速度變得更快,垃圾收集停頓時(shí)間縮短是以犧牲吞吐 量和新生代空間為代價(jià)換取的:系統(tǒng)把新生代調(diào)得小一些,收集 300MB 新生代肯定比收集 500MB 快,但這也直接導(dǎo)致垃圾收集發(fā)生得更頻繁,原來(lái) 10 秒收集一次、每次停頓 100 毫秒,現(xiàn)在變成 5 秒收集一次、 每次停頓 70 毫秒。停頓時(shí)間的確在下降,但吞吐量也降下來(lái)了。
-XX:GCTimeRatio
-XX:GCTimeRatio 參數(shù)的值則應(yīng)當(dāng)是一個(gè)大于 0 小于 100 的整數(shù),也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于吞吐量的倒數(shù)。
例如:把此參數(shù)設(shè)置為 19, 那允許的最大垃圾收集時(shí)占用總時(shí)間的 5% (即 1/(1+19)), 默認(rèn)值為 99,即允許最大 1% (即 1/(1+99))的垃圾收集時(shí)間由于與吞吐量關(guān)系密切,ParallelScavenge 是“吞吐量?jī)?yōu)先垃圾回收器”。
-XX:+UseAdaptiveSizePolicy
-XX:+UseAdaptiveSizePolicy (默認(rèn)開(kāi)啟)。這是一個(gè)開(kāi)關(guān)參數(shù), 當(dāng)這個(gè)參數(shù)被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden 與 Survivor 區(qū)的比例(-XX:SurvivorRatio)、 晉升老年代對(duì)象大小(-XX:PretenureSizeThreshold)等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量。
3、ParNew/CMS
ParNew
多線(xiàn)的垃圾回收器與Parallel差不多,唯一的區(qū)別:多線(xiàn)程,多 CPU 的,停頓時(shí)間比 Serial 少。(在 JDK9 以后,把 ParNew 合并到了 CMS 了) 。
Concurrent Mark Sweep(CMS)
此類(lèi)垃圾回收器是追求最短的回收停頓時(shí)間(STW)為目標(biāo)的。目前還有是很大一部分的 Java 應(yīng)用集中在互聯(lián)網(wǎng)或者 B/S 系統(tǒng)的服務(wù)端上,這類(lèi)應(yīng)用比較重視服務(wù)的響應(yīng)速度,希望停頓時(shí)間更短以提升用戶(hù)的體驗(yàn)。
Mark Sweep 從名字上可以看出來(lái),這個(gè)回收器采用的是標(biāo)記 - 清除法。而它的步驟比起前面的幾個(gè)回收器都更麻煩些。
整體過(guò)程分為 4 個(gè)步驟:
初始標(biāo)記:只標(biāo)記與 GC Root 有直接關(guān)聯(lián)的對(duì)象,這類(lèi)的對(duì)象比較少,標(biāo)記快。
并發(fā)標(biāo)記:標(biāo)記與初始化標(biāo)記的對(duì)象有關(guān)聯(lián)的所有對(duì)象,這類(lèi)的對(duì)象比較多所以采用的并發(fā),與用戶(hù)線(xiàn)程一起跑。
重新標(biāo)記:修正那些并發(fā)標(biāo)記時(shí)候標(biāo)記產(chǎn)生異動(dòng)的對(duì)象標(biāo)記,這塊的時(shí)間比初始標(biāo)記稍長(zhǎng)一些,但是比起并發(fā)標(biāo)記要快很多。
并發(fā)清除:與用戶(hù)線(xiàn)程一起運(yùn)行,進(jìn)行對(duì)象回收。
-XX:+UseConcMarkSweepGC ,表示新生代使用 ParNew,老年代的用 CMS。
缺點(diǎn):
CPU敏感:因?yàn)椴捎玫牟l(fā)的技術(shù)所以對(duì)處理器的核心要求較大。
浮動(dòng)垃圾:在CMS進(jìn)行并發(fā)清楚的時(shí)候因?yàn)椴捎玫氖遣l(fā)的輕快,所以在清除的時(shí)候用戶(hù)線(xiàn)程會(huì)產(chǎn)出新的垃圾。
因此在進(jìn)行回收的時(shí)候需要預(yù)留一部分的空間來(lái)存放這些產(chǎn)生垃圾(JDK 1.6 設(shè)置的閾值為92%)。
但是如果用戶(hù)線(xiàn)程產(chǎn)出的垃圾比較快,預(yù)留內(nèi)存放不下的時(shí)候就會(huì)出現(xiàn) Concurrent Mode Failure,這時(shí)虛擬機(jī)將臨時(shí)啟用 Serial Old 來(lái)替代 CMS。
內(nèi)存碎片:因?yàn)椴捎玫氖?標(biāo)記 - 清除 法所以會(huì)產(chǎn)生內(nèi)存碎片。
特點(diǎn):
總體來(lái)說(shuō)因?yàn)?CMS 是 JVM 產(chǎn)生的第一個(gè)并發(fā)垃圾收集器,所以還是具有代表性的。為什么采用 標(biāo)記 - 清除 法,因?yàn)樵趯?shí)現(xiàn) CMS 的時(shí)候如果還整理對(duì)象的話(huà),那么需要再暫停業(yè)務(wù)線(xiàn)程,進(jìn)行一個(gè)對(duì)象的整理那么 STW 的時(shí)間會(huì)更長(zhǎng),為了追求 STW 的時(shí)間所以沒(méi)有采用 標(biāo)記 - 整理。
但是最大的問(wèn)題是 CMS 采用了標(biāo)記清除算法,所以會(huì)有內(nèi)存碎片,當(dāng)碎片較多時(shí),給大對(duì)象的分配帶來(lái)很大的麻煩,為了解決這個(gè)問(wèn)題,CMS 提供一個(gè) 參數(shù):-XX:+UseCMSCompactAtFullCollection,一般是開(kāi)啟的,如果分配不了大對(duì)象,就進(jìn)行內(nèi)存碎片的整理過(guò)程。 這個(gè)地方一般會(huì)使用 Serial Old ,因?yàn)?Serial Old 是一個(gè)單線(xiàn)程,所以如果內(nèi)存空間很大、且對(duì)象較多時(shí),CMS 發(fā)生這樣情況會(huì)很卡。
該垃圾回收器適合回收堆空間幾個(gè) G~ 20G 左右。
4、 Garbage First (G1)
G1 垃圾回收器的設(shè)計(jì)思想與前面所有的垃圾回收器的都不一樣,前面垃圾回收器采用的都是 分代劃分 的方式進(jìn)行設(shè)計(jì)的,而 G1 則是將堆看作是一個(gè)整體的區(qū)域,這個(gè)區(qū)域被劃分成了一個(gè)個(gè)大小一致的獨(dú)立區(qū)域(Region),而每個(gè)區(qū)域都可以根據(jù)需要扮演Eden、Survivor以及老年代區(qū)域。當(dāng)進(jìn)行對(duì)象回收的時(shí)候就可以根據(jù)每個(gè)區(qū)域的情況進(jìn)行一個(gè)回收,從而效率。
Region
上面講到除了每個(gè)Region可以扮演不同的區(qū)域,還有一個(gè)類(lèi)似老年代的區(qū)域 Humongous 區(qū)域,用來(lái)專(zhuān)門(mén)存放大對(duì)象的。當(dāng)一個(gè)對(duì)象超過(guò)了Region區(qū)空間的一半大小則判定為大對(duì)象。(每個(gè) Region 的大小可以通過(guò)參數(shù)-XX:G1HeapRegionSize 設(shè)定,取值范圍為 1MB~32MB,且應(yīng)為 2 的 N 次 冪。)
而對(duì)于那些超過(guò)了整個(gè) Region 容量的超級(jí)大對(duì)象,將會(huì)被存放在 N 個(gè)連續(xù)的 Humongous Region 之中,G1 的進(jìn)行回收大多數(shù)情況下都把 Humongous Region 作為老年代的一部分來(lái)進(jìn)行看待。
開(kāi)啟參數(shù) :-XX:+UseG1GC `
分區(qū)大小:-XX:+G1HeapRegionSize
一般建議逐漸增大該值,隨著 size 增加,垃圾的存活時(shí)間更長(zhǎng),GC 間隔更長(zhǎng),但每次 GC 的時(shí)間也會(huì)更長(zhǎng)。
最大 GC 暫停時(shí)間 :-XX:MaxGCPauseMillis
運(yùn)行過(guò)程
G1 的運(yùn)作過(guò)程大致可劃分為以下四個(gè)步驟:
初始標(biāo)記 (Initial Marking) :標(biāo)記與 GC Roots 能關(guān)聯(lián)到的對(duì)象,修改 TAMS 指針的值,這個(gè)過(guò)程是需要暫停用戶(hù)線(xiàn)程的,但是耗時(shí)非常的短。
TAMS (Top at Mark Start):當(dāng)進(jìn)行下一步并發(fā)標(biāo)記的時(shí)候用戶(hù)線(xiàn)程是會(huì)產(chǎn)生新的對(duì)象的,而這些對(duì)象是被判定為可存活對(duì)象而非垃圾。這個(gè)時(shí)候就需要?jiǎng)澐忠恍K區(qū)域來(lái)存放這這些對(duì)象。
并發(fā)標(biāo)記 (Concurrent Marking):進(jìn)行掃描標(biāo)記所有課回收的對(duì)象。當(dāng)掃描完成后,并發(fā)會(huì)有引用變化的對(duì)象,而這些對(duì)象會(huì)漏標(biāo)這些漏標(biāo)的對(duì)象會(huì)被 SATB 算法所解決。
SATB(snapshot-at-the-beginning):類(lèi)似快照,對(duì)當(dāng)前區(qū)域進(jìn)行一個(gè)快照的保存,之后再最終標(biāo)記的時(shí)候進(jìn)行對(duì)比查看漏標(biāo)的會(huì)被重新標(biāo)記上(后面的文章會(huì)詳解)。
最終標(biāo)記 (Final Marking): 暫停所有的用戶(hù)線(xiàn)程,對(duì)之前漏標(biāo)的對(duì)象進(jìn)行一個(gè)標(biāo)記。
篩選回收( Live Data Counting and Evacuation):更新Region的統(tǒng)計(jì)數(shù)據(jù),對(duì)各個(gè) Region 的回收價(jià)值進(jìn)行一個(gè)排序,根據(jù)用戶(hù)所設(shè)置的停頓時(shí)間制定一個(gè)回收計(jì)劃,自由選擇任意個(gè) Region 進(jìn)行回收。將需要回收的Region 復(fù)制到空的 Region 區(qū)域中,再清除掉原來(lái)的整個(gè)Region區(qū)域。這塊還涉及到對(duì)象的移動(dòng)所以需要暫停所有的用戶(hù)線(xiàn)程,有多條回收器線(xiàn)程進(jìn)行完成。
特點(diǎn):
并行與并發(fā):G1 能充分利用多 CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè) CPU(CPU 或者 CPU 核心)來(lái)縮短 Stop-The-World 停頓的時(shí)間,部分其他收集器
原本需要停頓 Java 線(xiàn)程執(zhí)行的 GC 動(dòng)作,G1 收集器仍然可以通過(guò)并發(fā)的方式讓 Java 程序繼續(xù)執(zhí)行。
分代收集:與其他收集器一樣,分代概念在 G1 中依然得以保留。雖然 G1 可以不需要其他收集器配合就能獨(dú)立管理整個(gè) GC 堆,但它能夠采用不同的方式
去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過(guò)多次 GC 的舊對(duì)象以獲取更好的收集效果。
空間整合:與 CMS 的“標(biāo)記—清理”算法不同,G1 從整體來(lái)看是基于“標(biāo)記—整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè) Region 之間)上來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的,但無(wú)論如何,這兩種算法都意味著 G1 運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o(wú)法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次 GC。
追求停頓時(shí)間:-XX:MaxGCPauseMillis 指定目標(biāo)的最大停頓時(shí)間,G1 嘗試調(diào)整新生代和老年代的比例,堆大小,晉升年齡來(lái)達(dá)到這個(gè)目標(biāo)時(shí)間。
該垃圾回收器適合回收堆空間上百 G。一般在 G1 和 CMS 中間選擇的話(huà)平衡點(diǎn)在 6~8G,只有內(nèi)存比較大 G1 才能發(fā)揮優(yōu)勢(shì)