Java 垃圾回收詳解

Java 垃圾回收詳解

知道 Java 的垃圾回收(GC)怎么工作有什么好處?作為一個(gè)軟件工程師,滿足智力上的好奇心可能是一個(gè)理由,但是同時(shí)理解 GC 怎么工作可以幫助你寫出更好的 Java 應(yīng)用。

這是我自己非常個(gè)人、主觀的看法,但是我相信一個(gè)精通 GC 的人很可能是一個(gè)更好的 Java 開發(fā)者。如果你對 GC 的過程感興趣,那說明你已經(jīng)有了開發(fā)一定規(guī)模應(yīng)用的經(jīng)驗(yàn)。如果你曾仔細(xì)思考選擇正確的 GC 算法,說明你已經(jīng)完全理解了你所開發(fā)的應(yīng)用的功能。當(dāng)然,這可能不是評(píng)判一個(gè)優(yōu)秀開發(fā)者的通用標(biāo)準(zhǔn)。然而,當(dāng)我說要想成為一名出色的 Java 開發(fā)者必須理解 GC 時(shí),我想很少會(huì)有人反對。

這是我“成為 Java GC 專家” 系列文章的第一篇。本文將介紹 GC,在下一篇文章中,我將討論分析 GC 的狀態(tài)以及 NHN 的 GC 調(diào)節(jié)示例。

在了解 GC 之前你需要知道一個(gè)術(shù)語。這個(gè)術(shù)語就是“全局暫停事件”(stop-the-world)。不管你選擇什么 GC 算法,全局暫停事件都會(huì)發(fā)生。全局暫停事件意味著JVM將停止當(dāng)前應(yīng)用的運(yùn)行來執(zhí)行 GC。當(dāng)全局暫停事件發(fā)生時(shí),除了 GC 所需要的線程外,所有的線程都會(huì)停止執(zhí)行任務(wù)。被中斷的任務(wù)只有當(dāng) GC 任務(wù)完成以后才會(huì)恢復(fù)。GC 調(diào)節(jié)通常意味著減少全局暫停事件的次數(shù)。

垃圾回收的來源

Java 不會(huì)在代碼中手動(dòng)指定一塊內(nèi)存再釋放它。有的開發(fā)者會(huì)將相關(guān)對象置為 null 或者使用 System.gc() 方法手動(dòng)釋放內(nèi)存。設(shè)置為 null 不是什么大問題,但是調(diào)用 System.gc() 方法會(huì)劇烈的影響系統(tǒng)的性能,所以不應(yīng)該使用。(幸好,我還沒有看到 NHN 的開發(fā)者有使用這個(gè)方法。)

在 Java 中,由于開發(fā)者不需要在代碼中手動(dòng)釋放內(nèi)存,垃圾搜集器會(huì)查找不需要的對象(垃圾)并釋放它們。垃圾搜集器基于以下兩條假設(shè)創(chuàng)建(稱它們?yōu)橥茰y或者先決條件也許更準(zhǔn)確)

  • 大多數(shù)對象很快變成不可達(dá)。
  • 只存在少量從老的對象到新對象的引用

這些假設(shè)稱為“弱分代假設(shè)”(weak generational hypothesis),為了強(qiáng)化這一假設(shè),HotSpot 虛擬機(jī)在物理上分為兩個(gè)部分-新生代(young generation)老年代(old generation)。

新生代:大多數(shù)新創(chuàng)建的對象都存放在這里。因?yàn)榇蠖鄶?shù)對象很快就會(huì)變得不可達(dá),很多對象都在新生代創(chuàng)建,然后就消失。當(dāng)一個(gè)對象從這個(gè)區(qū)域消失的時(shí)候,我們就說發(fā)生了一次“小的 GC”(minor GC)

老年代:那些在新生代存活下來,并沒有變成不可達(dá)的對象被復(fù)制到這里。它通常要比新生代大。由于容量更大,GC 發(fā)生的次數(shù)就沒有新生代頻繁。當(dāng)對象從老年代消失時(shí),我們就說發(fā)生了一次“大 GC”(major GC)(或者是 "全 GC"(full GC))。

我們一起來看一下這幅圖:

圖1:GC 區(qū)域和 數(shù)據(jù)流程

上圖中的持久代(permanent generation)通常也稱為“方法區(qū)(method area)”,它用于存儲(chǔ)類或者字符常量。所以這個(gè)區(qū)域不是用于永久存儲(chǔ)從老年代存活下來的對象。這個(gè)區(qū)域也可能會(huì)發(fā)生 GC。這個(gè)區(qū)域發(fā)生的 GC 也算作大 GC。

有人可能會(huì)想:

如果一個(gè)處于老年代的對象需要引用一個(gè)處于新生代的對象會(huì)怎么樣?

