G1垃圾收集器概述

前言

開始學習前,丟擲兩個常見面試問題:1。G1的回收原理是什麼?為什麼G1比傳統的GC回收效能好?2。為什麼G1如此完美仍然會有ZGC?簡單的回顧下CMS垃圾回收機制,下面介紹了一個極端的場景(而且是經常發生的) 在發生Minor GC時,由於Survivor區已經放不下了,多出的物件只能提升(Promotion)到老年代。但是此時老年代因為空間碎片的緣故,會發生Concurrent mode failure的錯誤。這個時候,就需要降級為Serial Old垃圾回收器進行收集。這就是比concurrent mode failure 更加嚴重的promotion failed的問題。一個簡單的Minor,竟然能演化成耗時最長的Full GC。最要命的是,這個停頓時間是不可預知的。有沒有一種方法,能夠首先定義一個停頓時間,然後反向推算收集內容呢?就像是領導在年初制定KPI一樣,分配的任務多就多幹些,任務少就少乾點。類似需要徒步一段很長的路,然後在路中有多個里程碑,到達一個後可以休息一會。G1的思路說起來類似,它不要求每次都把垃圾清理得乾乾淨淨,只是努力做它認為對的事情。我們要求G1,在任意1秒的時間內,停頓不得超過10ms,這就是在給它制定KPI。G1會盡量達成這個目標,它能夠推算出本次要收集的大體區域,以增量的方式完成收集。這也是使用G1垃圾回收器不得不設定的一個引數:-XX:MaxGCPauseMilis=10

簡介

G1(Garbage First)垃圾收集器是當今垃圾回收技術最前沿的成果之一。早在JDK7就已加入JVM的收集器大家庭中,成為HotSpot重點發展的垃圾回收技術。同優秀的CMS垃圾回收器一樣,G1也是關注最小時延的垃圾回收器,也同樣適合大尺寸堆記憶體的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1最大的特點是引入分割槽的思路,弱化了分代的概念,合理利用垃圾收集各個週期的資源,解決了其他收集器甚至CMS的眾多缺陷。

為解決CMS演算法產生空間碎片和其它一系列的問題缺陷,HotSpot提供了另外一種垃圾回收策略,G1(Garbage First)演算法,透過引數-XX:+UseG1GC來啟用,該演算法在JDK 7u4版本被正式推出,官網對此描述如下:

The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories。 It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput。 The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases。 The G1 collector is designed for applications that:

•Can operate concurrently with applications threads like the CMS collector。•Compact free space without lengthy GC induced pause times。•Need more predictable GC pause durations。•Do not want to sacrifice a lot of throughput performance。•Do not require a much larger Java heap。

官網的上述翻譯如下:

G1垃圾收集演算法主要應用在多CPU大記憶體的服務中,在滿足高吞吐量的同時,儘可能的滿足垃圾回收時的暫停時間,該設計主要針對如下應用場景:

•垃圾收集執行緒和應用執行緒併發執行,和CMS一樣•空閒記憶體壓縮時避免冗長的暫停時間•應用需要更多可預測的GC暫停時間•不希望犧牲太多的吞吐效能•不需要很大的Java堆 (翻譯得有點虛,多大才算大?)

為什麼叫G1?

G1的目標是用來幹掉CMS的,它同樣是一款軟實時垃圾回收器。相比CMS,G1的使用更加人性化。比如,CMS垃圾回收器的相關引數有72個,而G1的引數只有26個。G1的全稱是GarbageFirst GC,為了達成上面制定的KPI,它和前面介紹的垃圾回收器,在堆的劃分上有一些不同。其它的回收器,都是對某個年代的整體收集,收集時間上自然不好控制。G1把堆切成了很多份,把每一份當作一個小目標,每一份收集時間自然是很好控制的。那麼題又來了:G1有年輕代和老年代區分嗎?

G1垃圾收集器概述

