JVM垃圾回收詳解

Hodu發表於2017-12-16

JVM垃圾回收詳解

前言

最近線上出現了JVM 頻繁FGC的問題,查詢了很多GC相關的資料,做了一些整理翻譯。文章比較長可以收藏後慢慢閱讀。

一、什麼是垃圾回收?(Garbage Collection)

一個垃圾回收器有一下三個職責

  • 分配記憶體
  • 確保有引用的物件能夠在記憶體中保留。
  • 能夠在正在執行的程式碼環境中回收已經死亡物件的記憶體。

這裡提到的有引用是指存活的物件,後面會提到一些演算法用來判斷物件是否存活。不在有引用的物件將被認為是死亡的,也就是常說的垃圾garbage。找到並釋放這些垃圾物件佔用的空間的過程就被稱作是垃圾回收garbage collection

垃圾回收可以解決很多記憶體分配的問題,但並不意味這全部。 比如:你可以不斷地建立物件並保持對它們的引用直到沒有可用的記憶體分配。垃圾回收本身就是一項非常複雜和消耗資源的過程。

二、理想的垃圾收集器需要哪些特性?

  1. 垃圾收集器必須是安全和全面的。這就意味著,存活的物件絕對不能被釋放,相反垃圾物件在很少的垃圾回收迴圈裡必須被回收。
  2. 垃圾回收必須是高效的,不允許出現正在執行的程式長時間暫停。
  3. 記憶體碎片整理,垃圾被收集以後記憶體會存在很多不連續的記憶體碎片,可能導致大物件無法分配到足夠連續的記憶體。
  4. 擴充套件性,在多處理器系統、多執行緒應用中,記憶體分配和垃圾收集不能成為效能瓶頸。

三、設計選擇

在設計一款垃圾收集器時,有一些選擇可供選擇:

  • 序列 vs 並行

序列收集,即使在多cpu環境中也是單執行緒處理垃圾收集工作。當使用並行收集時,垃圾收集任務就會被分為几子任務由不同的執行緒的執行,不僅僅是在多CPU環境中使用,在單核的系統中也可以使用,只是收集效果可能比使用序列效率還低。所以再單核的環境下儘量使用序列收集。

  • 併發 vs 暫停(stop-the-word)

併發是指垃圾收集執行緒和應用執行緒同時執行,併發和stop-the-word並不是互斥的,在一個執行一次垃圾收集的過程中兩種情況都可能存在。例如CMSG1垃圾蒐集器。併發式GC會併發執行其垃圾收集任務,但是,可能也會有一些步驟需要以stop-the-world方法執行,導致應用程式暫停。與併發式GC相比,Stop-the-world式的GC更簡單.

  • 整理 vs 不整理 vs 複製

這個描述的主要是垃圾被收集以後,對記憶體碎片的處理方式。

整理、不整理,垃圾回收以後是否將存活的物件統一移動到一個地方。整理後的記憶體空間方便後續的物件分配記憶體,但是更消耗資源和時間,而不整理效率更高存在記憶體碎片的風險。

複製,首先將記憶體分割成兩塊一樣大小的區域,垃圾收集後會將存活的物件拷貝到另一塊不同的記憶體區域。這樣做的好處是,拷貝後,源記憶體區域可以作為一塊空的、立即可用的區域對待,方便後續的記憶體分配,但是這種方法的缺點是需要用額外的時間、空間來拷貝物件。

四、物件是否存活?

Jvm要對回收一個物件必須知道這個物件是否存活,即是否有有效的引用?介紹幾種判斷物件是否死亡的演算法。

  1. 引用計數法 給物件新增一個引用計數器,每次引用到它時引用計數器加一,當引用失效時引用計時器減一。當引用計數器為0時即表示當前物件可以被回收。 這個演算法實現簡單、判定效率也很高,但是無法處理迴圈引用的問題,即 A 物件引用了 B, B 物件也引用了 A,那麼A、B都有引用,他們的應用計數都為一,但實際他們是可以被回收的。

  2. 可達性分析演算法 演算法規定了一些稱為GC Root的根物件,當物件沒有引用鏈到達這些GC Root時就被判定為可回收的物件。

    image

