理解Java垃圾回收

在開始學(xué)習(xí)GC之前你應(yīng)該知道一個(gè)詞:stop-the-world。不管選擇哪種GC算法,stop-the-world都是不可避免的。Stop-the-world意味著從應(yīng)用中停下來并進(jìn)入到GC執(zhí)行過程中去。一旦Stop-the-world發(fā)生,除了GC所需的線程外,其他線程都將停止工作,中斷了的線程直到GC任務(wù)結(jié)束才繼續(xù)它們的任務(wù)。GC調(diào)優(yōu)通常就是為了改善stop-the-world的時(shí)間。

基于的分代理論的垃圾回收

在Java程序里不需要顯式的分配和釋放內(nèi)存。有些人通過給對(duì)象賦值為null或調(diào)用System.gc()以期望顯式的釋放內(nèi)存空間。給對(duì)象設(shè)置null雖沒什么用,但問題不會(huì)太大;如果調(diào)用了System.gc()卻可能會(huì)為系統(tǒng)性能帶來嚴(yán)重的波動(dòng),即便調(diào)用System.gc()系統(tǒng)也未必立即響應(yīng)去執(zhí)行垃圾回收。(所幸的是,在NHN未曾看到有工程師這么做。)

在使用Java時(shí),程序員不需要在程序代碼中顯式的釋放內(nèi)存空間,垃圾回收器會(huì)幫你找到不再需要的(垃圾)對(duì)象并把他們移出。垃圾回收器的創(chuàng)建基于以下兩個(gè)假設(shè)(也許稱之為推論或前提更合適):

  • 大多數(shù)對(duì)象的很快就會(huì)變得不可達(dá)
  • 只有極少數(shù)情況會(huì)出現(xiàn)舊對(duì)象持有新對(duì)象的引用

這兩條假設(shè)被稱為"弱分代假設(shè)"。為了證明此假設(shè),在HotSpot VM中物理內(nèi)存空間被劃分為兩部分:新生代(young generate)和老年代(old generation)。

新生代:大部分的新創(chuàng)建對(duì)象分配在新生代。因?yàn)榇蟛糠謱?duì)象很快就會(huì)變得不可達(dá),所以它們被分配在新生代,然后消失不再。當(dāng)對(duì)象從新生代移除時(shí),我們稱之為"minor GC"。

老年代:存活在新生代中但未變?yōu)椴豢蛇_(dá)的對(duì)象會(huì)被復(fù)制到老年代。一般來說老年代的內(nèi)存空間比新生代大,所以在老年代GC發(fā)生的頻率較新生代低一些。當(dāng)對(duì)象從老年代被移除時(shí),我們稱之為"major GC"(或者full GC)。

看一下下圖的示意:


GC區(qū)域和數(shù)據(jù)流向

圖中的permanent generation稱為方法區(qū),其中存儲(chǔ)著類和接口的元信息以及interned的字符串信息。所以這一區(qū)域并不是為老年代中存活下來的對(duì)象所定義的持久區(qū)。方法區(qū)中也會(huì)發(fā)生GC,這里的GC同樣也被稱為major GC。

有些人可能認(rèn)為:

如果老年代的對(duì)象需要持有新生代對(duì)象的引用怎么辦?

為了處理這種場(chǎng)景,在老年代中設(shè)計(jì)了"索引表(card table)",是一個(gè)512字節(jié)的數(shù)據(jù)塊。不管何時(shí)老年代需要持有新生代對(duì)象的引用時(shí),都會(huì)記錄到此表中。當(dāng)新生代中需要執(zhí)行GC時(shí),通過搜索此表決定新生代的對(duì)象是否為GC的目標(biāo)對(duì)象,從而降低遍歷所有老年代對(duì)象進(jìn)行檢查的代價(jià)。該索引表使用寫柵欄(write barrier)進(jìn)行管理。wite barrier是一個(gè)允許高性能執(zhí)行minor GC的設(shè)備。盡管它會(huì)引入一定的開銷,卻能帶來總體GC時(shí)間的大幅降低。

索引表結(jié)構(gòu)

新生代的結(jié)構(gòu)

為了深入理解GC,我們先從新生代開始學(xué)起。所有的對(duì)象在初始創(chuàng)建時(shí)都會(huì)被分配在新生代中。新生代又可分為三個(gè)部分:

  • 一個(gè)Eden區(qū)
  • 兩個(gè)Survivor區(qū)

