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