1. 程式人生 > >JVM探祕:垃圾收集器

JVM探祕:垃圾收集器

本系列筆記主要基於《深入理解Java虛擬機器:JVM高階特性與最佳實踐 第2版》,是這本書的讀書筆記。

垃圾收集器

垃圾收集演算法是是記憶體回收的方法論,垃圾收集器是記憶體回收的具體實現。不同的虛擬機器會有不同的垃圾收集器的實現,我們主要討論的是預設的HotSpot虛擬機器,這個虛擬機器包含的垃圾收集器如下圖;


如上圖所示,一共有7種垃圾收集器,如果兩個垃圾收集器之間有雙箭頭連線,則兩個垃圾收集器可搭配使用。上面是新生代的收集器,下面是老年代的收集器。每個垃圾收集器都有各自適合的使用場景。

Serial 收集器

Serial是一個“單執行緒”的新生代收集器,使用複製演算法,它只會使用一個CPU或者一條收集器執行緒去完成垃圾收集工作,並且它在垃圾收集時,必須暫停所有其他的工作執行緒,直到它收集結束。“Stop The World”會在使用者不可見的情況下,把使用者的工作執行緒全部停掉,這往往是令人難以接受的。

下圖是 Serial/Serial Old 收集器執行示意圖:

上圖中,新生代是Serial收集器採用複製演算法,老年代是Serial Old收集器採用標記-整理演算法。Serial雖然是一個缺點鮮明的收集器,但它依然是虛擬機器在Client模式下的預設收集器,它也有優點,比如簡單高效(與其他收集器單執行緒相比),對於單個CPU來說,Serial由於沒有執行緒互動的開銷,效率比較高,對於桌面應用來說,分配給虛擬機器的記憶體不會很大,收集時的停頓也是在可接受範圍內的。

ParNew 收集器

ParNew收集器是Serial收集器的多執行緒版本,也是使用複製演算法的新生代收集器,它除了使用多條執行緒進行垃圾收集以外,其他的比如收集器的控制引數、收集演算法、Stop-The-World、物件分配規則、回收策略都和Serial收集器完全一樣。

下圖是 ParNew/Serial Old 收集器執行示意圖:

上圖中,新生代是ParNew收集器採用複製演算法,老年代是Serial Old收集器採用標記-整理演算法。ParNew是許多Server模式下虛擬機器的首選新生代收集器,多是因為它能與CMS收集器配合工作。CMS收集器是HotSpot虛擬機器中第一個併發的垃圾收集器,CMS第一次實現了讓使用者執行緒與垃圾收集執行緒同時工作。

