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)
    重映射所做的就是修正整個堆中指向重分配集中舊對象的所有引用。
    併發重映射並不是一個必須要“迫切”去完成的任務,但是當所有指針都被修正之後,原來記錄新舊對象關係的轉發表就可以釋放掉了。

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