五、分代收集演算法

當使用稱為分代收集的技術時,記憶體將被分為不同的幾代,即,會將物件按其年齡分別儲存在不同的物件池中。例如,目前最廣泛使用的是分代是將物件分為年輕代物件和老年代物件。

在分代記憶體管理中,使用不同演算法對不同代的物件執行垃圾收集的工作,每種演算法都是基於對某代物件的特性進行優化的。考慮到應用程式可以是用包括Java在內的不同的程式語言編寫,分代垃圾收集使用了稱為 弱代理論(weak generational hypothesis)的方法,具體描述如下:

大多數分配了記憶體的物件並不會存活太長時間,在處於年輕代時就會死掉; 很少有物件會從老年代變成年輕代。 年輕代物件的垃圾收集相對頻繁一些,同時會也更有效率,更快一些,因為年輕代物件所佔用的記憶體通常較小,也比較容易確定哪些物件是已經無法再被引用的。

當某些物件經過幾次年輕代垃圾收集後依然存活,則這些物件會被 提升(promoted)到老年代。典型情況下,老年代所佔用的記憶體會比年輕代大,而且還會隨時漸漸慢慢增大。這樣的結果是,對老年代的垃圾收集就不能頻繁進行,而且執行時間也會長很多。

image

選擇年輕代的垃圾收集演算法時會更看重執行速度,因為年輕代的垃圾收集工作會頻繁執行。另一方面,管理老年代的演算法則更注重空間效率,因為老年代會佔用堆中的大部分空間,這要求演算法必須要處理好垃圾收集的工作,儘量降低堆中的垃圾記憶體的密度。

六、HotSpot 分代收集

主要介紹幾種常見的垃圾收集器序列收集器(Serial Collector)並行垃圾收集器(Parallel Collector)並行整理收集器(Parallel Compacting Collector)併發標記清理垃圾收集器(Concurrent Mark-Sweep,CMS)Garbage-First (G1) 圖中有連線的表示可以組合使用。

JVM垃圾回收詳解

6.1 HotSpot中的代的劃分

在Java HotSpot虛擬機器中,記憶體被分為3代:年輕代、老年代和永生代(java8已經取消永久代)。大多數物件最初都是分配在年輕代記憶體中的,年輕代中物件經過幾次垃圾收集後還存活的,會被轉到老年代。一些體積比較大的物件在建立的時候可能就會在老年代中。 在年輕代中包含三個分割槽,一個 Eden區和兩個 Survivor區(FROM、TO),如圖所示。大部分物件最初是分配在Eden區中的(但是,如前面所述,一些較大的物件可能會直接分配在老年代中)。Survivor始終保持一個區域為空,當經過一定次數(-XX:MaxTenuringThreshold=n來指定預設值為15)的年輕代GC後依然存活的物件可以被晉升到老年代。

JVM垃圾回收詳解

6.2 垃圾收集分類

當年輕代被填滿時,開始執行年輕代的垃圾收集(minor collection)。當老年代被填滿時,也會執行老年代垃圾收集(full GCmajor collection),一般來說,年輕代GC會先執行,執行多次young GC 會觸發FGC,當然這不是絕對的,因為大物件會直接分配到老年代,當老年代的分配的記憶體不足時就可能觸發頻繁的FGC。目前除了CMS收集器外,在執行FGC的時候都會對整個堆進行垃圾收集。

6.3 序列收集器(Serial Collector)

使用序列收集器,年輕代和老年代的垃圾收集工作會序列完成(在單一CPU系統上),這時是stop-the-world模式的。即,當執行垃圾收集工作時,應用程式必須停止執行。

6.3.1 使用序列收集器的年輕代垃圾收集