在三個(gè)區(qū)域中有兩個(gè)是Survivor區(qū)。對(duì)象在三個(gè)區(qū)域中的存活過程如下:

  1. 大多數(shù)新生對(duì)象都被分配在Eden區(qū)。
  2. 第一次GC過后Eden中還存活的對(duì)象被移到其中一個(gè)Survivor區(qū)。
  3. 再次GC過程中,Eden中還存活的對(duì)象會(huì)被移到之前已移入對(duì)象的Survivor區(qū)。
  4. 一旦該Survivor區(qū)域無空間可用時(shí),還存活的對(duì)象會(huì)從當(dāng)前Survivor區(qū)移到另一個(gè)空的Survivor區(qū)。而當(dāng)前Survivor區(qū)就會(huì)再次置為空狀態(tài)。
  5. 經(jīng)過數(shù)次在兩個(gè)Survivor區(qū)域移動(dòng)后還存活的對(duì)象最后會(huì)被移動(dòng)到老年代。

如上所述,兩個(gè)Survivor區(qū)域在任何時(shí)候必定有一個(gè)保持空白。如果同時(shí)有數(shù)據(jù)存在于兩個(gè)Survivor區(qū)或者兩個(gè)區(qū)域的的使用量都是0,則意味著你的系統(tǒng)可能出現(xiàn)了運(yùn)行錯(cuò)誤。

下圖向你展示了經(jīng)過minor GC把數(shù)據(jù)遷移到老年代的過程:


GC前后

在HotSpot VM中,使用了兩項(xiàng)技術(shù)來實(shí)現(xiàn)更快的內(nèi)存分配:"指針碰撞(bump-the-pointer)"和"TLABs(Thread-Local Allocation Buffers)"。

Bump-the-pointer技術(shù)會(huì)跟蹤在Eden上新創(chuàng)建的對(duì)象。由于新對(duì)象被分配在Eden空間的最上面,所以后續(xù)如果有新對(duì)象創(chuàng)建,只需要判斷新創(chuàng)建對(duì)象的大小是否滿足剩余的Eden空間。如果新對(duì)象滿足要求,則其會(huì)被分配到Eden空間,同樣位于Eden的最上面。所以當(dāng)有新對(duì)象創(chuàng)建時(shí),只需要判斷此新對(duì)象的大小即可,因此具有更快的內(nèi)存分配速度。然而,在多線程環(huán)境下,將會(huì)有別樣的狀況。為了滿足多個(gè)線程在Eden空間上創(chuàng)建對(duì)象時(shí)的線程安全,不可避免的會(huì)引入鎖,因此隨著鎖競(jìng)爭(zhēng)的開銷,創(chuàng)建對(duì)象的性能也大打折扣。在HotSpot中正是通過TLABs解決了多線程問題。TLABs允許每個(gè)線程在Eden上有自己的小片空間,線程只能訪問其自己的TLAB區(qū)域,因此bump-the-pointer能通過TLAB在不加鎖的情況下完成快速的內(nèi)存分配。

本小節(jié)快速瀏覽了新生代上的GC知識(shí)。上面講的兩項(xiàng)技術(shù)無需刻意記憶,只需要明白對(duì)象開始是創(chuàng)建在Eden區(qū),然后經(jīng)過在Survivor區(qū)域上的數(shù)次轉(zhuǎn)移而存活下來的長壽對(duì)象最后會(huì)被移到老年代

新生代垃圾回收

在新生代中,使用“停止-復(fù)制”算法進(jìn)行清理,將新生代內(nèi)存分為2部分,1部分 Eden區(qū)較大,1部分Survivor比較小,并被劃分為兩個(gè)等量的部分。每次進(jìn)行清理時(shí),將Eden區(qū)和一個(gè)Survivor中仍然存活的對(duì)象拷貝到 另一個(gè)Survivor中,然后清理掉Eden和剛才的Survivor。

這里也可以發(fā)現(xiàn),停止復(fù)制算法中,用來復(fù)制的兩部分并不總是相等的(傳統(tǒng)的停止復(fù)制算法兩部分內(nèi)存相等,但新生代中使用1個(gè)大的Eden區(qū)和2個(gè)小的Survivor區(qū)來避免這個(gè)問題)

由于絕大部分的對(duì)象都是短命的,甚至存活不到Survivor中,所以,Eden區(qū)與Survivor的比例較大,HotSpot默認(rèn)是 8:1,即分別占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的內(nèi)存超過了10%,則需要將一部分對(duì)象分配到 老年代。用-XX:SurvivorRatio參數(shù)來配置Eden區(qū)域Survivor區(qū)的容量比值,默認(rèn)是8,代表Eden:Survivor1:Survivor2=8:1:1.

