JVM - 內功修煉之垃圾收集器

JVM - 內功修煉之垃圾收集器

1.垃圾收集器概述

 如果說上一篇講的垃圾回收算法是內存回收的方法論,那這一篇我們要介紹的垃圾收集器就是內存回收的具體實現。

 Java虛擬機規範中對垃圾收集器應該如何實現並沒有任何規定,因此不同的廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大差別,並且一般都會提供參數供用戶根據自己的應用特點和要求組合出各個年代所使用的收集器。

 我們需要明確一個觀點,我們接下來會對一部分垃圾收集器進行介紹以及比較,但目的並不是爲了篩選出最好的垃圾收集器。因爲目前爲止並沒有一款最好的垃圾收集器可以同時滿足所有場景,都需要根據實際情況進行選擇。如果目前有這麼一款完美的垃圾收集器的話,那各大廠商也不會實現那麼多不同的垃圾收集器了。

2.Serial垃圾收集器

在這裏插入圖片描述

2.1 Serial垃圾收集器是什麼?

Serial垃圾收集器是最基本、發展歷史最悠久的收集器。這個收集器是一個單線程的垃圾收集器,它不僅僅在進行垃圾回收時只會使用單個線程去回收。如上圖所示,當達到一個安全點時會觸發GC線程,此時Serial垃圾收集器開始工作,並且在回收的同時會將其他所有工作線程暫停,直到回收結束。這就是大名鼎鼎“Stop The World”-STW

2.2 Serial垃圾收集器的特點

 這種收集器在思路上比較簡單但是在很多時候也比較實用,不然也不會在某種情況下作爲默認收集器。這裏我們可以總結出幾個特點:

  1. “Stop The World(STW)”:當它進行垃圾回收時,必須暫停其他所有的工作線程,直到它收集結束。
  2. 效率:由於採用單線程處理垃圾回收,無需過多的線程交互,簡單高效。當JVM管理小數據量內存(新生代幾十到一兩百M)時,停頓時間可以控制在幾十毫秒最多一百多毫秒內,也可通過-XX:+UseSerialGC配置該垃圾收集器。
  3. 使用場景:多用於桌面應用,Client端的垃圾回收器,由於桌面應用內存小,進行垃圾回收的時間比較短,只要不頻繁發生停頓就可以接受。

3.ParNew垃圾收集器

在這裏插入圖片描述

3.1 ParNew垃圾收集器是什麼?

ParNew收集器其實就是上面Serial收集器的多線程版本,除了使用多條線程秉性進行垃圾收集之外,其餘包括Serial收集器可用的所有控制參數、收集算法、Stop The World(STW)、對象分配規則、回收策略等都與Serial收集器完全一致。在實現上,這兩種收集器也共用了相當多的代碼。

3.2 ParNew垃圾收集器的特點

  1. ParNew收集器除了多線程並行收集之外,其他與Serial收集器相比並沒有太大差異,但它卻是許多運行在 Server模式下的虛擬機中首選的新生代收集器。其中有一個與性能無關但很重要的原因就是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。
  2. 可以使用-XX: ParallelGCThreads參數來限制垃圾收集的線程數。
  3. 由於存在線程切換的開銷,ParNew收集器在單CPU的環境中比不上Serial收集器。但隨着可用的CPU數量的增加, 收集效率肯定也會大大增加,建議將-XX: ParallelGCThreads設置成和CPU核數相同,如果設置太多的話就會產生上下文切換消耗。

3.3 並行和併發

 爲了接下來各個垃圾收集器的介紹更加順利,這裏先給大家簡單接受兩個概念:並行和併發。

  1. 並行(Parallel):多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。
  2. 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在保持運行,而垃圾收集程序運行於另一個CPU上。

4.Parallel Scavenge垃圾收集器

4.1 Parallel Scavenge垃圾收集器是什麼?

Parallel Scavenge收集器是一個新生代收集器,同樣也是一種使用複製算法、並行的多線程垃圾收集器。和ParNew收集器不同的是Parallel Scavenge更關注吞吐量,由於與吞吐量關係密切,Parallel Scavenge收集器也經常稱爲“吞吐量優先”收集器。

 那麼吞吐量是什麼呢?即CPU用於運行用戶代碼的時間與CPU總時間的比值,假設99分鐘時間用於執行用戶線程,1分鐘時間用於回收垃圾 ,此時吞吐量就是99%。

4.2 Parallel Scavenge垃圾收集器是特點

  1. Parallel Scavenge收集器的特點主要是其關注點與其他收集器不同,上述其他收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。

    上面我們也提到了,所謂吞吐量就是CPU用於執行用戶代碼時間與CPU總消耗時間的比值,即吞吐量=執行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的場景。
  2. 虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應調節策略。
  3. 可以通過-XX:MaxGCPauseMillis參數修改GC停頓時間,當然並不會越小越好,如果我們將該參數設置太小,每次GC清除的內存空間就會變少,則會導致程序頻繁發生GC。停頓時間雖然減少了,但是吞吐量也下降了。
  4. 可以通過-XX:GCTimeRatio參數參數修改垃圾收集時間佔總時間比例,相當於吞吐量倒數。例如設置爲19則表示允許最大GC時間佔總時間比5%,即1/(1+19)。默認爲99,則允許最大GC時間爲1%,即1/(1+99)。

5.Serial Old垃圾收集器

在這裏插入圖片描述

5.1 Serial Old垃圾收集器是什麼?

 記下來要介紹的這兩個垃圾收集器因爲就是在之前介紹的基礎上有一些簡單的區別,所以這裏會簡單介紹下。

Serial Old收集器其實就是Serial收集器的老年代版本,主要意義也是給Client模式下使用。它也是一個單線程收集器,並且採用標記-整理算法。當然也可以被選作Server模式使用,主要作用有兩點:

  1. 在JDK1.5及之前版本可能會被選擇與Parallel Scavenge收集器搭配使用。
  2. 作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

6.Parallel Old垃圾收集器

在這裏插入圖片描述

6.1 Parallel Old垃圾收集器是什麼?

 跟Serial Old類似,Parallel Old收集器其實就是Parallel Scavenge收集器的老年代版本。採用多線程+標記-整理算法

 上面我們也提到了,JDK1.5及之前版本Parallel Scavenge收集器一般都和Serial Old收集器是黃金搭檔。一個主要的原因就是由於Parallel Scavenge無法和CMS等收集器配合,這個後面講解CMS收集器時會提到。而由於Serial Old收集器在服務端應用的表現並不理想,所以Parallel Scavenge並不能在整體應用上體現吞吐量最大化的效果。在某些老年代空間較大並且硬件高級(CPU處理能力強勁)的環境下,這對黃金搭檔的吞吐量可能還不及ParNew+CMS給力。

 從JDK1.6開始,Parallel Old登上了舞臺。此時吞吐量優先的收集器纔有了名副其實的名頭,在注重吞吐量和CPU資源敏感的場景下,都可以優先考慮Parallel ScavengeParallel Old收集器的組合。

7.CMS垃圾收集器(Concurrent Mark Sweep)

7.1 CMS垃圾收集器是什麼?

 看到這裏,相信各位對JVM中的垃圾收集器已經有了一個不錯的認識了。結合之前介紹的垃圾回收算法,攻破垃圾收集器實在不在話下。

 那麼CMS垃圾收集器是什麼?這是一種以最短回收停頓時間爲核心目標的收集器,基於標記-清除算法實現的。

 目前很大一部分的Java應用集中在服務端模式上,這類服務十分重視服務的響應速度,停頓時間越短給用戶帶來的體驗越好。而CMS收集器就非常符合這種場景。

7.2 CMS垃圾收集器的步驟流程

在這裏插入圖片描述

 從上圖我們看到,CMS收集器進行垃圾回收主要有4個步驟:

  1. 初始標記(Initial Mark):僅僅標記一下GC Roots能直接關聯到的對象,速度很快,需要停頓。
  2. 併發標記(Concurrent Mark):併發標記階段就是進行GC RootsTracing,也就是標記引用鏈的過程,它在整個回收過程中耗時最長,但不需要停頓。
  3. 重新標記(Remark):用於修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要停頓。
  4. 併發清除(Concurrent Sweep):清除標記內存空間,不需要停頓。


 可以看出整個過程中耗時最長的兩個階段是併發標記併發清除,而此時GC線程是可以與用戶線程併發工作的,所以並不會產生停頓。