簡單介紹下垃圾收集中的並行與併發概念:

  • 並行(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒是等待狀態。
  • 併發(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行,使用者程式執行的同時,垃圾收集程式運行於另一個CPU上。

Parallel Scavenge 收集器

Parallel Scavenge也是使用複製演算法的新生代收集器,並且也是一個並行的多執行緒收集器。Parallel收集器跟其它收集器關注GC停頓時間不同,它關注的是吞吐量。低停頓時間適合需要與使用者互動的程式,而高吞吐量可以高效率的利用CPU時間,能儘快完成運算任務,適合用於後臺計算較多而互動較少的任務。

  • 吞吐量(Throughput):CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,吞吐量 = 執行使用者程式碼時間 /(執行使用者程式碼時間+垃圾收集時間)。

Parallel收集器提供了兩個虛擬機器引數用以控制吞吐量,-XX:MaxGCPauseMillis引數可以控制垃圾收集的最大停頓時間,-XX:GCTimeRatio引數可以直接設定吞吐量大小。

-XX:MaxGCPauseMillis的值是一個大於0的毫秒數,使用它減小GC停頓時間是犧牲吞吐量和新生代空間換來的,例如系統把新生代調小,收集300M的新生代肯定比500M的快,這也導致垃圾收集發生的更頻繁,原來10秒收集一次每次停頓100毫秒,現在5秒收集一次每次停頓70毫秒,停頓時間下降了,但是吞吐量也下降了。

-XX:GCTimeRatio的值是一個0到100的整數,通過它我們告訴JVM吞吐量要達到的目標值,-XX:GCTimeRatio=N指定目標應用程式執行緒的執行時間(與總的程式執行時間)達到N/(N+1)的目標比值。例如,它的預設值是99,就是說要求應用程式執行緒在整個執行時間中至少99/100是活動的(GC執行緒佔用其餘的1/100),也就是說,應用程式執行緒應該執行至少99%的總執行時間。

除這兩個引數外,還有一個引數-XX:-UseAdaptiveSizePolicy值得關注,這是一個開關引數,當它開啟之後,就不需要手工指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據系統的執行情況收集效能監控資訊,動態的調整這些引數來提高GC效能,這種調節方式稱為GC自適應調節策略。這個引數是預設啟用的,自適應行為也是JVM優勢之一。

Serial Old 收集器

Serial Old是Serial收集器的老年代版本,同樣是一個“單執行緒”收集器,使用標記-整理演算法。這個收集器主要是給Client模式下的虛擬機器使用,Server模式下還有兩個用途,一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另一個是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。工作過程請看Serial 收集器部分的 Serial/Serial Old 收集器執行示意圖。

Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多執行緒和標記-整理演算法。此收集器在JDK1.6中開始出現,在Parallel Old出現之前,只有Serial Old能夠與Parallel Scavenge收集器配合使用。由於Serial Old這種單執行緒收集器的效能拖累,導致在老年代比較大的場景下,Parallel Scavenge和Serial Old的組合吞吐量甚至還不如ParNew加CMS的組合。而有了Parallel Old收集器之後,Parallel Scavenge與Parallel Old成了名副其實的吞吐量優先的組合,在注重吞吐量和CPU資源敏感的場景下,都可以優先考慮這對組合。

下圖是 ParNew/Serial Old 收集器執行示意圖:

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是基於標記-清除演算法的老年代收集器,它以獲取最短回收停頓時間為目標。CMS是一款優秀的收集器,特點是併發收集、低停頓,它的執行過程稍微複雜些,分為4個步驟:

  1. 初始標記(CMS initial mark)
  2. 併發標記(CMS concurrent mark)
  3. 重新標記(CMS remark)
  4. 併發清除(CMS concurrent sweep)

4個步驟中只有初始標記、重新標記這兩步需要“Stop The World”。初始標記只是標記一下GC Roots能直接關聯的物件,速度很快。併發標記是進行GC Roots Tracing的過程,也就是從GC Roots開始進行可達性分析。重新標記則是為了修正併發標記期間因使用者執行緒繼續執行而導致標記發生變動的那一部分記錄。併發清理當然就是進行清理被標記物件的工作。

下圖是 CMS 收集器執行示意圖:

整個過程中,併發標記與併發清除過程耗時最長,但它們都可以與使用者執行緒一起工作,所以整體上說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

但是CMS收集器也並不完美,它有以下3個缺點:

  1. CMS收集時對CPU資源非常敏感,併發階段雖然不會導致使用者執行緒停頓,但是會因為佔用CPU資源導致應用程式變慢、總吞吐量變低。
  2. CMS收集器無法處理浮動垃圾(Floating Garbage),可能會產生Full GC。浮動垃圾就是在併發清理階段,依然在執行的使用者執行緒產生的垃圾。這部分垃圾出現在標記過程之後,CMS無法在當次集中處理它們,只能等下一次GC時清理。
  3. CMS是基於標記-清除演算法的收集器,可能會產生大量的空間碎片,從而無法分配大物件而導致Full GC提前產生。

G1 收集器

G1(Garbage-First)收集器是面向服務端應用的垃圾收集器,它被寄予厚望以用來替換CMS收集器。在G1之前的收集器中,收集的範圍要麼是整個新生代要麼就是老年代,而G1不再從物理上區分新生代老年代,G1可以獨立管理整個Java堆。它將Java堆劃分為多個大小相等的獨立區域(Region),雖然還有新生代老年代的概念,但不再是物理隔離的,而都是一部分Region(不需要連續)的集合。

與其他收集器相比,G1收集器的特點有:

  1. 並行與併發:G1能充分利用多CPU或者多核心的CPU,來縮短Stop The World的停頓時間。
  2. 分代收集:雖然G1收集器可以獨立管理整個GC堆,但它能採用不同的方式處理“新物件”和“老物件”,以達到更好的收集效果。
  3. 空間整合:G1從整體看是基於標記-整理演算法的,從區域性看(兩個Region之間)是基於複製演算法實現的,這兩個演算法在收集時都不會產生空間碎片,這樣就有連續可用的記憶體用以分配大物件。
  4. 可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型,可以明確指定一個最大停頓時間(-XX:MaxGCPauseMillis),停頓時間需要不斷調優找到一個理想值,過大過小都會拖慢效能。

G1收集器之所以能建立可預測的停頓時間模型,是因為它可以避免在整個Java堆中進行全區域的垃圾收集,G1根據各個Region裡垃圾堆積的價值大小(回收所獲空間大小及所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,這也是Garbage-First名稱的由來。

G1收集器的Region如下圖所示:

圖中的E代表是Eden區,S代表Survivor,O代表Old區,H代表humongous表示巨型物件(大於Region空間的物件)。從圖中可以看出各個區域邏輯上並不是連續的,並且一個Region在某一個時刻是Eden,在另一個時刻就可能屬於老年代。G1在進行垃圾清理的時候就是將一個Region的物件拷貝到另外一個Region中。

再介紹一個概念:Remembered Set(記憶集)。每個Region中都有一個Remembered Set,記錄的是其他Region中的物件引用本Region物件的關係(誰引用了我的物件)。所以在垃圾回收時,在GC根節點的列舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。G1裡面還有另外一種資料結構叫Collection Set,Collection Set記錄的是GC要收集的Region的集合,Collection Set裡的Region可以是任意代的。在GC的時候,對於跨代物件引用,只要掃描對應的Collection Set中的Remembered Set即可。

不算上維護Remembered Set的話,G1收集器的收集過程如下圖所示:

如圖所示,G1收集過程有如下幾個階段:

  1. 初始標記(Initial Marking)
  2. 併發標記(Concurrent Marking)
  3. 最終標記(Final Marking)
  4. 篩選回收(Live Data Counting and Evacuation)

初始標記只標記一下GC Roots能關聯到的物件,需要停頓執行緒但是耗時短,會停頓使用者執行緒(Stop the World)。併發標記是從GC Root開始對堆中物件進行可達性分析,找出存活物件,這階段耗時長但是可以與使用者執行緒併發執行。最終標記就是為了修正在併發標記階段,因使用者執行緒繼續執行而導致標記產生變動的那一部分標記記錄,這階段需要停頓使用者執行緒(Stop the World),但是可並行執行。篩選回收階段會對各個Region的回收價值和成本進行排序,根據使用者期望的GC停頓時間來制定回收計劃,該階段也是會停頓使用者執行緒(Stop the World)。

垃圾收集引數

查詢當前使用的垃圾收集器:
java -XX:+PrintCommandLineFlags -version
此命令讓JVM打印出那些已經被使用者或者JVM設定過的詳細的XX引數的名稱和值。

<
VM引數 描述
-XX:+UseSerialGC 指定Serial收集器+Serial Old收集器組合執行記憶體回收
-XX:+UseParNewGC 指定ParNew收集器+Serilal Old組合執行記憶體回收
-XX:+UseParallelGC 指定Parallel收集器+Serial Old收集器組合執行記憶體回收
-XX:+UseParallelOldGC 指定Parallel收集器+Parallel Old收集器組合執行記憶體回收
-XX:+UseConcMarkSweepGC 指定CMS收集器+ParNew收集器+Serial Old收集器組合執行記憶體回收。優先使用ParNew收集器+CMS收集器的組合,當出現ConcurrentMode Fail或者Promotion Failed時,則採用ParNew收集器+Serial Old收集器的組合
-XX:+UseG1GC 指定G1收集器併發、並行執行記憶體回收
-XX:+PrintGCDetails 列印GC詳細資訊
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式)
-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的資訊
-XX:+PrintTenuringDistribution 在進行GC時列印survivor中的物件年齡分佈資訊
-Xloggc:$CATALINA_HOME/logs/gc.log 指定輸出路徑收集日誌到日誌檔案
-XX:NewRatio 新生代與老生代(new/old generation)的大小比例(Ratio). 預設值為 2
-XX:SurvivorRatio eden/survivor 空間大小的比例(Ratio). 預設值為 8
-XX:GCTimeRatio GC時間佔總時間的比率,預設值99%,僅在Parallel Scavenge收集器時生效
-XX:MaxGCPauseMills 設定GC最大停頓時間,僅在Parallel Scavenge收集器時生效
-XX:PretensureSizeThreshold 直接晉升到老年代的物件大小,大於這個引數的物件直接在老年代分配
-XX:MaxTenuringThreshold 提升年老代的最大臨界值(tenuring threshold). 預設值為 15
-XX:UseAdaptiveSizePolicy 動態調整Java堆中各個區域的大小及進入老年代的年齡
-XX:HandlePromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代整個Eden和Survivor中物件都存活的極端情況
-XX:ParallelGCThreads 設定垃圾收集器在並行階段使用的執行緒數,預設值隨JVM執行的平臺不同而不同
-XX:ParallelCMSThreads 設定CMS的執行緒數量
-XX:ConcGCThreads 併發垃圾收集器使用的執行緒數量. 預設值隨JVM執行的平臺不同而不同
-XX:CMSInitiatingOccupancyFraction 設定CMS收集器在老年代空間被使用多少後觸發垃圾收集,預設68%
-XX:+UseCMSCompactAtFullCollection 設定CMS收集器在完成垃圾收集後是否要進行一次記憶體碎片的整理
-XX:CMSFullGCsBeforeCompaction 設定進行多少次CMS垃圾回收後,進行一次記憶體壓縮
-XX:+CMSClassUnloadingEnabled 允許對類元資料進行回收
-XX:CMSInitiatingPermOccupancyFraction 當永久區佔用率達到這一百分比時,啟動CMS回收
-XX:UseCMSInitiatingOccupancyOnly 表示只在到達閥值的時候,才進行CMS回收
-XX:InitiatingHeapOccupancyPercent 指定當整個堆使用率達到多少時,觸發併發標記週期的執行,預設值是45%
-XX:G1HeapWastePercent 併發標記結束後,會知道有多少空間會被回收,再每次YGC和發生MixedGC之前,會檢查垃圾佔比是否達到此引數,達到了才會發生MixedGC
-XX:G1ReservePercent 設定堆記憶體保留為假天花板的總量,以降低提升失敗的可能性. 預設值是 10
-XX:G1HeapRegionSize 使用G1時Java堆會被分為大小統一的的區(region)。此引數可以指定每個heap區的大小. 預設值將根據 heap size 算出最優解. 最小值為 1Mb, 最大值為 32Mb