為了解決這個(gè)問題,在老年代有一個(gè)稱為"card table"的東西,是一個(gè)512字節(jié)大小的塊。當(dāng)老年代中的對象要引用一個(gè)新生代的對象時(shí),它就會(huì)被記錄在這個(gè) table 中。當(dāng)新生代執(zhí)行 GC 的時(shí)候,只需要搜索這個(gè) table 來確定它是否屬于需要 GC 的對象,而不用檢查老年代所有引用的對象。card table 通過 write barrier 管理。write barrier 給小 GC 性能上帶來極大的提升。盡管會(huì)有一點(diǎn)額外的開銷,但是 GC 的總體時(shí)間減少了。

圖2:Card Table 的結(jié)構(gòu)

新生代的組成

為了理解 GC, 我們先了解一下新生代,也就是對象第一次被創(chuàng)建的地方。新生代被分成3個(gè)區(qū)域。

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

總共3個(gè)區(qū)域,其中兩個(gè)是存活區(qū)。每一個(gè)區(qū)域的執(zhí)行順序是這樣的:

  • 1、大部分新創(chuàng)建的對象都處于 Eden 區(qū)
  • 2、在 Eden 區(qū)域執(zhí)行第一次 GC 以后,存活下來的對象被移動(dòng)到其中一個(gè)存活區(qū)。
  • 3、在 Eden 區(qū)域再次執(zhí)行 GC 以后,存活下來的對象繼續(xù)堆積已經(jīng)有對象的那個(gè)存活區(qū)。
  • 4、一旦一個(gè)存活區(qū)被存滿,存活對象就會(huì)被移動(dòng)到另一個(gè)存活區(qū)。然后被存滿的那一個(gè)存活區(qū)數(shù)據(jù)就會(huì)被清掉(修改為無數(shù)據(jù)狀態(tài))。
  • 5、如此反復(fù)一定次數(shù)之后,還處于存活狀態(tài)的對象被移動(dòng)到老年區(qū)。

如果你仔細(xì)檢查這些步驟,存活區(qū)域總是有一個(gè)是空的。如果兩個(gè)存活區(qū)域同時(shí)都有數(shù)據(jù),或者同時(shí)都為空,這意味著你的系統(tǒng)存在問題。

通過小 GC 將數(shù)據(jù)堆積到老年代的過程可以參考下圖:

圖3:GC 前后

注意在 HotSpot 虛擬機(jī)中,有兩種技術(shù)用于快速內(nèi)存分配。一個(gè)成為“bump-the-pointer”,另一個(gè)稱為“TLABs(Thread-Local Allocation Buffers)”。

Bump-the-pointer 技術(shù)跟蹤 Eden 區(qū)域最后分配的對象。那個(gè)對象將處于 Eden 區(qū)域的頂部。如果有新的對象需要?jiǎng)?chuàng)建,只需要檢查對象的大小是否適合 Eden 區(qū)域。如果合適,新的對象將被放在 Eden 區(qū)域,并且新的對象處于頂部。所以,當(dāng)創(chuàng)建新的對象時(shí),只需要檢查上一次創(chuàng)建的對象,這樣可以做到較快的內(nèi)存分配。但是,如果是在多線程環(huán)境那將是另外一個(gè)場景。為了保證 Eden 區(qū)域多線程使用的對象是線程安全的,將不可避免的使用鎖,這會(huì)導(dǎo)致性能的下降。HotSpot 虛擬機(jī)使用 TLABs 來解決這個(gè)問題。使用 TLABs 允許每一個(gè)線程在 Eden 區(qū)域有自己的一小塊分區(qū)。由于每一個(gè)線程只能訪問它們自己的 TLAB,即使是 bump-the-pointer 技術(shù)也可以不使用鎖就分配內(nèi)存。

到現(xiàn)在我們快速的概述了新生代的 GC。你不必完全記住我剛才所提到的兩種技術(shù)。你不知道它們也沒什么大不了。但是請記?。簩ο笫窃?Eden 區(qū)域創(chuàng)建,然后長期存活的對象通過存活區(qū)移動(dòng)到老年代。

老年代的 GC

老年代在數(shù)據(jù)存滿時(shí)會(huì)執(zhí)行 GC。各種 GC 的執(zhí)行過程因類型而異,所以如果你知道不同類型的 GC, 理解起來會(huì)容易一些。

在 JDK 7中,一共有5中類型的 GC。

  • 1、Serial GC
  • 2、Parallel GC
  • 3、Parallel Old GC(Parallel Compacting GC)
  • 4、ConCurrent Mark & Sweep GC (CMS)
  • 5、Garbage First(G1)GC

所有這些 GC 當(dāng)中,serial GC 不可以在服務(wù)端使用。這種 GC 在只有一個(gè) CPU 的桌面系統(tǒng)中才會(huì)創(chuàng)建。使用 serial GC 會(huì)明顯的降低應(yīng)用的性能。

現(xiàn)在我們一起來學(xué)習(xí)每一種 GC。

Serial GC(-XX:+UseSerialGC)

