1. 程式人生 > >JDK11-G1垃圾收集器

JDK11-G1垃圾收集器

同時歡迎觀看本人錄得兩個視訊教程:

G1垃圾收集器簡介

G1垃圾收集器主要是為那些擁有大記憶體的多核處理器而設計的。它在以很高的概率滿足垃圾收集的停頓時間的要求同時還可以達到很高的吞吐量,同時幾乎不需要做什麼配置。G1的目標是為應用提供停頓時間和吞吐量的最佳平衡,它的主要特性包含:

  • 堆記憶體達到數十個G甚至更大,超過50%的堆記憶體都是存活的物件
  • 物件分配和晉升的速度隨時間變化非常大
  • 堆中存在大量的記憶體碎片
  • 可預測的停頓時間的目標在幾百毫秒以內,不會存在長時間的停頓

在jdk11中,G1已經取代了CMS,是預設的垃圾收集器

在隨後的章節會介紹G1收集器達到如此高效能和滿足停頓時間目標的多種方式。

啟用G1

因為G1是預設的收集器,因此一般不需要做任何額外的操作就開啟。你也可以用 -XX:+UseG1GC來明確開啟(譯者注:因為是JDK11)。

基礎的一些概念

G1是一款分帶、增量、並行、大部分時候併發、STW並且標記整理的收集器,在每一個STW停頓的時候,它都會監控停頓時間的目標。跟其他的收集器類似,G1把堆分成邏輯上的young區和old區。記憶體回收主要集中在young區,在這個區域的記憶體回收也是非常高效的,偶爾也會發生在old區。

為了提高吞吐量,有些操作總是STW的,還有一些操作在應用停止的情況下會花費更多的時間,比如一些對整個堆的操作,像全域性的標記就是並行和併發來執行的。在空間回收的時候,為了讓STW時間更短,G1是增量的分步和並行來回收的。G1是通過記錄上一次應用的行為和GC的停頓資訊來實現可預計的停頓時間的,可以利用這些資訊來計算在停頓時間之內要做的工作的多少。比如:G1會首先回收那些可以高效回收的記憶體區域(也就是大部分都被填滿垃圾的區域,這也是為啥叫G1的原因)。

G1大部分使用標記整理演算法來回收記憶體:收集選中的記憶體區域中的存活物件,然後把他們拷貝到新的記憶體區域中,同時會對這些物件佔用的空間進行壓縮。回收完成以後,之前被存活物件佔用的空間可以被應用用來重新分配物件。

G1並不是一個實時的垃圾收集器。長時間來看,它可以以很高的概率滿足停頓時間的要求,但並不能絕對滿足。

堆的結構

G1把整個堆分成很多相等大小的塊(Region),每一個region都是一些連續的虛擬記憶體就如同圖9-1所示。region是記憶體分配和回收的基本單元,在某個特定的時間點,一個region可能是空的(淺灰色表示),或者是分配到了某個區(generation)中,可能是young區或者old區。當收到需要分配記憶體的請求的時候,記憶體管理器就會提供空閒的region出去。記憶體管理器會把他們賦值給某個generation然後返回給應用程式,應用就可以在這裡面分配物件。
在這裡插入圖片描述

young區包含了eden區(紅色表示)和survivor區(標有S的紅色)。這些region和其他收集器裡面的連續的記憶體空間的作用是一樣的,不一樣的地方在於:G1裡面,這些region在記憶體裡面可以是物理上不連續的。old region(淺藍色表示)組成了old區。old區的region有可能是大物件區(humongous )(標有H的淺藍色表示),這些物件可以橫跨多個region。

應用總是在young區分配物件,更確切地說是在eden區,大物件除外,因為大物件是直接分配在old區。

G1在停頓的時候可以回收整個young區的記憶體,在任何停頓都可以回收一些可選的old區的記憶體。停頓的時候,G1會把物件從一個地方複製到堆裡面的一個或者多個region,物件要拷貝到的目的地region取決於來源region:來自young區的物件要麼被拷貝到survior區要麼到old區,來自old區的物件會從一個old region拷貝到另一個old region,不同的old region是有不同的年齡的。

垃圾收集的生命週期

巨集觀上看,G1的垃圾回收在兩個階段中來回交替執行的。young-only階段會逐步把old區填滿存活物件,space-reclamation階段除了會回收young區的記憶體以外,還會增量回收old區的記憶體。然後就會重新開始young-only階段。

圖9-2用一個可能會發生的回收停頓的例子描述了這個週期:

在這裡插入圖片描述

下面的章節詳細的講述了G1回收週期中的這些階段、停頓、還有階段之間的變遷:

Young-only階段