圖3展示了使用序列收集器的年輕代垃圾收集的執行過程。EdenSurvivor FROM區存活的物件會被拷貝到初始為空的另一個Survivor區(圖中標識為To的區)中,這其中,那些體積過大以至於Survivor區裝不下的物件會被直接拷貝到老年代中。相對於已經被拷貝到To區的物件,源Survivor區(圖中標識為From的區)中的存活物件仍然比較年輕,而被拷貝到老年代中物件則相對年紀大一些。

JVM垃圾回收詳解

在年輕代垃圾收集完成後,Eden區和From區會被清空,只有To區會繼續持有存活的物件。此時,From區和To區在邏輯上交換,To區變成From區,原From區變成To區,如圖4所示。

JVM垃圾回收詳解

6.3.2 使用序列收集器的老年代垃圾收集

對於序列收集器,老年代和永生代會在進行垃圾收集時使用標記-清理-整理(Mark-Sweep-Compact)演算法。在標記階段,收集器會標識哪些物件是live狀態的。清理階段會跨代清理,標識垃圾物件。然後,收集器執行整理(sliding compaction),將存活物件移動到老年代記憶體空間的起始部分(永生代中情況於此類似),這樣在老年代記憶體空間的尾部會產生一個大的連續空間。如圖5所示。這種整理可以使用碰撞指標完成。

JVM垃圾回收詳解

6.3.3 什麼時候使用序列垃圾收集器

大多數執行在客戶機上的應用程式會選擇使用並行垃圾收集器,因為這些應用程式對低暫停時間並沒有較高的要求。對於當今的硬體來說,序列垃圾收集器已經可以有效的管理許多具有64M堆的重要應用程式,並且執行一次完整垃圾收集也不會超過半秒鐘。

6.3.4 選擇序列垃圾收集器

在J2SE 5.0的發行版中,在非伺服器類使用的機器上,預設選擇的是序列垃圾收集器。在其他型別使用的機器上,可以通過新增引數 -XX:+UseSerialGC來顯式的使用序列垃圾收集器。

6.4 並行垃圾收集器(Parallel Collector)

當前,很多的Java應用程式都跑在具有較大實體記憶體和多CPU的機器上。並行垃圾收集器,也稱為吞吐量垃圾收集器,被用於垃圾收集工作。該收集器可以充分的利用多CPU的特點,避免一個CPU執行垃圾收集,其他CPU空閒的狀態發生。

6.4.1 使用並行垃圾收集器的年輕代垃圾收集

這裡,對年輕代的並行垃圾收集使用的序列垃圾收集演算法的並行版本。它仍然會stop-the-world,拷貝物件,但執行垃圾收集時是使用多CPU並行進行的,減少了垃圾收集的時間損耗,提高了應用程式的吞吐量。圖6展示了序列垃圾收集器和並行垃圾收集器對年輕代進行垃圾收集時的區別。

JVM垃圾回收詳解

6.4.2 使用並行垃圾收集器的老年代垃圾收集

老年代中的並行垃圾收集使用了與序列垃圾收集器相同的序列 標記-清理-整理(mark-sweep-compact)演算法。

6.4.3 什麼時候使用並行垃圾收集器

當應用程式執行在具有多個CPU上,對暫停時間沒有特別高的要求時,使用並行垃圾收集器會有較好的效果,因為雖不頻繁,但可能時間會很長的老年代垃圾收集仍然會發生。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程式更適合使用並行垃圾收集。

可能你會想用並行整理垃圾收集器(會在下一節介紹)來替代並行收集器,因為前者對所有代執行垃圾收集,而後者指對年輕代執行垃圾收集。

6.4.4 選擇並行垃圾收集器

在J2SE 5.0的發行版中,若應用程式是執行在伺服器類的機器上,則會預設使用並行垃圾收集器。在其他機器上,可以通過 -XX:+UseParallelGC引數來顯式啟用並行垃圾收集器。

6.6 並行整理整理收集器(Parallel Compacting Collector)

並行整理垃圾收集器是在J2SE 5.0 update 6中被引入的,其與並行垃圾收集器的區別在於,並行整理垃圾收集器使用了新的演算法對老年代進行垃圾收集。注意,最終,並行整理垃圾收集器會取代並行垃圾收集器。