上一段中我們介紹的新生代的 GC 使用的是這種類型。老年代的 GC 使用叫做 "標(biāo)記-清除-壓縮(mark-sweep-compact)"的算法。

  • 1、這個(gè)算法的第一步是標(biāo)記老年代中的存活對象
  • 2、然后、從頭開始檢查堆,將存活的對象放到后面(交換)
  • 3、最后一步,用存活對象從頭開始填充堆,這樣這些存活對象連續(xù)堆放,并且將對分為兩部分:一部分有對象另一部分沒有對象(壓縮)

Serial GC 適合小型內(nèi)存和有少量CPU 內(nèi)核的環(huán)境。

Parallel GC(-XX:+UseParallelGC)

圖4:Serial GC 和 Parallel GC 之間的差別

從這張圖片上很容易發(fā)現(xiàn)Serial GC 和 Parallel GC 之間的差異。Serial GC 只是用一個(gè)線程執(zhí)行 GC,parallel GC 使用多個(gè)線程執(zhí)行 GC,所以更快。當(dāng)內(nèi)存足夠并且 CPU 內(nèi)核夠多時(shí)這種 GC 非常有用。它也被稱作”吞吐量 GC(throughput GC)?!?/strong>

Parallel Old GC(-XX:+UseParallelOldGC)

JDK 5 以后開始支持 Parallel Old GC。與并行 GC 相比,唯一的區(qū)別是這個(gè) GC 算法是為老年代設(shè)計(jì)的。它的執(zhí)行一共有三個(gè)步驟:標(biāo)記-匯總-壓縮。匯總這一步為 GC 已經(jīng)執(zhí)行過的區(qū)域單獨(dú)標(biāo)記存活的對象,這一步和 標(biāo)記-交換-壓縮 算法中的交換步驟是不一樣的。這需要通過更復(fù)雜的步驟來完成。

CMS GC(-XX:UseConcMarkSweepGC)

圖5:串行 GC 和 CMS GC

如你所見,CMS GC 比我們前面所介紹的任何 GC 都要復(fù)雜的多。剛開始的 初始標(biāo)記 步驟很簡單。離類加載器最近的對象中的存活對象被搜索出來。所以,暫停時(shí)間很短。在并發(fā)標(biāo)記步驟中,剛才已經(jīng)確認(rèn)的存活對象所引用的對象被跟蹤并檢查。這一步的差別在于它在處理的同時(shí)其他線程同時(shí)也在處理。在重新標(biāo)記階段,新添加的對象或者在并發(fā)標(biāo)記階段被停止引用的對象會(huì)被檢查。最后,并發(fā)清除階段,垃圾回收過程被執(zhí)行。垃圾回收在其他線程還在進(jìn)行的時(shí)候就執(zhí)行。因?yàn)檫@一類型的 GC 是以這樣的方式執(zhí)行,GC 的暫停時(shí)間很短。CMS GC也被稱作低延時(shí) GC,所以當(dāng)響應(yīng)時(shí)間對所有的應(yīng)用都很關(guān)鍵的時(shí)候使用這種 GC。

CMS GC 擁有較短的全局暫停時(shí)間這一優(yōu)點(diǎn),同時(shí)也有以下缺點(diǎn)。

  • 它比其他類型的 GC 使用更多的內(nèi)存和 CPU
  • 默認(rèn)沒有提供壓縮算法。

在使用這種 GC 之前需要認(rèn)真檢查。同時(shí),如果多個(gè)內(nèi)存碎片需要壓縮,全局暫停時(shí)間的時(shí)間會(huì)比任何其他類型的 GC 都要長。所以你需要確認(rèn)壓縮任務(wù)執(zhí)行的頻率和時(shí)間。

G1 GC

最后,我們一起來看一下垃圾優(yōu)先(G1)GC。


圖6:G1 GC 的布局

如果你想理解 G1 GC,忘掉你所知道的新生代和老年代的所有一切。如上圖所示,每一個(gè)對象被分配到每個(gè)網(wǎng)格中,然后會(huì)執(zhí)行 GC。一旦一個(gè)區(qū)域被填滿,對象就會(huì)被分配到另一個(gè)區(qū)域,然后執(zhí)行一次 GC。在G1 GC 中,將數(shù)據(jù)從新生代的3個(gè)區(qū)域移動(dòng)到老年區(qū)的所有步驟都不存在。G1 GC 的創(chuàng)建時(shí)用于替換 CMS GC,因?yàn)閺拈L遠(yuǎn)看后者會(huì)引發(fā)很多問題。

G1 GC 最大的優(yōu)點(diǎn)是性能。它比我們前面討論過的任何 GC 類型都要快。但是在 JDK 6中,這是一個(gè)所謂的早期版本所有只能用于測試。JDK 7的官方版本中已經(jīng)包含這一類型 GC。以我個(gè)人的意見,我們在將 JDK 7應(yīng)用到 NHN 的實(shí)際服務(wù)之前需要很長的時(shí)間的測試(至少一年),所以你可能需要等待一段時(shí)間。同時(shí)我聽說了幾次在 JDK 中使用 G1 GC 后JVM出現(xiàn)崩潰。所以請繼續(xù)等待直到它更穩(wěn)定。

本文譯自:Understanding Java Garbage Collection

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

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

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