老年代垃圾回收

老年代用的算法是標(biāo)記-整理算法,即:標(biāo)記出仍然存活的對(duì)象(存在引用的),將所有存活的對(duì)象向一端移動(dòng),以保證內(nèi)存的連續(xù)。

在發(fā)生Minor GC時(shí),虛擬機(jī)會(huì)檢查每次晉升進(jìn)入老年代的大小是否大于老年代的剩余空間大小,如果大于,則直接觸發(fā)一次Full GC,否則,就查看是否 設(shè)置了-XX:+HandlePromotionFailure(允許擔(dān)保失敗),如果允許,則只會(huì)進(jìn)行MinorGC,此時(shí)可以容忍內(nèi)存分配失敗;如果不允許,則仍然進(jìn)行Full GC(這代表著如果設(shè)置-XX:+Handle PromotionFailure,則觸發(fā)MinorGC就會(huì)同時(shí)觸發(fā)Full GC,哪怕老年代還有很多內(nèi)存,所以,最好不要這樣做)。

當(dāng)老年代數(shù)據(jù)滿時(shí),便會(huì)執(zhí)行老年代垃圾回收。根據(jù)GC算法的不同其執(zhí)行過程也會(huì)有所區(qū)別,所以當(dāng)你了解了每種GC的特點(diǎn)后再來理解老年代的垃圾回收就會(huì)容易很多。

垃圾收集器

在GC機(jī)制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具體實(shí)現(xiàn),Java虛擬機(jī)規(guī)范中對(duì)于垃圾收集器沒有任何規(guī)定,所以不同廠商實(shí)現(xiàn)的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下圖(圖來源于《深入理解Java虛擬機(jī):JVM高級(jí)特效與最佳實(shí)現(xiàn)》,圖中兩個(gè)收集器之間有連線,說明它們可以配合使用):

在介紹垃圾收集器之前,需要明確一點(diǎn),就是在新生代采用的停止復(fù)制算法中,“停 止(Stop-the-world)”的意義是在回收內(nèi)存時(shí),需要暫停其他所 有線程的執(zhí)行。這個(gè)是很低效的,現(xiàn)在的各種新生代收集器越來越優(yōu)化這一點(diǎn),但仍然只是將停止的時(shí)間變短,并未徹底取消停止。

  • Serial收集器:新生代收集器,使用停止復(fù)制算法,使用一個(gè)線程進(jìn)行GC,串行,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運(yùn)行進(jìn)行內(nèi)存回收(這也是虛擬機(jī)在Client模式下運(yùn)行的默認(rèn)值)

  • ParNew收集器:新生代收集器,使用停止復(fù)制算法,Serial收集器的多線程版,用多個(gè)線程進(jìn)行GC,并行,其它工作線程暫停,關(guān)注縮短垃圾收集時(shí)間。使用-XX:+UseParNewGC開關(guān)來控制使用ParNew+Serial Old收集器組合收集內(nèi)存;使用-XX:ParallelGCThreads來設(shè)置執(zhí)行內(nèi)存回收的線程數(shù)。

  • Parallel Scavenge 收集器:新生代收集器,使用停止復(fù)制算法,關(guān)注CPU吞吐量,即運(yùn)行用戶代碼的時(shí)間/總時(shí)間,比如:JVM運(yùn)行100分鐘,其中運(yùn)行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運(yùn)行后臺(tái)運(yùn)算(關(guān)注縮短垃圾收集時(shí)間的收集器,如CMS,等待時(shí)間很少,所以適 合用戶交互,提高用戶體驗(yàn))。使用-XX:+UseParallelGC開關(guān)控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認(rèn)值);使用-XX:GCTimeRatio來設(shè)置用戶執(zhí)行時(shí)間占總時(shí)間的比例,默認(rèn)99,即1%的時(shí)間用來進(jìn)行垃圾回收。使用-XX:MaxGCPauseMillis設(shè)置GC的最大停頓時(shí)間(這個(gè)參數(shù)只對(duì)Parallel Scavenge有效),用開關(guān)參數(shù)-XX:+UseAdaptiveSizePolicy可以進(jìn)行動(dòng)態(tài)控制,如自動(dòng)調(diào)整Eden/Survivor比例,老年代對(duì)象年齡,新生代大小等,這個(gè)參數(shù)在ParNew下沒有。

  • Serial Old收集器:老年代收集器,單線程收集器,串行,使用標(biāo)記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對(duì)象干掉,只留幸存的對(duì)象,壓縮是將移動(dòng)對(duì)象,將空間填滿保證內(nèi)存分為2塊,一塊全是對(duì)象,一塊空閑)算法,使用單線程進(jìn)行GC,其它工作線程暫停(注意,在老年代中進(jìn)行標(biāo)記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。

  • Parallel Old收集器:老年代收集器,多線程,并行,多線程機(jī)制與Parallel Scavenge差不錯(cuò),使用標(biāo)記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對(duì)象復(fù)制到預(yù)先準(zhǔn)備好的區(qū)域,而不是像Sweep(清理)那樣清理廢棄的對(duì)象)算法,在Parallel Old執(zhí)行時(shí),仍然需要暫停其它線程。Parallel Old在多核計(jì)算中很有用。Parallel Old出現(xiàn)后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現(xiàn)Parallel Scavenge收集器吞吐量?jī)?yōu)先的效果。使用-XX:+UseParallelOldGC開關(guān)控制使用Parallel Scavenge +Parallel Old組合收集器進(jìn)行收集。

  • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于獲取最短回收停頓時(shí)間(即縮短垃圾回收的時(shí)間),使用標(biāo)記清除算法,多線程,優(yōu)點(diǎn)是并發(fā)收集(用戶線程可以和GC線程同時(shí)工作),停頓小。使用-XX:+UseConcMarkSweepGC進(jìn)行ParNew+CMS+Serial Old進(jìn)行內(nèi)存回收,優(yōu)先使用ParNew+CMS(原因見后面),當(dāng)用戶線程內(nèi)存不足時(shí),采用備用方案Serial Old收集。CMS收集的執(zhí)行過程是:初始標(biāo)記(CMS-initial-mark) -> 并發(fā)標(biāo)記(CMS-concurrent-mark) -->預(yù)清理(CMS-concurrent-preclean)-->可控預(yù)清理(CMS-concurrent-abortable-preclean)-> 重新標(biāo)記(CMS-remark) -> 并發(fā)清除(CMS-concurrent-sweep) ->并發(fā)重設(shè)狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset)