4.6.1 使用並行整理垃圾收集器的年輕代垃圾收集

年輕代中,並行整理垃圾收集器使用了與並行垃圾收集器相同的垃圾收集演算法。

4.6.2 使用並行整理垃圾收集器的老年代垃圾收集

當使用並行整理垃圾收集時,老年代和永生代會使用stop-the-world的方式執行垃圾收集,大多數的並行模式都會使用移動整理(sliding compaction)。垃圾收集分為三個階段。首先,將每一個代從邏輯上分為固定大小的區域。

在 標記階段(mark phase),應用程式程式碼可以直接到達的live物件的初始集合會被劃分到各個垃圾收集執行緒中,然後,所有的live物件會被並行標記。若一個物件被標記為live,則會更新該物件所在的區域中與該物件的大小和位置相關的資料。

在 總結階段(summary phase)會對區域,而非單獨的物件進行操作。由於之前的垃圾收集執行了整理,每一代的左側部分的物件密度會較高,包含了大部分live物件。這些物件密度較高的區域被恢復為可用後,就不值得再花時間去整理了。所以,在總結階段要做的第一件事是從最左端物件開始檢查每個區域的live物件密度,直到找到了一個恢復其本區域和恢復其右側的空間的開銷都比較小時停止。找到的區域的左側所有區域被稱為dense prefix,不會再有物件被移動到這些區域裡了。這個區域後側的區域會被整理,清除所有已死的空間(清理垃圾物件佔用的空間)。總結階段會計算並儲存每個整理後的區域中物件的新地址。注意,在當前實現中,總結階段是序列的;當然總結階段也可以實現為並行的,但相對於效能總結階段的並行不及標記整理階段來得重要。

在 整理階段(compaction phase),垃圾收集執行緒使用總結階段收集到的資料決定哪些區域課餘填充資料,然後各個執行緒獨立的將資料拷貝到這些區域中。這樣就產生了一個底端物件密度大,連一端是一個很大的空區域塊的堆。

4.6.3 什麼時候使用並行整理垃圾收集器

相對於並行垃圾收集器,使用並行整理垃圾收集器對那些執行在多CPU的應用程式更有好處。此外,老年代垃圾收集的並行操作可以減少應用程式的暫停時間,對於那些對暫停時間有較高要求的應用程式來說,並行整理垃圾程式比並行垃圾收集更加適用。並行整理垃圾收集程式可能並不適用於那些與其他很多應用程式並存於一臺機器的應用程式上,這種情況下,沒有一個應用程式可以獨佔所有的CPU。在這樣的機器上,需要考慮減少執行垃圾收集的執行緒數(使用-XX:ParallelGCThreads=n命令列選項),或者使用另一種垃圾收集器。

4.6.4 選擇並行整理垃圾收集選項

若你想使用並行整理垃圾收集器,你必須顯式指定-XX:+UseParallelOldGC命令列選項。

4.7 併發標記清理(Concurrent Mark-Sweep,CMS)垃圾收集器

對於很多應用程式來說,點到點的吞吐量並不如快速響應來的重要。典型情況下,年輕代的垃圾收集並不會引起較長時間的暫停。但是,老年代的垃圾收集,雖不頻繁,卻可能引起長時間的暫停,特別是使用了較大的堆的時候。為了應付這種情況,HotSpot JVM使用了CMS垃圾收集器,也稱為低延遲(low-latency)垃圾收集器。

4.7.1 使用CMS垃圾收集器的年輕代垃圾收集

CMS垃圾收集器只對老年代進行收集,年輕代實際預設使用ParNewGC(一種年輕代的並行垃圾收集器)收集。

4.7.2 使用CMS垃圾收集器的老年代垃圾收集

大部分老年代的垃圾收集使用了CMS垃圾收集器,垃圾收集工作是與應用程式的執行併發進行的。