這個階段從young區的一些普通的young GC開始,這些GC會把young區的物件晉升到old區。當old區的佔有率達到一個閾值的時候就開始young-only階段到space-reclamation階段的轉換,這個時候,就開始Concurrent Start的young GC而不是普通的young GC。

  • Concurrent Start : 這種型別的GC除了做普通的young GC以外,還會開始標記(marking)過程。併發標記會找出當前old區中所有的存活的物件以備隨後的 space-reclamation階段來使用。當併發標記還沒結束的過程中,還是可能會發生普通young GC的。標記經過兩個特殊的STW停頓之後才會結束:重新標記(remark)和清理(cleanup)。
  • 重新標記(Remark): 這個停頓中會結束掉mark階段、做全域性的引用處理和類解除安裝,回收全空的region和清理掉內部的資料結構。在重新標記(remark)和清理(cleanup)之間,G1會併發的在選中的old區的region中計算隨後可以回收的空間,這個計算會在Cleanup的時候結束。
  • 清理(Cleanup)。這個停頓中會決定是否要開始space-reclamation階段。如果要開始space-reclamation, young-only階段就結束了,緊接著就開始一個Mixed young GC。

Space-reclamation階段

這個階段由多個Mixed GC組成,不光是回收young區的region,同時也會回收old區的region。當G1發現,無法回收更多old區的region的時候,space-reclamatio階段就結束了。

space-reclamation階段以後,會以young-only階段開始一個新的回收週期。為了以防萬一,在收集存活物件的時候,如果應用的記憶體耗盡了,G1也會像其他的收集器一樣做整個堆的STW的堆壓縮(也就是FullGC)。

G1的內部實現

本節描述了G1收集器的一些重要的細節。

如何確定初始標記的閾值

Initiating Heap Occupancy Percent (IHOP) 是觸發開始初始標記的閾值,它的值是old區大小的一個百分比。通過觀察標記過程使用的時間和標記過程中old區分配的記憶體大小,G1預設會自動的調整一個最優的IHOP,這個特性就叫做自適應的IHOP。如果激活了這個特性, -XX:InitiatingHeapOccupancyPercent 這個選項決定了IHOP的初始值,因為初始的時候還沒有足夠的資料來對這個值做預測。-XX:-G1UseAdaptiveIHOP這個選項可以關閉自適應,此時,由 -XX:InitiatingHeapOccupancyPercent設定的閾值就始終不會改變。

自適應IHOP會這樣來設定這個閾值: 當開始space-reclamation階段的第一個Mixed GC的時候,old區的佔有率=old區的最大值減去 -XX:G1HeapReservePercent

標記(Marking)

G1使用SATB演算法來做標記。在初始標記開始的時候,G1會儲存堆的一份虛擬映象,初始標記開始時候存活的物件在後續的標記過程中仍然被認為是存活的。這意味著就算是標記過程中這部分物件死亡了,對於space-reclamation階段來說它們仍然是存活的(有少部分例外)。跟其他的收集器相比,這會導致一些額外的記憶體被錯誤的佔用。但是,SATB給Remark提供了更低的延遲。那些mark中死亡的物件在下一次mark中會被回收掉。關於mark的一些問題可以參考G1調優

記憶體非常緊張時候的表現

當應用的存活物件佔用了大量的記憶體,無法容納回收剩餘的物件的時候,就會發生evacuation failure。 Evacuation failure發生的時候,G1為了完成當前的GC,它會保持已經位於新的位置上的存活物件,僅僅是調整物件之間的引用,而不會複製或者移動這些物件。Evacuation failure會導致一些額外的開銷,但是一般會跟別的young GC一樣快。evacuation failure完成以後,G1會跟往常一樣繼續恢復應用的執行。G1會假設 evacuation failure是發生在GC的後期,也就是說,大部分物件已經移動過了,已經有足夠的剩餘記憶體來繼續執行應用程式一直到mark結束 space-reclamation開始。

如果這個假設不成立,G1最終會發起Full GC。這種型別的GC會對整個堆做壓縮,可能會非常非常的慢!

關於allocation failure或者是Full GC的而更多資訊可以參考G1調優

大物件(Humongous Objects)

大物件是說那些物件大小超過了region一半的物件。如果沒有設定 -XX:G1HeapRegionSize這個選項,當前region的大小自適應計算的,在G1自適應預設值這一節有詳細講述。

G1對這些大物件會做特殊處理:

  • 大象是分配在old區的一系列連續的region中,物件的開始總是位於這一系列region的第一個region的開始,在整個物件被回收掉之前,最後一個region中剩餘的空間都不會被分配出去。

  • 一般來說,只有在Cleanup停頓階段mark結束以後或者FullGC的時候,死亡的大物件才會被回收掉。但是,基本型別的陣列的大物件是例外的,比如bool陣列、所有的整形陣列、浮點型陣列等。G1會在任何GC停頓的適當時候回收這些大物件,如果他們不再有引用。這個預設是開啟的,但是可以使用 -XX:G1EagerReclaimHumongousObjects這個選項禁用掉。

  • 分配大物件會導致過早的發生GC停頓。G1在分配每一個大物件的時候都會去檢查IHOP,如果當前的堆佔用率超過了IHOP閾值,就會強制立即發起一個初始標記的young GC。

  • 即使是在FullGC的時候,這些大物件也是永遠不會被移動的。這會導致過早的發生FullGC或者是意外的OOM,儘管此時還有大量的空閒記憶體,但是這些記憶體都是region中的記憶體碎片。