在JDK 7中,內(nèi)置了5種GC類型:

  1. Serial GC
  2. Parallel GC
  3. Parallel Old GC(Parallel Compacting GC)
  4. Concurrent Mark & Sweep GC (or "CMS")
  5. Garbage First (G1) GC

其中Serial GC務(wù)必不要在生產(chǎn)環(huán)境的服務(wù)器上使用,這種GC是為單核CPU上的桌面應(yīng)用設(shè)計(jì)的。使用Serial GC會(huì)明顯的損耗應(yīng)用的性能。

下面分別介紹每種GC的特性。

Serial GC(-XX:+UseSerialGC)

在前面介紹的年輕代垃圾回收中使用了這種類型的GC。在老年代,則使用了一種稱之為"mark-sweep-compact"的算法。

  1. 首先該算法需要在老年代中標(biāo)記出存活著的對(duì)象
  2. 然后從前到后檢查堆空間中存活的對(duì)象,并保持位置不變(把不再存活的對(duì)象清理出堆空間,稱為空間清理)
  3. 最后,把存活的對(duì)象移到堆空間的前面部分以保持已使用的堆空間的連續(xù)性,從而把堆空間分為兩部分:有對(duì)象的和無對(duì)象的(稱為空間壓縮)

Serial GC適用于CPU核數(shù)較少且使用的內(nèi)存空間較小的場(chǎng)景。

Parallel GC(-XX:+UseParallelGC)

Serial GC與Parallel GC的區(qū)別

圖中可以容易的看出serial GC與parallel GC的區(qū)別。Serial GC使用單一線程執(zhí)行GC,而parallel GC則使用多個(gè)線程并發(fā)執(zhí)行,因此parallel GC 較serial GC具有更快的速度。Parallel GC適用于多核CPU且使用了較大內(nèi)存空間的場(chǎng)景。Parallel GC又被稱為"高吞吐GC(throughput GC)"

Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC在JDK 5中被引入,與Parallel GC相比唯一的區(qū)別在于Parallel的GC算法是為老年代設(shè)計(jì)的。它的執(zhí)行過程分為三步:標(biāo)記(mark)--總結(jié)(summary)--壓縮(compaction)。其中summary步驟會(huì)會(huì)分別為存活的對(duì)象在已執(zhí)行過GC的空間上標(biāo)出位置,因此與mark-sweep-compact算法中的sweep步驟有所區(qū)別,并需要一些復(fù)雜步驟才能完成。