7.3 CMS垃圾收集器的優缺點

 上面我們所瞭解了這麼多垃圾收集器,其實比對可以發現,CMS已經算得上是一款優秀的垃圾收集器了。它的優點也很明顯:併發標記併發收集低停頓。不過僅憑這些,CMS還遠達不到完美的程度,主要是因爲這種設計存在3個明顯的缺點:

  1. 首先從CMS收集器的設計上我們就能很清楚的感受到,它對CPU資源其實是十分敏感的。在併發標記併發刪除階段,雖然不會使用戶線程停頓,但是自身GC線程會佔用一部分資源從而導致用戶線程性能下降,吞吐量降低。CMS默認啓動的回收線程數=(CPU數量+3)/4,當CPU數量很少時,還要分出一大部分資源來同時進行垃圾回收,其實對性能的影響也是十分明顯的。

    可以看出低停頓時間是以犧牲吞吐量爲代價的。由於這種設計,其實所有面向併發設計程序對於CPU資源都是十分敏感的。

  1. 我們可以在腦海中回想一下剛纔我們所瞭解的CMS收集器的整個垃圾回收過程。當我們在併發清除階段,其實用戶線程依然是在執行的。而此時仍然會不斷有新的垃圾產生,這些新產生的垃圾並未被標記,所以這一部分垃圾會在下一個GC週期裏面被清除,這部分垃圾就叫做浮動垃圾(Floating Garbage)

    由於浮動垃圾的存在,因此需要預留一部分內存空間給用戶線程。當預留內存不足以存放浮動垃圾時,則會出現Concurrent Mode Failure,此時虛擬機將會臨時啓用Serial Old收集器來重新進行老年代的垃圾回收。如果我們應用中老年代增速不是太快,可以通過-XX:CMSInitiatingOccupancyFraction來提高GC觸發百分比,以降低內存回收次數從而提升性能;但若將此參數設置太高,可能會出現大量的Concurrent Mode Failure導致性能反而降低。

  1. 不知大家是否還得之前我們講過的,CMS收集器基於標記-清除算法,那麼意味着在GC後有可能會產生大量的空間碎片。若空間碎片過多的話,將會對大對象的分配造成影響,時常會出現老年代中還有不少空間剩餘但卻無法爲大對象分配內存,從而不得不提前觸發Full GC

    針對這個問題,CMS收集器其實是提供一個內存碎片合併整理的機制,可以通過-XX:+UseCMSCompactAtFullCollection參數開啓或關閉(默認開發)。該機制會在上面提及情況觸發Full GC時對內存碎片進行合併整理,但是這個過程是無法併發的,所以雖然解決了空間碎片問題,但也帶來了停頓時間增長的問題。另外我們還可以通過調整-XX:CMSFullGCsBeforeCompaction參數去控制每執行多少次不壓縮的Full GC後會從而進行一次空間碎片合併整理。

8.G1垃圾收集器(Garbage-First)

8.1 G1垃圾收集器是什麼?

G1(Garbage-First)是一款面向服務端應用的垃圾收集器,在多CPU和大內存的場景下都有很好的性能。這款垃圾收集器是目前爲止在垃圾收集器技術發展上都是比較前沿的成果之一。在JDK7-u4版本起就已經完全支持G1垃圾收集器了,而HotSpot開發團隊賦予給它的使命也是在未來可以替換掉CMS

 我們都知道堆被分爲新生代和老年代,對於我們之前所瞭解的其他收集器來說都是針對整個新生代或者整個老年代進行垃圾回收,而G1的設計可以對整個新生代以及老年代一起進行回收。官方也提供了【G1GettingStarted】可供我們對G1進行進一步的瞭解。

8.2 G1垃圾收集器的特點

在這裏插入圖片描述

 衆所周知我們所瞭解老的收集器將堆分爲三個固定內存大小的新生代老年代永久代。所有內存對象最終都屬於這三個部分之一。而G1收集器採用了不同的放劃分方式。
在這裏插入圖片描述
G1收集器將整個Java堆劃分爲N個大小相等的獨立區域(Region),也就是說新生代和老年代在某種意義上來說不再是物理隔離的。

 這種劃分方式,由於每個獨立區域的大小並非固定,使得內存在使用方面有了更大的靈活性。而每個Region都有一個與之對應的Remembered Set,用來記錄該Region對象的引用對象所在的Region,這樣做可以避免在做可達性分析的時進行全堆掃描。

 而正是由於其設計特點,也就有了其特有的優勢,這些下面我們會提到。

8.3 G1垃圾收集器的步驟流程

在這裏插入圖片描述

 從上圖我們看到,G1收集器進行垃圾回收主要有4個步驟:

  1. 初始標記(Initial Mark):僅僅標記一下GC Roots能直接關聯到的對象,速度很快,需要停頓。
  2. 併發標記(Concurrent Mark):併發標記階段就是進行GC RootsTracing,也就是標記引用鏈的過程,它在整個回收過程中耗時最長,但可與用戶程序併發執行不需要停頓。
  3. 最終標記(Final Marking):用於修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到 Remembered Set 中。這階段需要停頓線程,但是可並行執行。
  4. 篩選回收(Live Data Counting And Evacuation):先對各個Region中的回收價值和成本進行排序,根據用戶所期望的GC停頓時間制定回收計劃。此階段其實也可以做到與用戶程序一起併發執行,但是因爲只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率,故默認停頓並行回收。