Young-Only階段Generation的大小變化

在young-only階段,要回收的那些region集合(回收集)只由young區的
region組成。在每一個普通的youngGC結束的時候,G1總是會調整young區的大小。基於長期的對實際停頓時間的觀察,G1就可以滿足 -XX:MaxGCPauseTimeMillis-XX:PauseTimeIntervalMillis 設定的停頓時間的目標。它會計算出來回收相同大小的young區的記憶體需要花費多少時間,其中包括在GC的時候要拷貝多少個物件,物件之間是如何相互關聯的。

如果沒有其他的限定條件,G1會把young區的大小調整為 -XX:G1NewSizePercent-XX:G1MaxNewSizePercent 之間的值來滿足停頓時間的要求。關於如何解決長時間停頓問題可以參考G1調優

Space-Reclamation階段Generation的大小變化

在space-reclamation階段,G1會盡量在一個GC停頓之內回收儘可能多的old區的記憶體。young區的大小被調整為 -XX:G1NewSizePercent設定的允許的最小值,只要是存在可回收記憶體的old區的region都會被新增到回收集合中,一直到再增加就會超出停頓時間的目標為止。在特定的某個GC停頓之內,G1會按照這些region回收的效率來新增要回收的region,效率高的排在前面,得到最終要回收的region集合。

每一個GC停頓要回收的old區的region數量受限於候選region集合數量除以 -XX:G1MixedGCCountTarget這個選項設定的長度,候選region集合是old區的所有的佔用率低於 -XX:G1MixedGCLiveThresholdPercent 的那些region。

當候選region集合中可回收的空間低於 -XX:G1HeapWastePercent 的時候,這一階段就結束了。

關於G1會使用old區的多少個region和如何避免長時間的mixed停頓可以參考G1調優

G1自適應的一些預設值

本節講述了G1的一些重要的預設行為和預設值,講述瞭如果不做額外的處理,G1的預期的行為和資源的佔用情況。

選項和預設值 描述
-XX:MaxGCPauseMillis=200 最大停頓時間的目標
-XX:GCPauseTimeInterval=< ergo> 最大停頓時間間隔目標。G1預設不會設定任何目標
-XX:ParallelGCThreads=< ergo> 設定GC停頓時候的並行的GC收集執行緒數。它是根據虛擬機器所在的主機的可用CPU執行緒數來計算的:如果CPU少於8個這個值就是cpu的數量,否則,就等於cpu數量*5/8。每個停頓開始的時候,最大的GC執行緒數還受限於最大的堆記憶體,G1的內個執行緒能使用的最大堆記憶體是由-XX:HeapSizePerGCThread來設定的
-XX:ConcGCThreads=< ergo> 設定與應用併發執行的GC執行緒數,預設是-XX:ParallelGCThreads/4
-XX:+G1UseAdaptiveIHOP-XX:InitiatingHeapOccupancyPercent=45 預設自適應IHOP是開啟的,前幾個GC週期G1會使用 old區佔用45%作為開始mark的閾值
-XX:G1HeapRegionSize=< ergo> region的大小,整個堆大概有2048個region,region的大小可以在1-32M之間,必須是2的次方
-XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 young區的大小就在這兩個值之間變化
-XX:G1HeapWastePercent=5 候選region集合中可以不被回收的空間數量,如果候選region集合的空閒空間數量低於這個值G1就會終止space-reclamation階段
-XX:G1MixedGCCountTarget=8 space-reclamation階段需要做多少次GC
-XX:G1MixedGCLiveThresholdPercent=85 如果存活的物件佔有率超過了這個值,space-reclamation階段不會回收這個region

注意:< ergo>的意思是實際的值是根據環境自適應確定的。

跟其他收集器的比較

G1和其他收集器的主要區別:

  • 並行收集器(Parallel GC)可以壓縮和回收old區的記憶體,但是隻能對old區整體來操作。G1可以把整個工作增量的分散到多個時間更短的停頓中。這在減少停頓時間的同時會犧牲一部分吞吐量。
  • 跟CMS類似,G1是併發的回收old區的記憶體,但是,CMS不會處理old區的碎片,最終就會導致長時間的FullGC。
  • 由於併發的原因,G1可能會表現出比其他收集器更高的開銷,這會影響吞吐量。

基於它的工作原理,G1有多種提高GC效率的機制:

  • G1在任何的停頓都可以回收一些全空或者大量的old區的記憶體。這會避免不必要的GC,因為可以不費吹灰之力就可以釋放大量的記憶體空間。
  • G1可以選擇對整個堆裡面的String進行並行去重。

回收old區的空的、大物件是預設開啟的,可以使用 -XX:-G1EagerReclaimHumongousObjects 這個選項禁用掉。String去重預設是不開啟的,可以使用 -XX:+G1EnableStringDeduplication 開啟。

最後再插播一次廣告:歡迎觀看本人錄得兩個視訊教程: