JVM — 虛擬機垃圾收集器(四)

垃圾收集器

JVM堆空間圖示:
在這裏插入圖片描述
從上圖可以看出堆內存的分爲新生代、老年代和永久代。新生代又被進一步分爲:Eden 區+Survior1 區+Survior2 區。值得注意的是,在 JDK 1.8中移除整個永久代,取而代之的是一個叫元空間(Metaspace)的區域(永久代使用的是JVM的堆內存空間,而元空間使用的是物理內存,直接受到本機的物理內存限制)。

1、 新生代收集器

1.1、Serial收集器

Serial 收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK1.3.1之前)是虛擬機新生代收集的唯一選擇,使用複製算法。這個收集器是一個單線程的收集器,但它的”單線程“的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。”Stop The World“這項工作是由虛擬機在後臺自動發起和自動完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。
Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器。它有着優於其他收集器的地方:簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。所以,Serial收集器對於運行在Client模式下的虛擬機來說是一個很好的選擇。
總結:

  • 針對新生代
  • 串行
  • 複製算法
  • 單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工作,
  • 另一方面也意味着在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束爲止,這個過程也稱爲 Stop The world。
  • 後者意味着,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這顯然對很多應用是難以接受的。

下圖示意了Serial收集器的運行過程:
在這裏插入圖片描述

1.2、ParNew收集器

ParNew收集器就是Serial收集器的多線程版本,它也是一個新生代收集器。除了使用多線程進行垃圾收集外,其餘行爲包括Serial收集器可用的所有控制參數、收集算法(複製算法)、Stop The World、對象分配規則、回收策略等與Serial收集器完全相同,也同樣使用複製算法,兩者共用了相當多的代碼。

ParNew收集器除了使用多線程收集外,其他與Serial收集器相比並無太多創新之處,但它卻是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作,CMS收集器是HotSpot虛擬機中第一款真正意義上的併發(Concurrent)收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
ParNew 收集器在單CPU的環境中絕對不會有比Serial收集器有更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越。在多CPU環境下,隨着CPU的數量增加,它對於GC時系統資源的有效利用是很有好處的。
總結:

  • 針對新生代
  • 複製算法
  • 串行
  • 多線程
  • GC時需要暫停所有用戶線程,直到GC結束
  • Serial多線程版本,其他特點與Serial相同

ParNew收集器的工作過程如下圖:
在這裏插入圖片描述
注意:從ParNew收集器開始,後面還會接觸到幾款併發和並行的收集器。併發和並行都是併發編程中的概念,在垃圾收集器的上下文語境中,它們可以解釋如下:

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

1.3、 Parallel Scanvenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的**-XX:GCTimeRatio**參數。
由於與吞吐量關係密切,Parallel Scavenge收集器也經常稱爲“吞吐量優先”收集器。除上述兩個參數之外,Parallel Scavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)。自適應調節策略是Parallel Scavenge收集器與ParNew收集器的一個重要區別。
總結:

  • 針對新生代,Server模式下的默認垃圾收集器
  • 複製算法
  • 並行
  • 多線程
  • 高吞吐量爲目標,自適應調節策略

2、老年代收集器

2.1、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在於給Client模式下的虛擬機使用。如果在Server模式下,它主要還有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。Serial Old收集器的工作過程如圖所示:
在這裏插入圖片描述
總結:

  • Serial 新生代收集器採用的是複製算法,Serial Old 老年代採用的是標記 - 整理算法
  • Serial Old是Serial的老年代版本,除了採用標記-整理算法,其他與Serial相同

2.2、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇。由於老年代Serial Old收集器在服務端應用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由於單線程的老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。
  直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。
Parallel Old收集器的工作過程如圖所示:
  在這裏插入圖片描述
總結:

  • Parallel Old是Parallel Scavenge的老年代版本
  • Parallel Old 老年代採用的是標記 - 整理算法,其他特點與Parallel Scavenge相同
  • 在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器組合
  • JDK1.6及之後用來代替老年代的Serial Old收集器;特別是在Server模式,多CPU的情況下;
  • -XX:+UseParallelOldGC:指定使用Parallel Old收集器;