過程 描述
初始標記 標記老年代的存活物件,也可能包括年輕代的存活物件。暫停應用執行緒stop-the world
併發標記 和應用程式一起執行,標記應用程式執行過程中產生的存活的物件。
重標記 標記由於應用程式更新導致遺漏的物件,暫停應用執行緒stop-the world
併發清理 清理沒有被標記的物件,不會進行記憶體整理,可能導致記憶體碎片問題。
復位 清理資料等待下一次收集執行。

圖7展示了使用序列化的標記清理垃圾收集器和使用CMS垃圾收集器對老年代進行垃圾收集的區別。

JVM垃圾回收詳解

不進行記憶體空間整理節省了時間,但是可用空間不再是連續的了,垃圾收集也不能簡單的使用指標指向下一次可用來為物件分配記憶體的地址了。相反,這種情況下,需要使用可用空間列表。即,會建立一個指向未分配區域的列表,每次為物件分配記憶體時,會從列表中找到一個合適大小的記憶體區域來為新物件分配記憶體。這樣做的結果是,老年代上的記憶體的分配比簡單實用碰撞指標分配記憶體消耗大。這也會增加年輕代垃圾收集的額外負擔,因為老年代中的大部分物件是在新生代垃圾收集的時候從新生代提升為老年代的。

使用CMS垃圾收集器的另一個缺點是它所需要的對空間比其他垃圾收集器大。在標記階段,應用程式可以繼續執行,可以繼續分配記憶體,潛在的可能會持續的增大老年代的記憶體使用。此外,儘管垃圾收集器保證會在標記階段標記出所有的live物件,但是在此階段中,某些物件可能會變成垃圾物件,這些物件不會被回收,直到下一次垃圾收集執行。這些物件成為 浮動垃圾物件(floating garbage)。

最後,由於沒有使用整理,會造成記憶體碎片的產生。為了解決這個問題,CMS垃圾收集器會跟蹤常用物件的大小,預估可能的記憶體需要,可能會差分或合併記憶體塊來滿足需要。

與其他的垃圾收集器不同,當老年代被填滿後,CMS垃圾收集器並不會對老年代進行垃圾收集。相反,它會在老年代被填滿之前就執行垃圾收集工作。否則這就與序列或並行垃圾收集器一樣會造成應用程式長時間地暫停。為了避免這種情況,CMS垃圾收集器會基於統計數字來來定執行垃圾收集工作的時間,這個統計數字涵蓋了前幾次垃圾收集的執行時間和老年代中新增記憶體分配的速率。當老年代中記憶體佔用率超過了稱為初始佔用率的閥值後,會啟動CMS垃圾收集器進行垃圾收集。初始佔用率可以通過命令列選項-XX:CMSInitiatingOccupancyFraction=n進行設定,其中n是老年代佔用率的百分比的值,預設為68。

總體來看,與平行垃圾收集器相比,CMS減少了執行老年代垃圾收集時應用暫停的時間,但卻增加了新生代垃圾收集時應用暫停的時間、降低了吞吐量而且需要佔用更大的堆空間。

七、G1 收集器

G1最為新一代的垃圾回收器,設計之初就是為了取代CMS的。具備以下優點:

  • 併發執行垃圾收集
  • 很短的時間進行記憶體整理
  • GC的暫停時間可控
  • 不需要犧牲吞吐量
  • 不需要佔用額外的java堆空間 什麼需要使用G1收集器呢?
  • 頻繁的FGC
  • 物件分配率或提升的速率差異很大。
  • 無法接受過長的GC暫停和記憶體整理時間

G1收集器和之前垃圾收集器擁有完全不同的記憶體結構,雖然從邏輯上也存在年輕代、老年代,但是物理空間上不在連續而是雜湊在記憶體中的一個個regions。記憶體空間分割成很多個相互獨立的空間,被乘稱作regions。當jvm啟動時regins的大小就被確定了。jvm會建立大概2000個regions,每個region的大小在1M~32M之間。記憶體結構如下圖:

JVM垃圾回收詳解

7.1 使用G1進行年輕代收集