8.4 G1垃圾收集器的優缺點

 上面我們對G1收集器獨有的特點以及其運行流程都有了一個比較清晰的認識,我們會發現這款垃圾收集器其實結合了我們上面所瞭解收集器的大部分優點以及針對很多其他收集器有的缺點都提供了具體的解決方案。這裏我們來看看G1收集器幾個主要的優點:

  1. 並行與併發:這一點不用多說,作爲官方希望取代CMS的一款收集器,G1收集器能夠充分利用多CPU、多核的硬件優勢,儘可能地去縮短停頓時間。並且仍可選擇是否需要併發執行用戶線程。
  2. 空間整合:整體基於標記-整理算法實現,局部Region採用複製算法實現,這兩種算法都可使程序在運行期間不會產生內存空間碎片。
  3. 可預測的停頓G1收集器除了追求低停頓外,還能夠建立可預測的停頓時間模型。在使用的過程中,我們可以指定在一個長度X毫秒的時間片段內,消耗在GC上的時間不得超過Y毫秒。

    G1收集器之所以能夠建立可預測的停頓時間模型,和其獨特的堆區域劃分密不可分。它會記錄每個Region中垃圾堆積的價值(回收所需時間以及回收所得空間),並通過其數值維護一個優先級列表,每次根據允許的手機時間優先回收價值最大的RegionGarbage First也因此得名。這種方式也保證了G1收集器在有限時間內可以保證儘可能高的收集效率。

 瞭解過G1垃圾收集器的同學會發現一個現象,那就是在大多數書籍或者網上相關技術介紹都很少會提到G1的缺點。主要原因還是G1收集器雖然被官方計劃是替代CMS的選擇,但其在商用層面的案例還是很少的,並沒有表現出足夠的性能優勢。

G1收集器得不到廣泛商用的原因就和我們使用JDK等一樣,大部分項目我們都會儘可能去保證在一個穩定的情況下運行,並不會過於激進在真實項目中去嘗試使用過於新的技術。不過相信在不久的將來,若G1收集器不斷優化優勢越加明顯後就會被大家廣泛接受推廣。

8.5 G1垃圾收集器的應用場景

G1主要的應用場景就是針對需要運行堆內存較大並且GC延遲有限的應用程序,官方介紹說在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒。

 在很多情況下,應用程序可能搭配的是JDK1.5提供的CMS收集器,而我們在以下場景可以嘗試替換爲G1收集器:

  1. 完整GC時間過長或過於頻繁(大於0.5s-1s)。
  2. 對象分配頻率或年代提升頻率變化較大。
  3. 堆內存中活動數據佔用過多。

 也就是說如果我們程序未經理長時間的垃圾收集停頓,可以考慮使用原有ParallelOldCMS收集器。而且是否需要採用G1替代原有收集器也需要我們經過實際場景驗證纔可以下定論。

8.6 G1垃圾收集器的參數配置

 既然我們要使用G1收集器,那就需要對幾個簡單的參數配置做基本的瞭解:

  1. -XX:+UseG1GC:該參數用於指定使用G1收集器。
  2. -XX:MaxGCPauseMillis:設置G1收集器停頓時間,默認200ms。
  3. -XX:G1HeapRegionSize:設置單個Region大小(1MB-32MB),默認會根據堆大小計算最優值。
  4. -XX:InitiatingHeapOccupancyPercent:當整個Java堆的佔用率達到參數值時,開始併發標記階段。默認爲45。

9.總結

 關於JVM中垃圾收集器到這裏就告一段落了,我們這裏將大部分垃圾收集器都做了一個介紹,我們對每種收集器的特點以及流程也都有了一個大致的認識。不過這些知識對於整個JVM還只是冰山一角,但是當我們在實際開發中遇到一些JVM內存相關的問題時還是可以在一定程度上避免我們毫無頭緒的。

 當然,對於大多數情況以咱們瞭解的這些還是能夠作爲敲門磚摸到一些門路去想辦法找到問題根源的。不過這裏還是不建議大家在瞭解片面的情況下就將以往的收集器給隨意替換掉的,任何工具都有其適用的場景,適合你的不一定是最好的,最好的也不一定是適合你的。

 最後還是希望這些能夠給大家帶來幫助。這裏正逢2020到來,祝大家新年快樂,願大家只爭朝夕,不負韶華

發佈了155 篇原創文章 · 獲贊 78 · 訪問量 406萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章