2.3、CMS(Concurrent Mark Sweep)收集器

CMS是HotSpot在JDK5推出的第一款真正意義上的併發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;命名中用的是concurrent,而不是parallel,說明這個收集器是有與工作執行併發的能力的。MS則說明算法用的是Mark Sweep算法。它關注的是垃圾回收最短的停頓時間(低停頓),在老年代並不頻繁GC的場景下,是比較適用的。CMS是一種以獲取最短回收停頓時間爲目標的收集器。在重視響應速度和用戶體驗的應用中,CMS應用很多。

CMS收集器是基於“標記-清除”算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分爲4個步驟,包括:

1. 初始標記(CMS initial mark)

  • 單線程執行
  • 需要“Stop The World”
  • 但僅僅把GC Roots的直接關聯可達的對象給標記一下,由於直接關聯對象比較小,所以這裏的速度非常快

2. 併發標記(CMS concurrent mark)

  • 進行GC Roots Tracing的過程,從剛纔產生的集合中標記出存活對象;(也就是從GC Roots 開始對堆進行可達性分析,找出存活對象。)
  • 耗時較長,但應用程序也在運行;
  • 並不能保證可以標記出所有的存活對象;

3. 重新標記(CMS remark)

  • 最終標記和CMS的重新標記階段一樣,也是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,
  • 這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,
  • 也需要“Stop The World”。

4. 併發清除(CMS concurrent sweep)

  • 併發清除之前所標記的垃圾。
  • 其他用戶線程仍可以工作,不需要停頓。

CMS GC過程分四步完成:
在這裏插入圖片描述
參數:

  1. -XX:+UseConcMarkSweepGC:使用CMS收集器
  2. -XX:+ UseCMSCompactAtFullCollection:Full GC後,進行一次碎片整理;整理過程是獨佔的,會引起停頓時間變長
  3. -XX:+CMSFullGCsBeforeCompaction:設置進行幾次Full GC後,進行一次碎片整理
  4. -XX:ParallelCMSThreads:設定CMS的線程數量(一般情況約等於可用CPU數量)

缺點:
1、對CPU資源非常敏感
CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大,如果本來CPU負載就比較大,還分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然降低了50%。

2、浮動垃圾(Floating Garbage)
CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。
如果CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣會導致另一次Full GC的產生。這樣停頓時間就更長了,代價會更大,所以 "-XX:CMSInitiatingOccupancyFraction"不能設置得太大。
3、產生大量內存碎片
還有最後一個缺點,CMS是一款基於“標記-清除”算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啓的),用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機設計者還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入Full GC時都進行碎片整理)。

3、整堆收集器

3.1、G1收集器

G1(Garbage-First)是JDK7-u4才推出商用的收集器;G1(Garbage - First)名稱的由來是G1跟蹤各個Region裏面的垃圾堆的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。注意:G1與前面的垃圾收集器有很大不同,它把新生代、老年代的劃分取消了!這樣我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。
特點:

  • 並行與併發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程序繼續執行。
  • 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式取處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。
  • 空間整合:與CMS的“標記-清理”算法不同,G1從整體來看是基於“標記-整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的,但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次GC。
  • 可預測的停頓:這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,可以有計劃地避免在Java堆的進行全區域的垃圾收集;能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。

G1算法將堆劃分爲若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域複製到另外一個區域,完成了清理工作。這就意味着,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有CMS內存碎片問題的存在了。
在這裏插入圖片描述
在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認爲這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。爲了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC。在java 8中,持久代也移動到了普通的堆內存空間中,改爲元空間。

使用場景:

如果你的應用追求低停頓,那G1現在已經可以作爲一個可嘗試選擇,如果你的應用追求吞吐量,那G1並不會爲你帶來什麼特別的好處。

  • 面向服務端應用,針對具有大內存、多處理器的機器;最主要的應用是爲需要低GC延遲,並具有大堆的應用程序提供解決方案;
    如:在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒;
  • 用來替換掉JDK1.5的CMS收集器;