當年輕代GC被觸發時,Eden中存活的物件將會被複制或者移動evacuated到倖存區的regions,在倖存複製的次數到達閾值的存活物件將會晉升到老年區。這個過程也是一個Stop-the-world 暫停。eden和survivor的大小將會在下一次年輕代GC前重新計算。

JVM垃圾回收詳解
總而言之,G1在年輕代的手機行為包括以下幾點:

1、記憶體被分割成相互獨立的大小相等的regions。 2、年輕代雜湊在整個記憶體空間中,這樣做的好處是當需要重新分配年輕代大小時會非常方便。 3、stop-the-word 暫停所有執行緒。 4、實際上也是並行回收演算法,多執行緒並行收集。 5、存活的物件將被複制到新的 survivor或老年代 regions。

7.2 使用G1進行老年代收集

過程 描述
初始標記 stop-the world 通常伴隨在年輕代GC後面,標記有被老年代物件關聯的倖存區 regions
掃描根 Regions 和應用執行緒併發執行,掃描倖存區regions
併發標記 併發標記整個堆存活的物件
重標記 完成整個堆的存活物件標記,使用snapshot-at-the-beginning (SATB)演算法標記存活物件,該演算法比CMS中使用的更快。stop-the-word
並行清理 並行清理死亡的的物件,返回空的regoins到可用列表。
複製 複製存活的物件到新的regions,This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)].

最佳實踐

1、不要指定年輕代大小 -Xmn,G1每次垃圾收集結束後都會從新計算並設定年輕代的大小,將會影響全域性的暫停時間 2、響應時間配置 -XX:MaxGCPauseMillis=<N> 3、如何解決清理 or 複製失敗問題,通過增加-XX:G1ReservePercent=n配置預留空間的大小,防止Evacuation Failure,預設值是10.也可以使用-XX:ConcGCThreads=n增加併發標記的執行緒數來解決

八、相關命令

選擇垃圾回收

-XX:+UseSerialGC            序列垃圾收集器
-XX:+UseParallelGC          並行垃圾收集器
-XX:+UseParallelOldGC       並行整理垃圾收集器
-XX:+UseConcMarkSweepGC	    併發標記清理(CMS)垃圾收集年輕代預設使用-XX:+ParNewGC
-XX:+UserG1GC
複製程式碼

檢視垃圾收集日誌

-XX:+PrintGC                
-XX:+PrintGCDetails     
-XX:+PrintGCTimeStamps      
複製程式碼

對大小配置:

-Xmsn                           堆最小值
-Xmxn                           堆最大值
-Xmn                            年輕代大小
-XX:NewRatio=n	                年清代比例 Client_JVM=2 Server_JVM=8     
-XX:SurvivorRatio=n	            倖存去比例
-XX:MaxPermSize=n               依賴於不同平臺的實現永生代的最大值(java 8 以後啟用)。
複製程式碼

G1可用的配置

-XX:+UseG1GC	Use the Garbage First (G1) Collector
-XX:MaxGCPauseMillis=n	Sets a target for the maximum GC pause time. This is a soft goal, and the JVM will make its best effort to achieve it.
-XX:InitiatingHeapOccupancyPercent=n	Percentage of the (entire) heap occupancy to start a concurrent GC cycle. It is used by GCs that trigger a concurrent GC cycle based on the occupancy of the entire heap, not just one of the generations (e.g., G1). A value of 0 denotes 'do constant GC cycles'. The default value is 45.
-XX:NewRatio=n	Ratio of new/old generation sizes. The default value is 2.
-XX:SurvivorRatio=n	Ratio of eden/survivor space size. The default value is 8.
-XX:MaxTenuringThreshold=n	Maximum value for tenuring threshold. The default value is 15.
-XX:ParallelGCThreads=n	Sets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running.
-XX:ConcGCThreads=n	Number of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running.
-XX:G1ReservePercent=n	Sets the amount of heap that is reserved as a false ceiling to reduce the possibility of promotion failure. The default value is 10.
-XX:G1HeapRegionSize=n
複製程式碼

參考:

Getting Started with the G1 Garbage Collector

Memory Management in the Java HotSpot™ Virtual Machine

相關文章