如圖所示,G1也是有Eden區和Survivor區的概念的,只不過它們在記憶體上不是連續的,而是由一小份一小份組成的。這一小份區域的大小是固定的,名字叫做小堆區(Region)。小堆區可以是Eden也可以是Survivor區,還可以是Old區,所以G1的年輕代和老年代的概念都是邏輯上的。每一塊Region,大小都是一致的,它的數值在1M-32M位元組之間的一個2的冪值數。但假如我的物件太大,一個Region放不下怎們辦?注意圖中有一塊很大的黃色區域,叫Humongous Region,大小超過Region 50%的物件,將會在這裡分配。Region的大小,可以透過引數進行設定:-XX:G1HeapRegionSize=M 那麼回收的時候,到底回收那些小堆區呢?是隨機的嗎?當然不是,事實上,垃圾最多的小堆區,會被優先回收,這也是G1名字的由來。

堆記憶體結構

CMS

以往的垃圾回收演算法,如CMS,使用的堆記憶體結構如下:

G1垃圾收集器概述

1。新生代:eden space + 2個survivor2。老年代:old space3。持久代:1。8之前的perm space4。元空間:1。8之後的metaspace

以上這些space必須是地址連續的空間。

G1

在G1演算法中,採用了另外一種完全不同的方式組織堆記憶體,堆記憶體被劃分為多個大小相等的記憶體塊(Region),每個Region是邏輯連續的一段記憶體,結構如下:

G1垃圾收集器概述

每個Region被標記了E、S、O和H,說明每個Region在執行時都充當了一種角色,其中H是以往演算法中沒有的,它代表Humongous,這表示這些Region儲存的是巨型物件(humongous object,H-obj),當新建物件大小超過Region大小一半時,直接在新的一個或多個連續Region中分配,並標記為H。

Region

堆記憶體中一個Region的大小可以透過-XX:G1HeapRegionSize引數指定,大小區間只能是1M、2M、4M、8M、16M和32M,總之是2的冪次方,如果G1HeapRegionSize為預設值,則在堆初始化時計算Region的實際大小,具體實現如下:

G1垃圾收集器概述

預設把堆記憶體按照2048份均分,最後得到一個合理的大小。

GC模式

G1中提供了三種模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的條件下被觸發。

young gc

發生在年輕代的GC演算法,一般物件(除了巨型物件)都是在eden region中分配記憶體,當所有eden region被耗盡無法申請記憶體時,就會觸發一次young gc,這種觸發機制和之前的young gc差不多,執行完一次young gc,活躍物件會被複製到survivor region或者晉升到old region中,空閒的region會被放入空閒列表中,等待下次被使用。

G1垃圾收集器概述

mixed gc

當越來越多的物件晉升到老年代old region時,為了避免堆記憶體被耗盡,虛擬機器會觸發一個混合的垃圾收集器,即mixed gc,該演算法並不是一個old gc,除了回收整個young region,還會回收一部分的old region,這裡需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些old region進行收集,從而可以對垃圾回收的耗時時間進行控制。

那麼mixed gc什麼時候被觸發?

先回顧一下cms的觸發機制,如果添加了以下引數:

-XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly

當老年代的使用率達到80%時,就會觸發一次cms gc。相對的,mixed gc中也有一個閾值引數 -XX:InitiatingHeapOccupancyPercent,當老年代大小佔整個堆大小百分比達到該閾值時,會觸發一次mixed gc。

mixed gc的執行過程有點類似cms,主要分為以下幾個步驟:

1。initial mark: 初始標記過程,整個過程STW,標記了從GC Root可達的物件2。concurrent marking: 併發標記過程,整個過程gc collector執行緒與應用執行緒可以並行執行,標記出GC Root可達物件衍生出去的存活物件,並收集各個Region的存活物件資訊3。remark: 最終標記過程,整個過程STW,標記出那些在併發標記過程中遺漏的,或者內部引用發生變化的物件4。clean up: 垃圾清除過程,如果發現一個Region中沒有存活物件,則把該Region加入到空閒列表中

full gc

如果物件記憶體分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發一次full gc,G1的full gc演算法就是單執行緒執行的serial old gc,會導致異常長時間的暫停時間,需要進行不斷的調優,儘可能的避免full gc。