CMS GC(-XX:+UseConcMarkSweepGC)

Serial GC與CMS GC

從圖上可看出并發(fā)標(biāo)記-清理(Concurrent Mark-Sweep) GC比以后上其他GC都要復(fù)雜。開始時(shí)的初始標(biāo)記(initial mark)比較簡(jiǎn)單,只有靠近類加載器的存活對(duì)象會(huì)被標(biāo)記,因此停頓時(shí)間(stop-the-world)比較短暫。在并發(fā)標(biāo)記(concurrent mark)階段,由剛被確認(rèn)和標(biāo)記過的存活對(duì)象所關(guān)聯(lián)的對(duì)象將被會(huì)跟蹤和檢測(cè)存活狀態(tài)。此步驟的不同之處在于有多個(gè)線程并行處理此過程。在重標(biāo)記(remark)階段,由并發(fā)標(biāo)記所關(guān)聯(lián)的新增或中止的對(duì)象瘵被會(huì)檢測(cè)。在最后的并發(fā)清理(concurrent sweep)階段,垃圾回收過程被真正執(zhí)行。在垃圾回收?qǐng)?zhí)行過程中,其他線程依然在執(zhí)行。得益于CMS GC的執(zhí)行方式,在GC期間系統(tǒng)中斷時(shí)間非常短暫。CMS GC也被稱為低延遲GC,適用于所有應(yīng)用對(duì)響應(yīng)時(shí)間要求比較嚴(yán)格的場(chǎng)景。

CMS GC雖然具有中斷時(shí)間斷的優(yōu)勢(shì),其缺點(diǎn)也比較明顯:

  • 與其他GC相比,CMS GC要求更多的內(nèi)存空間和CPU資源
  • CMS GC默認(rèn)不提供內(nèi)存壓縮

使用CMS GC之前需要對(duì)系統(tǒng)做全面的分析。另外為了避免過多的內(nèi)存碎片而需要執(zhí)行壓縮任務(wù)時(shí),CMS GC會(huì)比任何其他GC帶來更多的stop-the-world時(shí)間,所以你需要分析和判斷壓縮任務(wù)執(zhí)行的頻率及其耗時(shí)情況。

G1 GC

最后我們學(xué)習(xí)有關(guān)G1垃圾回收的介紹。


G1 GC的布局

如果你想清晰的理解GC,請(qǐng)先忘記上面介紹的有關(guān)新生代和老年代的知識(shí)。如上圖所示,每個(gè)對(duì)象在創(chuàng)建時(shí)會(huì)分析到一個(gè)格子中,后續(xù)的GC也是在格子中完成的。每當(dāng)一個(gè)區(qū)域分配滿對(duì)象后,新創(chuàng)建的對(duì)象就會(huì)分配到另外一個(gè)區(qū)域,并開始執(zhí)行GC。在這種GC中不會(huì)出現(xiàn)其他GC中的對(duì)象在新生代和老生代三區(qū)域中移動(dòng)的現(xiàn)象。G1是為了取代在長期使用中暴露出大量問題且飽受抱怨的CMS GC。

G1最大的改進(jìn)在于其性能表現(xiàn),它比以上任何一種GC都更快速。它在JDK6中以早期版本的形式釋放出來以用于測(cè)試,它真正的發(fā)布是在JDK7中。我個(gè)人認(rèn)為在NHN真正在生產(chǎn)環(huán)境使用JDK7至少還需要1年的測(cè)試時(shí)間,所以還需要等待一段時(shí)間。并且我聽說在JDK6中使用G1偶爾會(huì)出現(xiàn)JVM崩潰現(xiàn)象。所以穩(wěn)定版尚需時(shí)日。

注意并發(fā)(Concurrent)和并行(Parallel)的區(qū)別:

    并發(fā)是指用戶線程與GC線程同時(shí)執(zhí)行(不一定是并行,可能交替,但總體上是在同時(shí)執(zhí)行的),不需要停頓用戶線程(其實(shí)在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個(gè)CPU上執(zhí)行);  

    并行收集是指多個(gè)GC線程并行工作,但此時(shí)用戶線程是暫停的;  

所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并發(fā)的.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容