G1收集器運作過程:
在這裏插入圖片描述
1、初始標記(Initial Marking)
初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。
2、併發標記(Concurrent Marking)
進行GC Roots Tracing的過程,從剛纔產生的集合中標記出存活對象;(也就是從GC Roots 開始對堆進行可達性分析,找出存活對象。)耗時較長,但應用程序也在運行;
並不能保證可以標記出所有的存活對象。
3、最終標記(Final Marking)
最終標記和CMS的重新標記階段一樣,也是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要“Stop The World”。
4、篩選回收(Live Data Counting and Evacuation)
首先排序各個Region的回收價值和成本;然後根據用戶期望的GC停頓時間來制定回收計劃;最後按計劃回收一些價值高的Region中垃圾對象;回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另一個空的Region,並且在此過程中壓縮和釋放內存;
可以併發進行,降低停頓時間,並增加吞吐量;
參數

  • “-XX:+UseG1GC”:指定使用G1收集器;
  • “-XX:InitiatingHeapOccupancyPercent”:當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45;
  • “-XX:MaxGCPauseMillis”:爲G1設置暫停時間目標,默認值爲200毫秒;
  • “-XX:G1HeapRegionSize”:設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個;

4、ZGC收集器

在JDK 11當中,加入了實驗性質的ZGC。它的回收耗時平均不到2毫秒。它是一款低停頓高併發的收集器。ZGC幾乎在所有地方併發執行的,除了初始標記的是STW的。所以停頓時間幾乎就耗費在初始標記上,這部分的實際是非常少的。那麼其他階段是怎麼做到可以併發執行的呢?ZGC主要新增了兩項技術,一個是着色指針Colored Pointer,另一個是讀屏障Load Barrier。ZGC 是一個併發、基於區域(region)、增量式壓縮的收集器。Stop-The-World 階段只會在根對象掃描(root scanning)階段發生,這樣的話 GC 暫停時間並不會隨着堆和存活對象的數量而增加。
ZGC 的設計目標

  1. TB 級別的堆內存管理;
  2. 最大 GC Pause 不高於 10ms;
  3. 最大的吞吐率(Throughput)損耗不高於 15%;
  4. 關鍵點:GC Pause 不會隨着 堆大小的增加 而增大。

ZGC 中關鍵技術

  • 加載屏障(Load barriers)技術;
  • 有色對象指針(Colored pointers);
  • 單一分代內存管理(這一點很有意思);
  • 基於區域的內存管理;
  • 部分內存壓縮;
  • 即時內存複用。

並行化處理階段

  • 標記(Marking);
  • 重定位(Relocation)/壓縮(Compaction);
  • 重新分配集的選擇(Relocation set selection);
  • 引用處理(Reference processing);
  • 弱引用的清理(WeakRefs Cleaning);
  • 字符串常量池(String Table)和符號表(Symbol Table)的清理;
  • 類卸載(Class unloading);

着色指針Colored Pointer
ZGC利用指針的64位中的幾位表示Finalizable、Remapped、Marked1、Marked0(ZGC僅支持64位平臺),以標記該指向內存的存儲狀態。相當於在對象的指針上標註了對象的信息。注意,這裏的指針相當於Java術語當中的引用。
在這個被指向的內存發生變化的時候(內存在Compact被移動時),顏色就會發生變化。
在G1的時候就說到過,Compact階段是需要STW,否則會影響用戶線程執行。那麼怎麼解決這個問題呢?

讀屏障Load Barrier
由於着色指針的存在,在程序運行時訪問對象的時候,可以輕易知道對象在內存的存儲狀態(通過指針訪問對象),若請求讀的內存在被着色了,那麼則會觸發讀屏障。讀屏障會更新指針再返回結果,此過程有一定的耗費,從而達到與用戶線程併發的效果。

與標記對象的傳統算法相比,ZGC在指針上做標記,在訪問指針時加入Load Barrier(讀屏障),比如當對象正被GC移動,指針上的顏色就會不對,這個屏障就會先把指針更新爲有效地址再返回,也就是,永遠只有單個對象讀取時有概率被減速,而不存在爲了保持應用與GC一致而粗暴整體的Stop The World。

參數
ZGC回收機預計在jdk11支持,ZGC目前僅適用於Linux / x64 。和G1開啓很像,用下面參數即可開啓:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章