1. 程式人生 > 實用技巧 >Java進階專題(十一) 想理解JVM看了這篇文章,就知道了!(中)

Java進階專題(十一) 想理解JVM看了這篇文章,就知道了!(中)

前言

​ 上次講解了JVM記憶體相關知識,今天繼續JVM專題。

JVM垃圾回收演算法

什麼是垃圾回收

​ 程式的執行必然需要申請記憶體資源,無效的物件資源如果不及時處理就會一直佔有記憶體資源,最終將導致記憶體溢位,所以對記憶體資源的管理是非常重要了。

什麼樣的物件需要回收

引用計數法

​ 引用計數是歷史最悠久的一種演算法,最早George E. Collins在1960的時候首次提出,50年後的今天,該演算法依然被很多程式語言使用。

原理

​ 假設有一個物件A,任何一個物件對A的引用,那麼物件A的引用計數器+1,當引用失效時,物件A的引用計數器
就-1,如果物件A的計數器的值為0,就說明物件A沒有引用了,可以被回收。

優缺點

優點:
實時性較高,無需等到記憶體不夠的時候,才開始回收,執行時根據物件的計數器是否為0,就可以直接回收。
在垃圾回收過程中,應用無需掛起。如果申請記憶體時,記憶體不足,則立刻報outofmember 錯誤。
區域性,更新物件的計數器時,只是影響到該物件,不會掃描全部物件。
缺點:
每次物件被引用時,都需要去更新計數器,有一點時間開銷。
浪費CPU資源,即使記憶體夠用,仍然在執行時進行計數器的統計。
無法解決迴圈引用問題。(最大的缺點)

class TestA{
 public TestB b;
}

class TestB{
 public TestA a;
}

//雖然a和b都為null,但是由於a和b存在迴圈引用,這樣a和b永遠都不會被回收。
public class Main{
   public static void main(String[] args){
       A a = new A();
       B b = new B();
       a.b=b;
       b.a=a;
       a = null; //釋放資源
       b = null; //釋放資源
   }
}

可達性分析演算法

​ 通過一系列稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據引用關係向下搜尋,搜尋過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個物件到GC Roots間沒有任何引用鏈相連,就說明從GC Roots到這個物件不可達時,則證明此物件是不可能再被使用的,就是可以回收的物件。

在JVM虛擬機器中,可作為GC Roots的物件包括以下幾種:

  • 在虛擬機器棧(棧幀中的本地變量表)中引用的物件,譬如各個執行緒被呼叫的方法堆疊中使用到的引數、區域性變數、臨時變數等。
  • 在方法區中類靜態屬性引用的物件,譬如Java類的引用型別靜態變數。
  • 在方法區中常量引用的物件,譬如字串常量池(String T able)裡的引用。
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的物件。
  • Java虛擬機器內部的引用,如基本資料型別對應的Class物件,一些常駐的異常物件(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類載入器。
  • 所有被同步鎖(synchronized關鍵字)持有的物件。反映Java虛擬機器內部情況的JMXBean、JVMTI中註冊的回撥、原生代碼快取等。

物件的引用

在java中,物件的引用分為:強引用(Strongly Re-ference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種。

  • 強引用
    在程式程式碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關係。
    無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。
  • 軟引用
    用來描述一些還有用,但非必須的物件。
    只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。
  • 弱引用
    用來描述那些非必須物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能存活到下一次垃圾收集發生為止。
    當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。
  • 虛引用
    最弱的一種引用關係,一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。
    為一個物件設定虛引用關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知。

如何回收垃圾

​ 自動化的管理記憶體資源,垃圾回收機制必須要有一套演算法來進行計算,哪些是有效的物件,哪些是無效的物件,對於無效的物件就要進行回收處理。
​ 常見的垃圾回收演算法有:標記清除法、標記壓縮法、複製演算法、分代演算法等。

標記清除演算法

標記清除演算法,是將垃圾回收分為2個階段,分別是標記和清除。
標記:從根節點開始標記引用的物件。
清除:未被標記引用的物件就是垃圾物件,可以被清理。

標記清除法可以說是最基礎的收集演算法,因為後續的收集演算法大多都是以標記-清除演算法為基礎,對其缺點進行改進而得到的。

標記前

標記後

回收後

優缺點

可以看到,標記清除演算法解決了引用計數演算法中的迴圈引用的問題,沒有從root節點引用的物件都會被回收。

同樣,標記清除演算法也是有缺點的:
效率較低,標記和清除兩個動作都需要遍歷所有的物件,並且在GC時,需要停止應用程式,對於互動性要求比較高的應用而言這個體驗是非常差的。
通過標記清除演算法清理出來的記憶體,碎片化較為嚴重,因為被回收的物件可能存在於記憶體的各個角落,所以清理出來的記憶體是不連貫的。

標記壓縮演算法

​ 標記壓縮演算法是在標記清除演算法的基礎之上,做了優化改進的演算法。和標記清除演算法一樣,也是從根節點開始,對物件的引用進行標記,在清理階段,並不是簡單的清理未標記的物件,而是將存活的物件壓縮到記憶體的一端,然後清理邊界以外的垃圾,從而解決了碎片化的問題。

原理

優缺點

​ 優缺點同標記清除演算法,解決了標記清除演算法的碎片化的問題,同時,標記壓縮演算法多了一步,物件移動記憶體位置的步驟,其效率也有有一定的影響。

複製演算法

複製演算法的核心就是,將原有的記憶體空間一分為二,每次只用其中的一塊,在垃圾回收時,將正在使用的物件複製
到另一個記憶體空間中,然後將該記憶體空間清空,交換兩個記憶體的角色,完成垃圾的回收。
如果記憶體中的垃圾物件較多,需要複製的物件就較少,這種情況下適合使用該方式並且效率比較高,反之,則不適合。

原理

優缺點

優點:
在垃圾物件多的情況下,效率較高
清理後,記憶體無碎片
缺點:
在垃圾物件少的情況下,不適用,如:老年代記憶體
分配的2塊記憶體空間,在同一個時刻,只能使用一半,記憶體使用率較低

分代演算法

​ 在堆記憶體中,有些物件短暫存活有些則是長久存活,所以需要將堆記憶體進行分代,將短暫存活的物件放到一起,進行高頻率的回收,長久存活的物件集中放到一起,進行低頻率的回收,這樣才能夠更加合理的利系統資源。分代演算法其實就是這樣的,根據回收物件的特點進行選擇,在jvm中,年輕代適合使用複製演算法,老年代適合使用標記清除或標記壓縮演算法。

垃圾回收的相關概念:
部分收集(Partial GC)
新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。
混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。
整堆收集(Full GC)

垃圾收集器

​ 前面我們講了垃圾回收的演算法,還需要有具體的實現,在jvm中,實現了多種垃圾收集器,包括:序列垃圾收集
器、並行垃圾收集器、CMS(併發)垃圾收集器、G1垃圾收集器和JDK11中的ZGC(超牛逼)接下來,我們一個個的瞭解學習。

序列垃圾收集器

​ 序列垃圾收集器,是指使用單執行緒進行垃圾回收,垃圾回收時,只有一個執行緒在工作,並且java應用中的所有執行緒都要暫停,等待垃圾回收的完成。這種現象稱之為STW(Stop-The-World)。
​ 對於互動性較強的應用而言,這種垃圾收集器是不能夠接受的。一般在Javaweb應用中是不會採用該收集器的。

並行垃圾收集器

​ 並行垃圾收集器在序列垃圾收集器的基礎之上做了改進,將單執行緒改為了多執行緒進行垃圾回收,這樣可以縮短垃圾回收的時間。(這裡是指,並行能力較強的機器)
​ 當然了,並行垃圾收集器在收集的過程中也會暫停應用程式,這個和序列垃圾回收器是一樣的,只是並行執行,速度更快些,暫停的時間更短一些。

ParNew垃圾收集器

​ ParNew垃圾收集器是工作在年輕代上的,只是將序列的垃圾收集器改為了並行。
​ 通過-XX:+UseParNewGC引數設定年輕代使用ParNew回收器,老年代使用的依然是序列收集器。

測試

#引數
-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m
#打印出的資訊
[GC (Allocation Failure) [ParNew: 4416K->512K(4928K), 0.0032106 secs] 4416K->1988K(15872K), 0.0032697 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

由以上資訊可以看出, ParNew: 使用的是ParNew收集器。其他資訊和序列收集器一致。

ParallelGC垃圾收集器

​ ParallelGC收集器工作機制和ParNewGC收集器一樣,只是在此基礎之上,新增了兩個和系統吞吐量相關的引數,使得其使用起來更加的靈活和高效。
相關引數如下:
​ -XX:+UseParallelGC
​ 年輕代使用ParallelGC垃圾回收器,老年代使用序列回收器。
​ -XX:+UseParallelOldGC
​ 年輕代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。
​ -XX:MaxGCPauseMillis
​ 設定最大的垃圾收集時的停頓時間,單位為毫秒
​ 需要注意的時,ParallelGC為了達到設定的停頓時間,可能會調整堆大小或其他的引數,如果堆的大小
設定的較小,就會導致GC工作變得很頻繁,反而可能會影響到效能。
該引數使用需謹慎。
​ -XX:GCTimeRatio
​ 設定垃圾回收時間佔程式執行時間的百分比,公式為1/(1+n)。
​ 它的值為0~100之間的數字,預設值為99,也就是垃圾回收時間不能超過1%
​ -XX:UseAdaptiveSizePolicy
​ 自適應GC模式,垃圾回收器將自動調整年輕代、老年代等引數,達到吞吐量、堆大小、停頓時間之間的
平衡。
​ 一般用於,手動調整引數比較困難的場景,讓收集器自動進行調整。

測試:

#引數
-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xms16m -Xmx16m
#列印的資訊
[GC (Allocation Failure) [PSYoungGen: 4096K->480K(4608K)] 4096K->1840K(15872K),
0.0034307 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Ergonomics) [PSYoungGen: 505K->0K(4608K)] [ParOldGen: 10332K->10751K(11264K)]
10837K->10751K(15872K), [Metaspace: 3491K->3491K(1056768K)], 0.0793622 secs] [Times:
user=0.13 sys=0.00, real=0.08 secs]

有以上資訊可以看出,年輕代和老年代都使用了ParallelGC垃圾回收器。

CMS垃圾收集器

​ CMS全稱Concurrent Mark Sweep,是一款併發的、使用標記-清除演算法的垃圾回收器,該回收器是針對老年代垃圾回收的,通過引數-XX:+UseConcMarkSweepGC進行設定

  • 初始化標記(CMS-initial-mark) ,標記root,會導致stw;
  • 併發標記(CMS-concurrent-mark),與使用者執行緒同時執行;
  • 預清理(CMS-concurrent-preclean),與使用者執行緒同時執行;
  • 重新標記(CMS-remark) ,會導致stw;
  • 併發清除(CMS-concurrent-sweep),與使用者執行緒同時執行;
  • 調整堆大小,設定CMS在清理之後進行記憶體壓縮,目的是清理記憶體中的碎片;
  • 併發重置狀態等待下次CMS的觸發(CMS-concurrent-reset),與使用者執行緒同時執行;

測試

#設定啟動引數
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m
#執行日誌
[GC (Allocation Failure) [ParNew: 4926K->512K(4928K), 0.0041843 secs] 9424K->6736K(15872K), 0.0042168 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第一步,初始標記
[GC (CMS Initial Mark) [1 CMS-initial-mark: 6224K(10944K)] 6824K(15872K), 0.0004209
secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第二步,併發標記
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第三步,預處理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第四步,重新標記
[GC (CMS Final Remark) [YG occupancy: 1657 K (4928 K)][Rescan (parallel) , 0.0005811
secs][weak refs processing, 0.0000136 secs][class unloading, 0.0003671 secs][scrub
symbol table, 0.0006813 secs][scrub string table, 0.0001216 secs][1 CMS-remark:
6224K(10944K)] 7881K(15872K), 0.0018324 secs] [Times: user=0.00 sys=0.00, real=0.00
secs]
#第五步,併發清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
#第六步,重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

由以上日誌資訊,可以看出CMS執行的過程。

G1垃圾收集器

G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方計劃在jdk9中將G1變成預設的垃圾收集器,
以替代CMS。
G1的設計原則就是簡化JVM效能調優,開發人員只需要簡單的三步即可完成調優:

  1. 第一步,開啟G1垃圾收集器
  2. 第二步,設定堆的最大記憶體
  3. 第三步,設定最大的停頓時間。

G1中提供了三種模式垃圾回收模式,Young GC、Mixed GC 和Full GC,在不同的條件下被觸發。

原理

​ G1垃圾收集器相對比其他收集器而言,最大的區別在於它取消了年輕代、老年代的物理劃分,取而代之的是將堆劃分為若干個區域(Region),這些區域中包含了有邏輯上的年輕代、老年代區域。
​ 這樣做的好處就是,我們再也不用單獨的空間對每個代進行設定了,不用擔心每個代記憶體是否足夠。

​ 在G1劃分的區域中,年輕代的垃圾收集依然採用暫停所有應用執行緒的方式,將存活物件拷貝到老年代或者Survivor空間,G1收集器通過將物件從一個區域複製到另外一個區域,完成了清理工作。
​ 這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms記憶體碎片問題的存在了。

在G1中,有一種特殊的區域,叫Humongous區域。

  • 如果一個物件佔用的空間超過了分割槽容量50%以上,G1收集器就認為這是一個巨型物件。
  • 這些巨型物件,預設直接會被分配在老年代,但是如果它是一個短期存在的巨型物件,就會對垃圾收集器造成負面影響。
  • 為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型物件。如果一個H區裝不下一個巨型物件,那麼G1會尋找連續的H分割槽來儲存。為了能找到連續的H區,有時候不得不啟動Full GC。

Young GC

Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。
Eden空間的資料移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分資料會直接晉升到年老代空間。
Survivor區的資料移動到新的Survivor區中,也有部分資料晉升到老年代空間中。最終Eden空間的資料為空,GC停止工作,應用執行緒繼續執行。

Remembered Set(已記憶集合)

在GC年輕代的物件時,我們如何找到年輕代中物件的根物件呢?
根物件可能是在年輕代中,也可以在老年代中,那麼老年代中的所有物件都是根麼?
如果全量掃描老年代,那麼這樣掃描下來會耗費大量的時間。
於是,G1引進了RSet的概念。它的全稱是Remembered Set,其作用是跟蹤指向某個堆內的物件引用。

​ 每個Region初始化時,會初始化一個RSet,該集合用來記錄並跟蹤其它Region指向該Region中物件的引用,每個Region預設按照512Kb劃分成多個Card,所以RSet需要記錄的東西應該是xx Region的xx Card。

Mixed GC

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

​ MixedGC什麼時候觸發? 由引數-XX:InitiatingHeapOccupancyPercent=n 決定。預設:45%,該引數的意思是:當老年代大小佔整個堆大小百分比達到該閥值時觸發。

它的GC步驟分2步:

  1. 全域性併發標記(global concurrent marking)
  2. 拷貝存活物件(evacuation)

全域性併發標記

全域性併發標記,執行過程分為五個步驟:
初始標記(initial mark,STW)
標記從根節點直接可達的物件,這個階段會執行一次年輕代GC,會產生全域性停頓。
根區域掃描(root region scan)
G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的物件。
該階段與應用程式(非STW)同時執行,並且只有完成該階段後,才能開始下一次STW 年輕代垃圾回收。
併發標記(Concurrent Marking)
G1 GC 在整個堆中查詢可訪問的(存活的)物件。該階段與應用程式同時執行,可以被STW 年輕代垃圾回收中斷。
重新標記(Remark,STW)
該階段是STW 回收,因為程式在執行,針對上一次的標記進行修正。
清除垃圾(Cleanup,STW)
清點和重置標記狀態,該階段會STW,這個階段並不會實際上去做垃圾的收集,等待evacuation階段來回收。

拷貝存活物件

​ Evacuation階段是全暫停的。該階段把一部分Region裡的活物件拷貝到另一部分Region中,從而實現垃圾的回收清理。

G1收集器相關引數

  • -XX:+UseG1GC
    使用G1 垃圾收集器
  • -XX:MaxGCPauseMillis
    設定期望達到的最大GC停頓時間指標(會盡力實現,但不保證達到),預設值是200 毫秒。
  • -XX:G1HeapRegionSize=n
    設定的G1 區域的大小。值是2 的冪,範圍是1 MB 到32 MB 之間。目標是根據最小的Java 堆大小劃分出約2048 個區域。預設是堆記憶體的1/2000。
  • -XX:ParallelGCThreads=n
    設定STW 工作執行緒數的值。將n 的值設定為邏輯處理器的數量。n 的值與邏輯處理器的數量相同,最多為8。
  • -XX:ConcGCThreads=n
    設定並行標記的執行緒數。將n 設定為並行垃圾回收執行緒數(ParallelGCThreads) 的1/4 左右。
  • -XX:InitiatingHeapOccupancyPercent=n
    設定觸發Mixed GC 的Java 堆佔用率閾值。預設佔用率是整個Java 堆的45%

測試

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xmx256m
#日誌
[GC pause (G1 Evacuation Pause) (young), 0.0044882 secs]
  [Parallel Time: 3.7 ms, GC Workers: 3]
     [GC Worker Start (ms): Min: 14763.7, Avg: 14763.8, Max: 14763.8, Diff: 0.1]
     #掃描根節點
     [Ext Root Scanning (ms): Min: 0.2, Avg: 0.3, Max: 0.3, Diff: 0.1, Sum: 0.8]
     #更新RS區域所消耗的時間
     [Update RS (ms): Min: 1.8, Avg: 1.9, Max: 1.9, Diff: 0.2, Sum: 5.6]
        [Processed Buffers: Min: 1, Avg: 1.7, Max: 3, Diff: 2, Sum: 5]
     [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
     [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
     #物件拷貝
     [Object Copy (ms): Min: 1.1, Avg: 1.2, Max: 1.3, Diff: 0.2, Sum: 3.6]
     [Termination (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.2]
        [Termination Attempts: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 3]
     [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
     [GC Worker Total (ms): Min: 3.4, Avg: 3.4, Max: 3.5, Diff: 0.1, Sum: 10.3]
     [GC Worker End (ms): Min: 14767.2, Avg: 14767.2, Max: 14767.3, Diff: 0.1]
  [Code Root Fixup: 0.0 ms]
  [Code Root Purge: 0.0 ms]
  [Clear CT: 0.0 ms] #清空CardTable
  [Other: 0.7 ms]
     [Choose CSet: 0.0 ms] #選取CSet
     [Ref Proc: 0.5 ms] #弱引用、軟引用的處理耗時
     [Ref Enq: 0.0 ms] #弱引用、軟引用的入隊耗時
     [Redirty Cards: 0.0 ms]
     [Humongous Register: 0.0 ms] #大物件區域註冊耗時
     [Humongous Reclaim: 0.0 ms] #大物件區域回收耗時
     [Free CSet: 0.0 ms]
  [Eden: 7168.0K(7168.0K)->0.0B(13.0M) Survivors: 2048.0K->2048.0K Heap:
55.5M(192.0M)->48.5M(192.0M)] #年輕代的大小統計
[Times: user=0.00 sys=0.00, real=0.00 secs] 

對於G1垃圾收集器優化建議

  • 年輕代大小
    避免使用-Xmn 選項或-XX:NewRatio 等其他相關選項顯式設定年輕代大小。固定年輕代的大小會覆蓋暫停時間目標。
  • 暫停時間目標不要太過嚴苛
    G1 GC 的吞吐量目標是90% 的應用程式時間和10%的垃圾回收時間。評估G1 GC 的吞吐量時,暫停時間目標不要太嚴苛。目標太過嚴苛表示您願意承受更多的垃圾回收開銷,而這會直接影響到吞吐量。

ZGC

​ ZGC是一款在JDK 11中新加入的具有實驗性質的低延遲垃圾收集器,是由Oracle公司研發的。ZGC的目標是希望在儘可能對吞吐量影響不太大的前提下,實現在任意堆記憶體大小下都可以把垃圾收集的停頓時間限制在10毫秒以內的低延遲。

記憶體佈局

​ ZGC的記憶體佈局與G1一樣,也採用基於Region的堆記憶體佈局,但不同的是,ZGC的Page(ZGC中稱之為頁面,道理和Region一樣)具有動態性——動態建立和銷燬,以及動態的區域容量大小。在x64硬體平臺下,ZGC的Pag可以具有大、中、小三類容量:

  • 小型頁面(Small Page):容量固定為2MB,用於放置小於256KB的小物件。

  • 中型頁面(Medium Page):容量固定為32MB,用於放置大於等於256KB但小於4MB的物件。

  • 大型頁面(Large Page):容量不固定,可以動態變化,但必須為2MB的整數倍,用於放置4MB或以上的大物件。

    每個大頁面中只會存放一個大物件,這也預示著雖然名字叫作“大型Page”,但它的實際容量完全有可能小於中型Page,最小容量可低至4MB。
    大型Page在ZGC的實現中是不會被重分配(重分配是ZGC的一種處理動作)的,因為複製一個大物件的代價非常高昂。

效能表現

​ 在效能方面,儘管目前還處於實驗狀態,還沒有完成所有特性,穩定性打磨和效能調優也仍在進行,但即使是這種狀態下的ZGC,其效能表現已經相當亮眼,從官方給出的測試結果來看,用“令人震驚的、革命性的ZGC”來形容都不為過。

​ ZGC與Parallel Scavenge、G1三款收集器通過SPECjbb 2015(java伺服器業務測試工具)的測試結果。在ZGC的“弱項”吞吐量方面,以低延遲為首要目標的ZGC已經達到了以高吞吐量為目標Parallel Scavenge的99%,直接超越了G1。如果將吞吐量測試設定為面向SLA(Service Level Agreements)應用的“Critical Throughput” (要求最大延遲不超過某個設定值(10毫秒到100毫秒)下測得的吞吐量)的話,ZGC的表現甚至還反超ParallelScavenge收集器。

​ ZGC的強項停頓時間測試上,它就毫不留情地與Parallel Scavenge、G1拉開了兩個數量級的差距。不論是平均停頓,還是95%停頓、99%停頓、99.9%停頓,抑或是最大停頓時間,ZGC均能毫不費勁地控制在十毫秒之內,以至於把它和另外兩款停頓數百近千毫秒的收集器放到一起對比,就幾乎顯示不了ZGC的柱狀條(圖a),必須把結果的縱座標從線性尺度調整成對數尺度(圖b,縱座標軸的尺度是對數增長的)才能觀察到ZGC的測試結果。

使用

​ 在jdk11下,只能在linux 64位的平臺上使用ZGC,如果想要在Windows下使用ZGC就需要升級jdk到14了。

cd /usr/local/src/
#上傳jdk-11.0.7_linux-x64_bin.tar.gz
tar -xvf jdk-11.0.7_linux-x64_bin.tar.gz
#如果本身已經安裝openjdk的話,先刪除
java –version
rpm -qa | grep java
rpm -e --nodeps java-xxxx-openjdk-xxxx.x86_64
vim /etc/profile
#寫入如下內容
#set java environment
JAVA_HOME=/usr/local/src/jdk-11.0.7
CLASSPATH=.:$JAVA_HOME/lib.tools.jar
PATH=$JAVA_HOME/bin:$PATH
export JAVA_HOME CLASSPATH PATH
#生效
source /etc/profile
#執行命令
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx256m -Xlog:gc*=info TestGC
#引數說明
-XX:+UnlockExperimentalVMOptions 解鎖實驗引數
-XX:+UseZGC  啟用ZGC垃圾收集器
-Xmx256m  設定最大記憶體
-Xlog:gc*=info  設定列印gc日誌資訊
#設定並行的執行緒數,一般預設即可
-XX:ConcGCThreads  
#執行日誌
[1.141s][info][gc,start    ] GC(2) Garbage Collection (Warmup)
[1.141s][info][gc,phases   ] GC(2) Pause Mark Start 0.190ms
[1.143s][info][gc,phases   ] GC(2) Concurrent Mark 1.837ms
[1.143s][info][gc,phases   ] GC(2) Pause Mark End 0.136ms
[1.144s][info][gc,phases   ] GC(2) Concurrent Process Non-Strong References 0.308ms
[1.144s][info][gc,phases   ] GC(2) Concurrent Reset Relocation Set 0.001ms
[1.144s][info][gc,phases   ] GC(2) Concurrent Destroy Detached Pages 0.000ms
[1.145s][info][gc,phases   ] GC(2) Concurrent Select Relocation Set 1.219ms
[1.145s][info][gc,phases   ] GC(2) Concurrent Prepare Relocation Set 0.009ms
[1.145s][info][gc,phases   ] GC(2) Pause Relocate Start 0.230ms
[1.146s][info][gc,phases   ] GC(2) Concurrent Relocate 0.853ms
[1.146s][info][gc,load     ] GC(2) Load: 0.00/0.02/0.05
[1.146s][info][gc,mmu      ] GC(2) MMU: 2ms/78.1%, 5ms/88.9%, 10ms/93.4%, 20ms/96.7%,
50ms/98.7%, 100ms/99.0%
[1.146s][info][gc,marking  ] GC(2) Mark: 1 stripe(s), 1 proactive flush(es), 1
terminate flush(es), 0 completion(s), 0 continuation(s)
[1.146s][info][gc,reloc    ] GC(2) Relocation: Successful, 1M relocated
[1.146s][info][gc,nmethod  ] GC(2) NMethods: 59 registered, 0 unregistered
[1.146s][info][gc,metaspace] GC(2) Metaspace: 4M used, 4M capacity, 5M committed, 8M
reserved
[1.146s][info][gc,ref      ] GC(2) Soft: 131 encountered, 0 discovered, 0 enqueued
[1.146s][info][gc,ref      ] GC(2) Weak: 222 encountered, 215 discovered, 0 enqueued
[1.146s][info][gc,ref      ] GC(2) Final: 0 encountered, 0 discovered, 0 enqueued
[1.146s][info][gc,ref      ] GC(2) Phantom: 1 encountered, 1 discovered, 0 enqueued
[1.146s][info][gc,heap     ] GC(2)                Mark Start          Mark End       
Relocate Start      Relocate End           High               Low
[1.146s][info][gc,heap     ] GC(2)  Capacity:      114M (45%)         114M (45%)      
  114M (45%)         114M (45%)         114M (45%)         114M (45%)
[1.146s][info][gc,heap     ] GC(2)   Reserve:       36M (14%)          36M (14%)      
   36M (14%)          36M (14%)          36M (14%)          36M (14%)
[1.146s][info][gc,heap     ] GC(2)      Free:      142M (55%)         142M (55%)      
  184M (72%)         184M (72%)         184M (72%)         142M (55%)
[1.146s][info][gc,heap     ] GC(2)      Used:       78M (30%)          78M (30%)      
   36M (14%)          36M (14%)          78M (30%)          36M (14%)
[1.146s][info][gc,heap     ] GC(2)      Live:         -                 1M (1%)        
   1M (1%)            1M (1%)             -                  -[1.146s][info][gc,heap     ] GC(2) Allocated:         -                 0M (0%)        
   0M (0%)            4M (2%)             -                  -[1.146s][info][gc,heap     ] GC(2)   Garbage:         -                76M (30%)      
   34M (14%)          34M (14%)            -                  -[1.146s][info][gc,heap     ] GC(2) Reclaimed:         -                  -            
   42M (16%)          42M (16%)            -                  -[1.146s][info][gc          ] GC(2) Garbage Collection (Warmup) 78M(30%)->36M(14%)

染色指標技術

ZGC為了實現目標,新增了染色指標技術。

​ 染色指標是一種直接將少量額外的資訊儲存在指標上的技術,在64位系統中,理論可以訪問的記憶體高達16EB(2的64次冪)位元組。實際上,64位的Linux則分別支援47位(128TB)的程序虛擬地址空間和46位(64TB)的實體地址空間,64位的Windows系統甚至只支援44位(16TB)的實體地址空間。
​ Linux下64位指標的高18位不能用來定址,但剩餘的46位指標所能支援的64TB記憶體在今天仍然能夠充分滿足大型伺服器的需要。
​ ZGC的染色指標技術使用上了這剩下的46位指標寬度,將其高4位提取出來儲存四個標誌資訊。通過這些標誌位,虛擬機器可以直接從指標中看到其引用物件的三色標記狀態、是否進入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到。
​ 由於這些標誌位進一步壓縮了原本就只有46位的地址空間,也直接導致ZGC能夠管理的記憶體不可以超過4TB(2的42次冪)。

染色指標的好處

  • 染色指標可以使得一旦某個Region的存活物件被移走之後,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正後才能清理。
  • 染色指標可以大幅減少在垃圾收集過程中記憶體屏障的使用數量。
    一般寫屏障的目的通常是為了記錄物件引用的變動情況,如果將這些資訊直接維護在指標中,顯然就可以省去一些專門的記錄操作。
    ZGC都並未使用任何寫屏障,只使用了讀屏障。
    染色指標可以作為一種可擴充套件的儲存結構用來記錄更多與物件標記、重定位過程相關的資料,以便日後進一步提高效能。

工作過程

​ ZGC的運作過程大致可劃分為四個大的階段,這四個階段都是可以併發執行的。僅在Mark Start、Initial Mark 階段中會存在短暫的STW。

  • 併發標記(Concurrent Mark)
    與G1一樣,併發標記是遍歷物件圖做可達性分析的階段,前後也要經過初始標記、最終標記的短暫停頓。

    ZGC的標記是在指標上而不是在物件上進行的,標記階段會更新染色指標中的Marked 0、Marked 1標誌位。

  • 併發預備重分配(Concurrent Prepare for Relocate)
    這個階段需要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。
    ZGC每次回收都會掃描所有的Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。
    ZGC的重分配集只是決定了裡面的存活物件會被重新複製到其他的Region中,裡面的Region會被釋放。

  • 併發重分配(Concurrent Relocate)

    重分配是ZGC執行過程中的核心階段,這個過程要把重分配集中的存活物件複製到新的Region上,併為重分配集中的每個Region維護一個轉發表(Forward T able),記錄從舊物件到新物件的轉向關係。
    由於使用了染色指標的支援,ZGC收集器能僅從引用上就明確得知一個物件是否處於重分配集之中,如果使用者執行緒此時併發訪問了位於重分配集中的物件,這次訪問將會被預置的記憶體屏障所截獲,然後立即根據Region上的轉發表記錄將訪問轉發到新複製的物件上,並同時修正更新該引用的值,使其直接指向新物件,ZGC將這種行為稱為指標的“自愈”(Self-Healing)能力。

  • 併發重對映(Concurrent Remap)
    重對映所做的就是修正整個堆中指向重分配集中舊物件的所有引用。
    併發重對映並不是一個必須要“迫切”去完成的任務,但是當所有指標都被修正之後,原來記錄新舊物件關係的轉發表就可以釋放掉了。