欧美free性护士vide0shd,老熟女,一区二区三区,久久久久夜夜夜精品国产,久久久久久综合网天天,欧美成人护士h版

首頁綜合 正文
目錄

柚子快報激活碼778899分享:JVM垃圾收集——垃圾收集器

柚子快報激活碼778899分享:JVM垃圾收集——垃圾收集器

http://yzkb.51969.com/

文章目錄

1、垃圾收集器的發(fā)展和分類1.1、評估垃圾收集器的性能指標1.1.1、吞吐量1.1.2、停頓時間1.1.3、吞吐量和停頓時間的比較

1.2、垃圾收集器的發(fā)展史1.3、垃圾收集器的分類1.4、查看默認的垃圾收集器

2、Serial收集器:串行回收3、ParNew收集器:并行回收4、Parallel Scaveng收集器:吞吐量優(yōu)先5、CMS收集器:低延遲5.1、CMS收集器介紹5.2、CMS的工作原理5.3、CMS收集器的參數(shù)設置5.4、JDK后續(xù)版本中CMS的變化

6、G1收集器:區(qū)域化分代式6.1、G1收集器6.2、G1收集器的特點和使用場景6.3、分區(qū)Region:化整為零6.4、G1收集器垃圾回收過程6.5、G1收集器的參數(shù)設置

7、垃圾收集器的新發(fā)展7.1、Epsilon和ZGC7.2、Shenandoah GC

8、總結

了解了垃圾收集算法有復制算法、標記–清除算法和標記–壓縮算法。此時相當于對垃圾收集的理解還處于一種理論狀態(tài),相當于只定義了接口,還沒有完成實現(xiàn)細節(jié)。本貼要講的垃圾收集器就是針對垃圾收集算法的具體實現(xiàn)。接下來我們會從垃圾收集器的發(fā)展史開始,詳細講解各種類型的垃圾收集器和其適用的應用場景。

1、垃圾收集器的發(fā)展和分類

內(nèi)存處理是編程人員容易出現(xiàn)問題的地方,忘記或者錯誤的內(nèi)存回收會導致程序或系統(tǒng)的不穩(wěn)定甚至崩潰。JVM有一套內(nèi)存的自動管理機制,Java程序員可以把絕大部分精力放在業(yè)務邏輯的實現(xiàn)上,不用過多地關心對象的內(nèi)存申請、分配、回收等問題。自動內(nèi)存管理機制是Java的招牌能力,極大地提高了開發(fā)效率,也大大降低了內(nèi)存溢出或內(nèi)存泄漏的風險。

自動內(nèi)存管理的內(nèi)存回收是靠垃圾收集器來實現(xiàn)的,垃圾收集器,英文全稱為Garbage Collector,簡稱GC。在JVM規(guī)范中,沒有對垃圾收集器做過多的規(guī)定。不同廠商、不同版本的JVM對垃圾收集器的實現(xiàn)也各有不同,隨著JDK版本的高速迭代,衍生了很多類型的垃圾收集器。

1.1、評估垃圾收集器的性能指標

沒有一款垃圾收集器能夠適用所有場合,不同的用戶需求、不同的程序運行環(huán)境和平臺對垃圾收集器的要求也各不相同,所以目前HotSpot虛擬機中是多種垃圾收集器并存的。另外,衡量一款垃圾收集器的優(yōu)劣也有多個指標,而且多個指標之間甚至互相矛盾、互相牽制,很難兩全其美。

吞吐量:運行用戶代碼的時間占總運行時間的比例??傔\行時間=程序的運行時間+內(nèi)存回收的時間。垃圾收集開銷:吞吐量的補數(shù),內(nèi)存回收所用時間與總運行時間的比例。停頓時間:執(zhí)行垃圾收集時,程序的工作線程被暫停的時間。收集頻率:垃圾收集操作發(fā)生的頻率。內(nèi)存占用:Java堆區(qū)大小設置。

其中吞吐量、停頓時間、內(nèi)存占用這三者共同構成一個“不可能三角”,即不可能同時都滿足,一款優(yōu)秀的收集器通常最多同時滿足其中的兩項。下面就吞吐量和停頓時間做個對比。

1.1.1、吞吐量

吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。比如虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

高吞吐量的應用程序往往有更長的時間基準,快速響應是不必考慮的,這種情況下,應用程序能容忍較高的單次停頓時間。如下圖所示: 圖中的垃圾回收時間是200 + 200 =400ms,CPU消耗總時間是6000ms,那么吞吐量為(6000-400)/6000 = 93.33%。

1.1.2、停頓時間

停頓時間是指一個時間段內(nèi)應用程序線程暫停,讓垃圾收集線程執(zhí)行的狀態(tài)。例如,GC期間100ms的停頓時間意味著在這100ms期間內(nèi)沒有應用程序線程是活動的。

停頓時間優(yōu)先,意味著盡可能讓單次程序停頓的時間最短。如下圖所示: 總的停頓時間是100 + 100 + 100 + 100 + 100= 500ms。雖然總的停頓時間變長了,但是每次停頓的時間都很短,這樣應用程序看起來延遲是比較低的,此時程序的吞吐量為(6000-500)/6000 =91.67%,明顯吞吐量會有所降低,但是單次停頓的時間變短了。

1.1.3、吞吐量和停頓時間的比較

高吞吐量會讓應用程序的用戶感覺只有應用程序線程在做“生產(chǎn)性”工作。直覺上,吞吐量越高程序運行越快。

停頓時間較高會讓用戶感覺延遲嚴重,不管是垃圾收集還是其他原因?qū)е乱粋€應用被掛起始終是不好的。不同類型的應用程序?qū)νnD時間的要求有很大差異,有時候甚至短暫的200ms暫停都可能打斷終端用戶體驗。因此,對于一個交互式應用程序,具有低停頓時間是非常重要的。

不幸的是,應用程序無法同時滿足高吞吐量和低停頓時間。如果選擇以吞吐量優(yōu)先,那么必然需要降低內(nèi)存回收的執(zhí)行頻率,這樣會導致垃圾收集需要更長的停頓時間來執(zhí)行內(nèi)存回收。相反的,如果選擇以低延遲優(yōu)先為原則,為了降低每次執(zhí)行內(nèi)存回收時的停頓時間,也只能頻繁地執(zhí)行內(nèi)存回收,但這又引起了新生代內(nèi)存的縮減和程序吞吐量的下降。

在垃圾收集器的發(fā)展過程中,不同的垃圾收集器也是在不斷地挑戰(zhàn)性能指標的極限,或者在盡量兼顧多個性能指標。

1.2、垃圾收集器的發(fā)展史

1998年12月8日,第二代Java平臺的企業(yè)版J2EE正式對外發(fā)布。為了配合企業(yè)級應用落地,1999年4月27日,Java程序的舞臺——Java HotSpot Virtual Machine(以下簡稱HotSpot)正式對外發(fā)布,并從這之后發(fā)布的JDK1.3版本開始,HotSpot成為Sun JDK的默認虛擬機。

1999年隨JDK1.3.1一起發(fā)布的是串行方式的Serial GC,它是第一款GC,并且這只是起點。Serial收集器是最基本、歷史最悠久的垃圾收集器,它是一個單線程收集器。而之后的ParNew垃圾收集器是Serial收集器的多線程升級版本,除了Serial收集器外,也只有它能與CMS收集器配合工作。

2002年2月26日,J2SE1.4發(fā)布。Parallel GC和Concurrent Mark Sweep(CMS)GC跟隨JDK1.4.2一起發(fā)布,并且Parallel GC在JDK6之后成為HotSpot默認GC。Parallel GC收集器看似與ParNew收集器在功能上類似,但是它們的側重點不同,Parallel Scavenge收集器關注點是吞吐量(高效率地利用CPU),CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。但是在2020年3月發(fā)布的JDK14中,CMS垃圾收集器被徹底刪除了。

2012年,在JDK1.7u4版本中,又有一種優(yōu)秀的垃圾收集器被正式投入使用,它就是Garbage First(G1)。隨著G1 GC的出現(xiàn),GC從傳統(tǒng)的連續(xù)堆內(nèi)存布局設計,逐漸走向不連續(xù)內(nèi)存塊,這是通過引入Region概念實現(xiàn),也就是說,由一堆不連續(xù)的Region組成了堆內(nèi)存。其實也不能說是不連續(xù)的,只是它從傳統(tǒng)的物理連續(xù)逐漸改變?yōu)檫壿嬌系倪B續(xù),這是通過Region的動態(tài)分配方式實現(xiàn)的,我們可以把一個Region分配給Eden、Survivor、老年代、大對象區(qū)間、空閑區(qū)間等的任意一個,而不是固定它的作用,因為越是固定,越是呆板。到2017年JDK9中,G1變成了默認的垃圾收集器,替代了CMS。2018年3月發(fā)布的JDK10中,G1垃圾收集器已經(jīng)可以并行完整垃圾回收了,G1實現(xiàn)并行性來改善最壞情況下的延遲。之后在JDK12,繼續(xù)增強G1,自動返回未用堆內(nèi)存給操作系統(tǒng)。

2018年9月,JDK11發(fā)布,在該版本中提到了兩個垃圾收集器,一個是Epsilon垃圾收集器,又被稱為“No-Op(無操作)”收集器。另一個是ZGC(The Z Garbage Collector),這是一款可伸縮的低延遲垃圾收集器,此時還是實驗性的。ZGC在2019年9月發(fā)布的JDK13中繼續(xù)得到增強,實現(xiàn)自動返回未用堆內(nèi)存給操作系統(tǒng)。在2020年3月發(fā)布的JDK14中ZGC擴展了在macOS和Windows平臺上的應用。經(jīng)過了幾個版本的迭代,ZGC在JDK15中成為正式特性,并且進行了進一步改進,將線程棧的處理從安全點移到了并發(fā)階段,這樣ZGC在掃描根時就不用Stop-The-World了。

2019年3月,JDK12發(fā)布,另一種實驗性GC被引入,它就是Shenandoah GC,也是一種低停頓時間的GC。

1.3、垃圾收集器的分類

Java堆分為新生代和老年代,生命周期較短的對象一般放在新生代,生命周期較長的對象會進入老年代。不同區(qū)域的對象,采取不同的收集方式,以便提高回收效率。因此根據(jù)垃圾收集器工作的內(nèi)存區(qū)間不同,可分為新生代垃圾收集器、老年代垃圾收集器和整堆垃圾收集器,如下圖所示: 新生代收集器:Serial、ParNew、Parallel Scavenge。 老年代收集器:Serial Old、Parallel Old、CMS。 整堆收集器:G1。

其次,新生代在每次垃圾收集發(fā)生時,大部分對象會被回收,存活對象數(shù)量較少,因此每次回收進行碎片整理是非常高效的。而老年代的每次回收,存活對象數(shù)量較多,復制算法明顯變得不合適,一般選用標記–清除算法,或者標記–清除算法與標記–壓縮算法混合實現(xiàn)。因此垃圾收集器可分為壓縮式垃圾收集器和非壓縮式垃圾收集器。壓縮式垃圾收集器會在回收完成后,對存活對象進行壓縮整理,消除回收后的碎片,如果再次分配對象空間,使用指針碰撞技術實現(xiàn),比如Serial Old就是壓縮式垃圾收集器。非壓縮式垃圾收集器不進行這步操作,如果再分配對象空間,只能使用空閑列表技術實現(xiàn),比如CMS就是非壓縮式垃圾收集器。

最后,垃圾收集器還可以分為串行垃圾收集器、并行垃圾收集器、并發(fā)式垃圾收集器等。這又是怎么回事呢?要弄清楚這些,我們需要先來看一下在操作系統(tǒng)中串行(Serial)、并行(Parallel)和并發(fā)(Concurrent)的概念。

在操作系統(tǒng)中串行是指單個線程處理多任務時,多個任務需要按順序執(zhí)行,即完成一個任務之后再去完成另外一個任務,多個任務之間的時間沒有重疊。

在操作系統(tǒng)中并發(fā)是指同一個時間段中有多個任務都處于已啟動運行到運行完畢之間,且這幾個任務都是在同一個CPU上運行。并發(fā)不是真正意義上的“同時”執(zhí)行,只是CPU把一個時間段劃分成幾個小的時間片段,然后多個任務分別被安排在不同的時間片段內(nèi)執(zhí)行,即CPU在這幾個任務之間來回切換,由于CPU處理的速度非???,只要時間間隔處理得當,即可讓用戶感覺是多個任務同時在進行。如下圖所示:

有三個應用程序A、B、C,當前只有一個處理器,在當前時間節(jié)點上,只能有一個應用被處理器執(zhí)行,另外兩個應用暫停,這種情景就是并發(fā)。即并發(fā)從微觀角度看,多個任務不是同時進行的,多個任務之間是互相搶占CPU資源的,但是從宏觀角度看,多個任務是“同時”進行的,它們的時間互相重疊,一個任務還未結束,另一個任務已經(jīng)開始了。

在操作系統(tǒng)中并行是指如果操作系統(tǒng)有一個以上CPU可用時,當一個CPU執(zhí)行一個任務的代碼時,另一個CPU可以執(zhí)行另一個任務的代碼,兩個任務互不搶占CPU資源,可以同時進行。如下圖所示: A、B、C三個應用在當前時間節(jié)點可以同時被不同的處理器執(zhí)行。因此要實現(xiàn)并行的效果的關鍵是需要有多個CPU可用,或者一個CPU存在多核也可以。

總結來看,串行指的是多個任務在不同時間段按順序執(zhí)行。并發(fā)指的是多個任務,在同一時間段內(nèi)“同時”發(fā)生了。并行指的是多個任務,在同一時間點上同時發(fā)生了。串行的多個任務是不會搶同一個CPU資源的,因為它們是順序執(zhí)行。并發(fā)的多個任務之間是會互相搶占CPU資源的。并行的多個任務之間是不互相搶占CPU資源的。而且只有在多CPU或者一個CPU多核的情況中,才會發(fā)生并行,否則,看似同時發(fā)生的事情,其實都是并發(fā)執(zhí)行的。

那么,串行垃圾收集器、并行垃圾收集器、并發(fā)垃圾收集器又是怎么回事呢?

串行垃圾收集器是指使用單線程收集垃圾,即使存在多個CPU可用,也只能用一個CPU執(zhí)行垃圾回收,所以應用程序一定會發(fā)生STW。使用串行方式的垃圾收集器有Serial等。并行垃圾收集器指使用多個垃圾收集線程并行工作,當多個CPU可用時,并行垃圾收集器會使用多個CPU同時進行垃圾回收,因此提升了應用的吞吐量,但此時用戶線程仍會處于等待狀態(tài),即STW現(xiàn)象仍然會發(fā)生。使用并行方式的垃圾收集器有ParNew、Parallel Scavenge、Parallel Old等。并發(fā)垃圾收集器是指用戶線程與垃圾收集線程“同時”,但此時用戶線程和垃圾收集線程不一定是并行的,可能會交替執(zhí)行。如果此時存在多個CPU或者一個CPU存在多核的情況,垃圾收集線程在執(zhí)行時不會“停頓”用戶程序的運行,即垃圾收集線程不會獨占CPU資源,用戶程序再繼續(xù)運行,而垃圾收集程序線程運行于另一個CPU上。使用并發(fā)方式的垃圾收集器有CMS和G1等。

因此,根據(jù)進行垃圾收集的工作線程數(shù)不同,垃圾收集器可以分為串行垃圾收集器和并行垃圾收集器。根據(jù)垃圾收集器的工作模式不同,即垃圾收集器工作時是否獨占CPU資源,可以把垃圾收集器分為并發(fā)式垃圾收集器和獨占式垃圾收集器。

獨占式垃圾收集器一旦運行,就停止應用程序中的其他所有線程,直到垃圾收集過程完全結束。

并發(fā)式垃圾收集器與應用程序線程交替工作,以盡可能減少應用程序的停頓時間。

我們把上面提到的垃圾收集器分類如下:

串行收集器:Serial、Serial Old。并行收集器:ParNew、Parallel Scavenge、Parallel Old。并發(fā)收集器:CMS、G1。

三種類型的垃圾收集器的工作流程如下圖所示: 圖中實線表示應用線程(Application threads),虛線表示垃圾回收線程(GC threads)。串行垃圾收集器是指使用單線程進行垃圾回收,垃圾回收時,只有一個線程在工作,并且Java應用中的所有線程都要暫停,等待垃圾回收的完成。并行垃圾收集器在串行垃圾收集器的基礎之上做了改進,將單線程改為多線程進行垃圾回收,這樣可以縮短垃圾回收的時間。并發(fā)垃圾收集器是指垃圾收集線程和用戶線程同時運行。

其中經(jīng)典的7個垃圾收集器之間的組合關系如下圖所示: 兩個收集器之間由實線連線,表明它們可以搭配使用,常見的組合有:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1,其中Serial Old作為CMS出現(xiàn)“Concurrent Mode Failure”失敗的后備預案。

兩個收集器之間由單虛線連接,表示由于維護和兼容性測試的成本,在JDK 8時將Serial/CMS和ParNew/Serial Old這兩個組合聲明為廢棄,并在JDK 9中完全移除了這些組合。

兩個收集器之間由雙虛線連接,表示JDK 14中,棄用Parallel Scavenge和Serial Old GC組合。需要注意的是JDK 14中已經(jīng)徹底刪除了CMS垃圾收集器。

為什么要有很多收集器,因為Java的使用場景很多,如移動端、服務器等。所以需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的性能。

雖然我們會對各個收集器進行比較,但并非為了挑選一個最好的收集器出來。沒有一種可以在任何場景下都適用的萬能垃圾收集器。所以我們選擇的只是對具體應用最合適的收集器。

1.4、查看默認的垃圾收集器

查看默認的垃圾收集器可以參考下面的方式:

(1)-XX:+PrintCommandLineFlags:查看命令行相關參數(shù)(包含使用的垃圾收集器)。(2)使用命令行指令:“jinfo -flag相關垃圾收集器參數(shù)進程ID”。

下面我們用一段代碼讓程序處于執(zhí)行狀態(tài),使用上面的方式查看虛擬機默認的垃圾收集器,JDK版本為JDK8,代碼清單如下所示:

import java.util.ArrayList;

/**

* @title GcUseTest

* @description -XX:+PrintCommandLineFlags

* @author: yangyongbing

* @date: 2024/4/3 8:52

*/

public class GcUseTest {

public static void main(String[] args) {

ArrayList list = new ArrayList<>();

while (true) {

byte[] arr = new byte[100];

list.add(arr);

try {

Thread.sleep(10);

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

}

}

}

設置VM options的參數(shù)為“-XX:+PrintCommandLineFlags”,即可查看當前JDK使用的是哪種垃圾收集器。例如,以下是基于JDK8的運行結果,其中“-XX:+UseParallelGC”表示使用了ParallelGC。

-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296

-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers

-XX:+UseCompressedOops -XX:+UseParallelGC

也可以使用命令行“jinfo –flag相關垃圾收集器參數(shù)進程ID”進行查看。下圖展示了在JDK8中是使用ParallelGC。 下圖展示了在JDK9中默認使用的是G1垃圾收集器。

2、Serial收集器:串行回收

Serial收集器是最基本、歷史最悠久的垃圾收集器了,是JDK1.3之前回收新生代唯一的選擇。Serial收集器作為HotSpot中Client模式下的默認新生代垃圾收集器,采用的是復制算法、串行回收和STW機制的方式執(zhí)行內(nèi)存回收。

除了新生代,Serial收集器還提供了用于執(zhí)行老年代垃圾收集的Serial Old收集器。Serial Old收集器同樣采用了串行回收和STW機制,只不過內(nèi)存回收算法使用的是標記—壓縮算法。

Serial Old是運行在Client模式下默認的老年代的垃圾收集器。Serial Old在Server模式下主要有兩個用途: (1)與新生代的Parallel Scavenge垃圾收集器搭配。 (2)作為老年代CMS收集器的后備方案。

Serial/Serial Old收集器是單線程的收集器,它的“單線程”的意義不僅僅意味著它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程——“Stop The World”,直到它收集結束。這就意味著每次垃圾收集時都會給用戶帶來一定的卡頓現(xiàn)象,造成不良的用戶體驗,如下圖所示: Serial垃圾收集器相比于其他收集器也有一定的優(yōu)點:簡單而高效。Serial收集器由于沒有線程交互的開銷,只需要專心做垃圾收集,自然可以獲得很高的單線程收集效率。虛擬機的Client模式下使用Serial垃圾收集器是個不錯的選擇。比如在用戶的桌面應用場景中,可用內(nèi)存一般不大(幾十M至一兩百M),可以在較短時間內(nèi)完成垃圾收集(幾十ms至一百多ms),只要不頻繁發(fā)生,使用Serial收集器是一個不錯的選擇。

在HotSpot虛擬機中,可以通過設置“-XX:+UseSerialGC”參數(shù)明確指定新生代和老年代都使用串行收集器。配置完該參數(shù)以后表示新生代用Serial垃圾收集器,老年代用Serial Old垃圾收集器。

在JDK8中手動設置使用Serial垃圾收集器。設置VM options的參數(shù)為“-XX:+PrintCommandLineFlags-XX:+UseSerialGC”,指定新生代和老年代都使用串行收集器。運行結果如下,其中“-XX:+UseSerialGC”表示使用了SerialGC。

-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296

-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers

-XX:+UseCompressedOops -XX:+UseSerialGC

這種垃圾收集器大家了解就可以,現(xiàn)在已經(jīng)幾乎不用該類型的垃圾收集器了,通常在單核CPU場景下才用。

3、ParNew收集器:并行回收

如果說Serial GC是新生代中的單線程垃圾收集器,那么ParNew收集器則是Serial收集器的多線程版本。Par是Parallel的縮寫,New指的是該收集器只能處理新生代。

ParNew收集器除了采用并行回收的方式執(zhí)行內(nèi)存回收外,和Serial垃圾收集器之間幾乎沒有任何區(qū)別。ParNew收集器在新生代中同樣也是采用復制算法和STW機制。ParNew是很多JVM運行在Server模式下新生代的默認垃圾收集器。

對于新生代,回收次數(shù)頻繁,使用并行方式高效。對于老年代,回收次數(shù)少,使用串行方式更加節(jié)省CPU資源。ParNew收集器與各線程的運作關系如下圖所示: 由于ParNew收集器是基于并行回收,那么是否可以斷定ParNew收集器的回收效率在任何場景下都會比Serial收集器更高效呢?

ParNew收集器運行在多CPU的環(huán)境下,由于可以充分利用多CPU、多核心等物理硬件資源優(yōu)勢,可以更快速地完成垃圾收集,提升程序的吞吐量。

但是在單個CPU的環(huán)境下,ParNew收集器不比Serial收集器更高效。雖然Serial收集器是基于串行回收,但是由于CPU不需要頻繁地做任務切換,因此可以有效避免多線程交互過程中產(chǎn)生的一些額外開銷。

除Serial外,目前只有ParNew垃圾收集器能與CMS收集器配合工作。在程序中,開發(fā)人員可以通過選項“-XX:+UseParNewGC”手動指定使用ParNew收集器執(zhí)行內(nèi)存回收任務。它表示新生代使用并行收集器,不影響老年代。

設置VM options的參數(shù)為“-XX:+PrintCommandLineFlags-XX:+UseParNewGC”,指定新生代使用ParNew垃圾收集器。運行結果如下,其中“-XX:+UseParNewGC”表示使用了ParNew垃圾收集器。

-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296

-XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers

-XX:+UseCompressedOops -XX:+UseParNewGC

4、Parallel Scaveng收集器:吞吐量優(yōu)先

HotSpot的新生代中除了擁有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器同樣也采用了復制算法、并行回收和STW機制。那么Parallel Scavenge收集器的出現(xiàn)是否多余?Parallel Scavenge收集器的目標是達到一個可控制的吞吐量,它也被稱為吞吐量優(yōu)先的垃圾收集器。自適應調(diào)節(jié)策略也是Parallel Scavenge與ParNew一個重要區(qū)別,Parallel Scavenge獲取應用程序的運行情況收集系統(tǒng)的性能監(jiān)控信息,動態(tài)調(diào)整參數(shù)以提供最合適的停頓時間或最大的吞吐量,這種調(diào)節(jié)方式稱為垃圾收集的自適應調(diào)節(jié)策略。

高吞吐量可以高效率地利用CPU時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務,例如,那些執(zhí)行批量處理、訂單處理、工資支付、科學計算的應用程序。

Parallel Scavenge收集器在JDK1.6時提供了用于執(zhí)行老年代垃圾收集的Parallel Old收集器,用來代替老年代的Serial Old收集器。Parallel Old收集器采用了標記—壓縮算法,但同樣也是基于并行回收和STW機制。

在程序吞吐量優(yōu)先的應用場景中,Parallel Scavenge收集器和Parallel Old收集器的組合,在Server模式下的內(nèi)存回收性能很不錯。Parallel Scavenge/Parallel Old收集器中GC線程和用戶線程之間的運作關系如下圖所示: Parallel垃圾收集器常用參數(shù)配置如下:

(1)-XX:+UseParallelGC:指定新生代使用Parallel并行收集器執(zhí)行內(nèi)存回收任務;-XX:+UseParallelOldGC:指定老年代都是使用并行回收收集器,JDK8默認開啟。默認情況下,開啟其中一個參數(shù),另一個也會被開啟(互相激活)。(2)-XX:ParallelGCThreads:設置新生代并行收集器的線程數(shù)。一般最好與CPU核心數(shù)量相等,以避免過多的線程數(shù)影響垃圾收集性能。

在默認情況下,當CPU核心數(shù)量小于8個,ParallelGCThreads的值等于CPU核心數(shù)量。當CPU核心數(shù)量大于8個,ParallelGCThreads的值等于3+[5*CPU_Count]/8]。

(3)-XX:MaxGCPauseMillis:設置垃圾收集器最大停頓時間(即STW的時間)。單位是毫秒。為了盡可能地把停頓時間控制在MaxGCPauseMills以內(nèi),收集器在工作時會調(diào)整Java堆大小或者其他一些參數(shù)。

對于用戶來講,停頓時間越短體驗越好,但是在服務器端,我們更加注重高并發(fā)和應用程序的吞吐量,所以Parallel垃圾收集器更適合服務器端。

(4)-XX:GCTimeRatio:設置垃圾收集時間占總時間的比例(1/(N+1))。用于衡量吞吐量的大小。該參數(shù)取值范圍是(0,100),默認值是99,表示垃圾收集時間不超過1%。該參數(shù)與前一個-XX:MaxGCPauseMillis參數(shù)有一定矛盾性。停頓時間越長,GCTimeRatio參數(shù)就越容易超過設定的比例。(5)-XX:+UseAdaptiveSizePolicy:開啟自適應調(diào)節(jié)策略。在這種模式下,新生代的大小、Eden區(qū)和Survivor區(qū)的比例、晉升老年代的對象年齡等參數(shù)會被自動調(diào)整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。

在手動調(diào)優(yōu)比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMills),讓虛擬機自己完成調(diào)優(yōu)工作。

設置VM options的參數(shù)為“-XX:+PrintCommandLineFlags-XX:+UseParallelGC”,指定新生代和老年代使用Parallel垃圾收集器。運行結果如下,其中“-XX:+UseParallelGC”表示使用了Parallel垃圾收集器。

-XX:InitialHeapSize=268435456

-XX:MaxHeapSize=4294967296

-XX:+PrintCommandLineFlags

-XX:+UseCompressedClassPointers

-XX:+UseCompressedOops

-XX:+UseParNewGC

5、CMS收集器:低延遲

5.1、CMS收集器介紹

CMS(Concurrent Low Pause Collector)是JDK1.4.2開始引入的新GC算法,在JDK5和JDK6中得到了進一步改進,它的主要適合場景是對響應時間的需求大于對吞吐量的要求。CMS垃圾收集器在強交互應用中幾乎可認為有劃時代意義。它是HotSpot虛擬機中第一款真正意義上的并發(fā)收集器,第一次實現(xiàn)了讓垃圾收集線程與用戶線程同時工作。

CMS收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間。停頓時間越短,延遲就越低,就越適合與用戶強交互的程序,因為良好的響應速度能更好地提升用戶體驗。

目前很大一部分的Java應用集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務端上,這類應用尤其重視服務的響應速度,希望減少系統(tǒng)停頓時間,以給用戶帶來較好的使用體驗。CMS收集器就非常符合這類應用的需求。

CMS的垃圾收集算法采用標記–清除算法,并且也會STW。不幸的是,CMS作為老年代的收集器,卻無法與新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。

5.2、CMS的工作原理

CMS整個過程比之前的收集器要復雜,整個過程分為4個主要階段,即初始標記階段、并發(fā)標記階段、重新標記階段和并發(fā)清除階段,如下圖所示:

(1)初始標記(Initial-Mark)階段:在這個階段中,程序中所有的工作線程都將會因為STW機制而出現(xiàn)短暫的暫停,這個階段的主要任務僅僅只是標記出GC Roots能直接關聯(lián)到的對象。一旦標記完成之后就會恢復之前被暫停的所有應用線程。由于直接關聯(lián)對象比較小,所以這里的速度非??臁?2)并發(fā)標記(Concurrent-Mark)階段:從GC Roots的直接關聯(lián)對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)運行。(3)重新標記(Remark)階段:由于在并發(fā)標記階段中,程序的工作線程會和垃圾收集線程同時運行或者交叉運行,為了修正在并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄,需要一次重新標記操作,通常這個階段的停頓時間會比初始標記階段稍長一些,但也遠比并發(fā)標記階段的時間短。(4)并發(fā)清除(Concurrent-Sweep)階段:此階段清理已經(jīng)被標記為死亡的對象,釋放內(nèi)存空間。由于不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發(fā)的。

盡管CMS收集器采用的是并發(fā)回收,但是在其初始化標記和再次標記這兩個階段中仍然需要執(zhí)行STW機制暫停程序中的工作線程,不過停頓時間并不會太長,因此可以說明目前所有的垃圾收集器都做不到完全不需要STW,只是盡可能地縮短停頓時間。

由于最耗費時間的并發(fā)標記與并發(fā)清除階段都不需要暫停工作,所以整體的回收是低停頓的。

另外,由于在垃圾收集階段用戶線程沒有中斷,所以在CMS回收過程中,還應該確保應用程序用戶線程有足夠的內(nèi)存可用。因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當堆內(nèi)存使用率達到某一閾值時,便開始進行回收,以確保應用程序在CMS工作過程中依然有足夠的空間支持應用程序運行。要是CMS運行期間預留的內(nèi)存無法滿足程序需要,就會出現(xiàn)一次“Concurrent Mode Failure”失敗,這時虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

CMS收集器的垃圾收集算法采用的是標記–清除算法,這意味著每次執(zhí)行完內(nèi)存回收后,由于被執(zhí)行內(nèi)存回收的無用對象所占用的內(nèi)存空間極有可能是不連續(xù)的一些內(nèi)存塊,不可避免地將會產(chǎn)生一些內(nèi)存碎片,如下圖所示: 圖中清理完內(nèi)存之后零碎的小內(nèi)存區(qū)域就是所謂的內(nèi)存碎片。那么CMS在為新對象分配內(nèi)存空間時,將無法使用指針碰撞(Bump the Pointer)技術,而只能夠選擇空閑列表(Free List)執(zhí)行內(nèi)存分配。

有人會覺得既然標記–清除會造成內(nèi)存碎片,那么為什么不把算法換成標記–壓縮呢?

答案其實很簡單,要保證用戶線程能繼續(xù)執(zhí)行,前提是它運行的資源(比如內(nèi)存占用)不受影響。當CMS并發(fā)清除的時候,原來的用戶線程依然在使用內(nèi)存,所以也就無法整理內(nèi)存。標記—壓縮算法更適合在STW這種場景下使用。

CMS的優(yōu)點是并發(fā)收集和低延遲。CMS的弊端也很明顯:

(1)會產(chǎn)生內(nèi)存碎片,導致并發(fā)清除后,用戶線程可用的空間不足。在無法分配大對象的情況下,不得不提前觸發(fā)Full GC。(2)對CPU資源非常敏感。在并發(fā)階段,它雖然不會導致用戶停頓,但是會因為占用了一部分線程而導致應用程序變慢,總吞吐量會降低。(3)由于在垃圾收集階段用戶線程沒有中斷,要是CMS運行期間預留的內(nèi)存無法滿足程序需要,就會出現(xiàn)一次“Concurrent Mode Failure”失敗而導致另一次Full GC的產(chǎn)生。(4)無法處理浮動垃圾。在并發(fā)清除階段由于程序的工作線程和垃圾收集線程是同時運行或者交叉運行的,那么在并發(fā)清除階段如果產(chǎn)生新的垃圾對象,CMS將無法對這些垃圾對象進行標記,最終會導致這些新產(chǎn)生的垃圾對象沒有被及時回收,從而只能在下一次執(zhí)行GC時釋放這些之前未被回收的內(nèi)存空間。

5.3、CMS收集器的參數(shù)設置

CMS收集器可以設置的參數(shù)如下:

(1)-XX:+UseConcMarkSweepGC:指定使用CMS收集器執(zhí)行內(nèi)存回收任務。開啟該參數(shù)后會自動將-XX:+UseParNewGC打開。即垃圾收集器組合為ParNew(Young區(qū)用)、CMS(Old區(qū)用)和Serial Old(CMS的備用方案)。(2)-XX:CMSlnitiatingOccupanyFraction:設置堆內(nèi)存使用率的閾值,一旦達到該閾值,便開始進行回收。JDK5及以前版本的默認值為68,即當老年代的空間使用率達到68%時,會執(zhí)行一次CMS回收。JDK6及以上版本默認值為92%。如果內(nèi)存增長緩慢,則可以設置一個稍大的值,大的閾值可以有效降低CMS的觸發(fā)頻率,減少老年代回收的次數(shù),可以較為明顯地改善應用程序性能。反之,如果應用程序內(nèi)存使用率增長很快,則應該降低這個閾值,以避免頻繁觸發(fā)老年代串行收集器。(3)-XX:+UseCMSCompactAtFullCollection:用于指定在執(zhí)行完Full GC后對內(nèi)存空間進行壓縮整理,以此避免內(nèi)存碎片的產(chǎn)生。不過由于內(nèi)存壓縮整理過程無法并發(fā)執(zhí)行,所帶來的問題就是停頓時間變得更長了。(4)-XX:CMSFullGCsBeforeCompaction:設置在執(zhí)行多少次Full GC后對內(nèi)存空間進行壓縮整理。(5)-XX:ParallelCMSThreads:設置CMS的線程數(shù)量。CMS默認啟動的線程數(shù)是(ParallelGCThreads+3)/4,ParallelGCThreads是新生代并行收集器的線程數(shù)。當CPU資源比較緊張時,受到CMS收集器線程的影響,應用程序的性能在垃圾回收階段可能會非常糟糕。

使用CMS垃圾收集器。設置VM options的參數(shù)為“-XX:+Print CommandLineFlags-XX:+UseConcMarkSweepGC”,指定老年代使用CMS垃圾收集器,同時,新生代會觸發(fā)對ParNew的使用。運行結果如下:

-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296

-XX:MaxNewSize=697933824 -XX:MaxTenuringThreshold=6

-XX:OldPLABSize=16 -XX:+PrintCommandLineFlags

-XX:+UseCompressedClassPointers -XX:+UseCompressedOops

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC

其中“-XX:+UseConcMarkSweepGC”表示老年代使用了CMS垃圾收集器,“-XX:+UseParNewGC”表示新生代使用了ParNew垃圾收集器。

到目前為止,已經(jīng)介紹了3種非常經(jīng)典的垃圾收集器:Serial垃圾收集器、Parallel垃圾收集器和Concurrent Mark Sweep垃圾收集器。那么這三個垃圾收集器該如何進行選擇呢?如果想要最小化地使用內(nèi)存和并行開銷,請選Serial垃圾收集器;如果想要最大化應用程序的吞吐量,請選Parallel垃圾收集器;如果想要最小化垃圾收集的停頓時間,請選CMS垃圾收集器。

5.4、JDK后續(xù)版本中CMS的變化

2017年JDK9中,G1變成了默認的垃圾收集器,替代了CMS。JDK9中CMS被標記為Deprecate,如果對JDK 9及以上版本的HotSpot虛擬機使用參數(shù)“-XX:+UseConcMarkSweepGC”來開啟CMS收集器的話,用戶會收到一個警告信息,提示CMS未來將會被廢棄。

2020年3月,JDK14發(fā)布,該版本徹底刪除了CMS垃圾收集器。如果在JDK14中使用“-XX:+UseConcMarkSweepGC”的話,JVM不會報錯,只是給出一個warning信息,不會退出JVM。JVM會自動回退以默認GC方式啟動JVM。

6、G1收集器:區(qū)域化分代式

6.1、G1收集器

G1(Garbage-First)垃圾收集器是在Java7 update 4之后引入的一個新的垃圾收集器,是當今收集器技術發(fā)展的最前沿成果之一。

既然我們已經(jīng)有了前面幾個強大的垃圾收集器,為什么還要發(fā)布G1垃圾收集器呢?原因就在于應用程序所應對的業(yè)務越來越龐大、復雜,用戶越來越多,沒有垃圾收集器不能保證應用程序正常進行,而經(jīng)常造成STW的垃圾收集器又跟不上實際的需求,所以才會不斷地嘗試對垃圾收集器進行優(yōu)化。為了實現(xiàn)在應用程序運行環(huán)境內(nèi)存不斷擴大,處理器數(shù)量不斷增加的情況下,進一步降低停頓時間,同時還能兼顧良好的吞吐量的目標,G1垃圾收集器應運而生。

官方給G1設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,擔負著“全功能的垃圾收集器”的重任和期望。G1是一款基于并行和并發(fā)的收集器,它把堆內(nèi)存分割為很多區(qū)域(Region),它們雖然物理上不連續(xù),但是邏輯上是連續(xù)的。然后使用不同的Region來表示Eden區(qū)、Survivor 0區(qū)、Survivor 1區(qū)、老年代等。

G1有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大?。ɑ厥账@得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。

由于這種方式的側重點在于回收垃圾最大量的區(qū)間(Region),所以我們給G1取一個名字就是垃圾優(yōu)先(Garbage First)。

G1在JDK1.7版本正式啟用,移除了Experimental(實驗性)的標識,是JDK 9以后的默認垃圾收集器,取代了CMS收集器以及Parallel/ Parallel Old組合。在JDK8中還不是默認的垃圾收集器,需要使用“-XX:+UseG1GC”來啟用。

6.2、G1收集器的特點和使用場景

G1是一款面向服務端應用的垃圾收集器,主要針對配備多核CPU及大容量內(nèi)存的機器,極大可能降低垃圾回收停頓時間的同時,還兼具高吞吐量的性能特征。與其他垃圾收集器相比,G1使用了全新的分區(qū)算法,其特點如下:

1、并行與并發(fā):(1)并行性是指G1在回收期間,可以有多個垃圾收集線程同時工作,有效利用多核計算能力。此時用戶線程STW。(2)并發(fā)性是指G1擁有與應用程序交替執(zhí)行的能力,部分工作可以和應用程序同時執(zhí)行,因此,一般來說,不會在整個回收階段發(fā)生完全阻塞應用程序的情況。2、分代收集:(1)從分代上看,G1依然屬于分代型垃圾收集器,它會區(qū)分新生代和老年代,新生代依然有Eden區(qū)和Survivor區(qū)。但從堆的結構上看,它不要求整個Eden區(qū)、新生代或者老年代都是連續(xù)的,也不再堅持固定大小和固定數(shù)量。(2)和之前的各類收集器不同,G1可以工作在新生代和老年代。其他收集器要么工作在新生代,要么工作在老年代。3、空間整合:(1)CMS采用了標記–清除算法,會存在內(nèi)存碎片,會在若干次GC后進行一次碎片整理。(2)G1將內(nèi)存劃分為一個個的Region。內(nèi)存的回收是以Region作為基本單位的。Region之間是復制算法,但整體上實際可看作是標記–壓縮算法,兩種算法都可以避免內(nèi)存碎片。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。尤其是當Java堆非常大的時候,G1的優(yōu)勢更加明顯。4、可預測的停頓時間模型:這是G1相對于CMS的另一大優(yōu)勢,G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為Mms的時間片段內(nèi),消耗在垃圾收集上的時間不得超過Nms。由于分區(qū)的原因,G1可以只選取部分區(qū)域進行內(nèi)存回收,這樣縮小了回收的范圍,因此STW的情況也可以得到較好的控制。G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經(jīng)驗值),在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。保證了G1收集器在有限的時間內(nèi)可以獲取盡可能高的收集效率。相比于CMS GC,G1未必能做到CMS在最好情況下的延時停頓,但是比最差情況要好很多。

G1垃圾收集器相較于CMS,還不具備全方位、壓倒性優(yōu)勢。比如在用戶程序運行過程中,G1無論是為了垃圾收集產(chǎn)生的內(nèi)存占用(Footprint)還是程序運行時的額外執(zhí)行負載(Overload)都要比CMS要高。從經(jīng)驗上來說,在小內(nèi)存應用上CMS的表現(xiàn)大概率會優(yōu)于G1,而G1在大內(nèi)存應用上則發(fā)揮其優(yōu)勢,平衡點在6~8G。

G1收集器主要面向服務端應用,針對具有大內(nèi)存、多處理器的機器,在普通大小的堆里表現(xiàn)并不驚喜。如果應用需要較低停頓時間,并且需要比較大的堆內(nèi)存提供支持時,那么G1收集器無疑是比較合適的垃圾收集器,例如在堆大小約6GB或更大時,可預測的停頓時間可以低于0.5秒。

一般我們認為在下面的幾種情況中,使用G1可能比CMS更好:

(1)超過50%的Java堆被活動數(shù)據(jù)占用。(2)對象分配頻率或年代提升頻率變化很大。(3)GC停頓時間過長(長于0.5~1s)。

HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用內(nèi)置的JVM線程執(zhí)行垃圾收集的多線程操作,而G1可以采用應用線程承擔后臺運行的垃圾收集工作,即當JVM的垃圾收集線程處理速度慢時,系統(tǒng)會調(diào)用應用程序線程幫助加速垃圾回收過程。

6.3、分區(qū)Region:化整為零

使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據(jù)堆空間的實際大小而定,整體被控制在1MB到32MB,且為2的N次冪,即1MB、2MB、4MB、8MB、16MB、32MB。Region塊大小可以通過“-XX:G1HeapRegionSize”設定。所有的Region大小相同,且在JVM生命周期內(nèi)不會被改變。

雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合。通過Region的動態(tài)分配方式實現(xiàn)邏輯上的連續(xù),如下圖所示: 一個Region有可能屬于Eden、Survivor或者Old/Tenured內(nèi)存區(qū)域。注意一個Region只可能屬于一個角色。上圖中的E表示該Region屬于Eden內(nèi)存區(qū)域,S表示屬于Survivor內(nèi)存區(qū)域,O表示屬于Old內(nèi)存區(qū)域。上圖中空白區(qū)域表示未使用的內(nèi)存空間。

G1垃圾收集器還增加了一種新的內(nèi)存區(qū)域,叫作Humongous內(nèi)存區(qū)域,如上圖中的H塊,主要用于存儲大對象,如果超過1.5個Region,就放到H。設置H的原因是對于堆中的大對象,默認直接會被分配到老年代,但是如果它是一個短期存在的大對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放大對象。如果一個H區(qū)裝不下一個大對象,那么G1會尋找連續(xù)的H區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC。G1的大多數(shù)行為都把H區(qū)作為老年代的一部分來看待。

正常的Region的內(nèi)存大小為4MB左右。Region區(qū)域使用指針碰撞算法來為對象分配內(nèi)存,每一個分配的Region被分成兩部分,已分配(allocated)和未分配(unallocate)的,它們之間的界限稱為top指針。將變量或?qū)ο髮嶓w存放到未使用的unallocate區(qū)域。當再分配新的對象的時候指針(top)右移將新對象存放到allocated區(qū)域,如下圖所示: 當然在多線程情況下,會有并發(fā)的問題,G1收集器采用的是TLAB(Thread Local Allocation Buffer)和CAS(Compare and Swap)來解決并發(fā)的安全問題。

6.4、G1收集器垃圾回收過程

G1可以作用于整個新生代和老年代,G1的垃圾回收過程主要包括如下三個環(huán)節(jié):

新生代GC(Young GC)。老年代并發(fā)標記(Concurrent Marking)?;旌匣厥?Mixed GC)。

作為JVM的兜底邏輯,如果應用程序垃圾收集時內(nèi)存不足,G1會像其他收集器一樣執(zhí)行Full GC,即強力回收內(nèi)存。

垃圾回收的流程如下圖所示: 按圖中順時針走向,以新生代GC→新生代GC+并發(fā)標記過程→混合GC順序進行垃圾回收。首先執(zhí)行新時代GC,之后執(zhí)行并發(fā)標記過程,該過程會伴隨著Young GC的發(fā)生,最后執(zhí)行混合GC。

應用程序分配內(nèi)存,當新生代的Eden區(qū)用盡時開始新生代回收過程。G1的新生代收集階段是一個并行的獨占式收集器。在新生代回收期,G1暫停所有應用程序線程,啟動多線程執(zhí)行新生代回收。然后從新生代區(qū)移動存活對象到Survivor區(qū)或者老年代區(qū),也有可能是兩個區(qū)都會涉及。

當堆內(nèi)存使用達到一定值(默認45%)時,開始老年代并發(fā)標記過程。標記完成馬上開始混合回收過程。對于一個混合回收期,G1從老年代區(qū)移動存活對象到空閑區(qū),這些空閑區(qū)也就成為老年代的一部分。G1收集器在老年代的處理方式和其他垃圾收集器不同,G1不需要回收整個老年代,一次只需要掃描/回收一小部分老年代的Region就可以了。同時,這個老年代Region是和新生代一起被回收的。

G1收集器在回收的過程會有很多問題,比如一個對象被不同區(qū)域引用的問題,一個Region不可能是孤立的,一個Region中的對象可能被其他任意Region中對象引用,判斷對象存活時,是需要掃描整個Java堆才能保證準確。在其他的分代收集器,也存在這樣的問題(而G1更突出)?;厥招律膊坏貌煌瑫r掃描老年代,因為判斷對象可達,需要通過GC Roots來判斷對象是否可達,那么尋找GC Roots的過程可能會放大范圍,查找到老年代的對象,這樣會降低Young GC的效率。

針對上述問題,JVM給出的解決方法如下:無論G1還是其他分代收集器,JVM都是使用記憶集(Remembered Set,Rset)來避免全局掃描。每個Region都有一個對應的Remembered Set。每次Reference類型數(shù)據(jù)寫操作時,都會產(chǎn)生一個寫屏障(Write Barrier)暫時中斷操作。然后檢查將要寫入的引用指向的對象是否和該引用類型數(shù)據(jù)在不同的Region(其他收集器:檢查老年代對象是否引用了新生代對象)。如果不同,通過CardTable把相關引用信息記錄到引用指向?qū)ο蟮乃赗egion對應的Remembered Set中。當進行垃圾收集時,在GC根節(jié)點的枚舉范圍加入Remembered Set;就可以保證不進行全局掃描,也不會有遺漏。

如下圖所示: 存在3個Region,每個Region包含一個Rset,當產(chǎn)生一個新對象放在Region2中時,此時判斷指向該對象的引用是否都在Region2中;可以發(fā)現(xiàn)該對象存在兩個引用對象,分別在Region1和Region3中,所以需要通過CardTable把引用信息記錄到Region2中的Rset中。

1、G1回收過程一:新生代GC JVM啟動時,G1先準備好Eden區(qū),程序在運行過程中不斷創(chuàng)建對象到Eden區(qū),當Eden空間耗盡時,G1會啟動一次新生代垃圾回收過程。新生代垃圾回收只會回收Eden區(qū)和Survivor區(qū)。

新時代GC時,首先G1停止應用程序的執(zhí)行(Stop-The-World),G1創(chuàng)建回收集(Collection Set),回收集是指需要被回收的內(nèi)存分段的集合,新生代回收過程的回收集包含新生代Eden區(qū)和Survivor區(qū)所有的內(nèi)存分段。如下圖所示:

可以看到內(nèi)存回收之后部分Eden區(qū)和Survivor區(qū)直接清空變?yōu)樾碌腟urvivor區(qū),也有Survivor區(qū)的直接晉升為Old區(qū)。

然后開始如下回收過程:

第一階段,掃描根。根是指static變量指向的對象,正在執(zhí)行的方法調(diào)用鏈條上的局部變量等。根引用連同RSet記錄的外部引用作為掃描存活對象的入口。第二階段,更新RSet。對于應用程序的引用賦值語句“object.field=object”,JVM會在更新RSet之前和之后執(zhí)行特殊的操作,在dirty card queue中入隊一個保存了對象引用信息的card。處理dirty card queue中的card,更新RSet。此階段完成后,RSet可以準確地反映老年代對所在的內(nèi)存分段中對象的引用。那為什么不在引用賦值語句處直接更新RSet呢?這是為了性能的需要,RSet的處理需要線程同步,開銷會很大,使用隊列性能會好很多。第三階段,處理RSet。識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認為是存活的對象。第四階段,復制對象。此階段,對象樹被遍歷,Eden區(qū)內(nèi)存段中存活的對象會被復制到Survivor區(qū)中空的內(nèi)存分段,Survivor區(qū)內(nèi)存段中存活的對象如果年齡未達閾值,年齡會加1,達到閾值會被復制到Old區(qū)中空的內(nèi)存分段。如果Survivor空間不夠,Eden空間的部分數(shù)據(jù)會直接晉升到老年代空間。第五階段,處理引用。處理Soft、Weak、Phantom、Final、JNI Weak等引用。最終Eden空間的數(shù)據(jù)為空,GC停止工作,而目標內(nèi)存中的對象都是連續(xù)存儲的,沒有碎片,所以復制過程可以達到內(nèi)存整理的效果,減少碎片。

新生代GC完成以后,接下來就是老年代并發(fā)標記過程了。

2、G1回收過程二:并發(fā)標記過程 并發(fā)標記過程主要包含5個步驟,如下所示:

初始標記階段:標記從根節(jié)點直接可達的對象。這個階段是STW的,并且會觸發(fā)一次新生代GC。

根區(qū)域掃描(Root Region Scanning):G1GC掃描Survivor區(qū)直接可達的老年代區(qū)域?qū)ο?,并標記被引用的對象。這一過程必須在新生代GC之前完成。

并發(fā)標記(Concurrent Marking):在整個堆中進行并發(fā)標記(和應用程序并發(fā)執(zhí)行),此過程可能被新生代GC中斷。在并發(fā)標記階段,若發(fā)現(xiàn)區(qū)域?qū)ο笾械乃袑ο蠖际抢沁@個區(qū)域會被立即回收。同時,并發(fā)標記過程中,會計算每個區(qū)域的對象活性(區(qū)域中存活對象的比例)。

再次標記(Remark):由于應用程序持續(xù)進行,需要修正上一次的標記結果,是STW的。G1中采用了比CMS更快的初始快照算法snapshot-at-the-beginning(SATB)。

獨占清理(Cleanup):計算各個區(qū)域的存活對象和GC回收比例,并進行排序,識別可以混合回收的區(qū)域。為下階段做鋪墊,這個過程是STW的。這個階段并不會實際上去做垃圾的收集。

并發(fā)清理階段:識別并清理完全空閑的區(qū)域。

3、G1回收過程三:混合回收(Mixed GC) 如下圖所示: 當越來越多的對象晉升到老年代區(qū)時,為了避免堆內(nèi)存被耗盡,虛擬機會觸發(fā)一個混合的垃圾收集器,即Mixed GC,該算法并不是一個Old GC,除了回收整個新生代區(qū),還會回收一部分的老年代區(qū)。這里需要注意的是回收一部分老年代,而不是全部老年代??梢赃x擇哪些老年代區(qū)進行收集,從而可以對垃圾回收的所耗時間進行控制。也要注意的是Mixed GC并不是Full GC。

并發(fā)標記結束以后,老年代中百分百為垃圾的內(nèi)存分段被回收了,部分為垃圾的內(nèi)存分段被計算了出來。G1的混合回收階段是可以分多次進行的,但每次都會進入STW狀態(tài),次數(shù)默認是8次(可以通過“-XX:G1MixedGCCountTarget”設置)被回收。運行邏輯是先STW,執(zhí)行一次混合回收回收一些Region,接著恢復系統(tǒng)運行,然后再STW,再執(zhí)行混合回收。

每次混合回收的回收集(Collection Set)包括需要回收的老年代區(qū)的八分之一、Eden區(qū)以及Survivor區(qū)?;旌匣厥盏乃惴ê托律厥盏乃惴ㄍ耆粯?,只是回收集多了老年代的內(nèi)存Region。具體過程請參考上面的新生代回收過程。

由于老年代中的內(nèi)存分段默認分8次回收,G1會優(yōu)先回收垃圾多的Region。垃圾占Region比例越高,越會被先回收。并且有一個閾值會決定Region是否被回收,“-XX:G1Mixe dGCLiveThresholdPercent”默認為65%,意思是垃圾占內(nèi)存分段比例要達到65%才會被回收。如果垃圾占比太低,意味著存活的對象占比高,在復制的時候會花費更多的時間。

混合回收并不一定要進行8次,事實上,混合回收階段具體執(zhí)行幾次回收,看的是空閑的Region數(shù)量何時達到堆內(nèi)存的10%,如果執(zhí)行3次回收就達到了10%,就不會再繼續(xù)執(zhí)行回收了。這個10%可以使用參數(shù)“-XX:G1HeapWastePercent”來控制。該參數(shù)默認值為10%,意思是允許整個堆內(nèi)存中有10%的空間被浪費,意味著如果發(fā)現(xiàn)可以回收的垃圾占堆內(nèi)存的比例低于10%,則不再進行混合回收。因為GC會花費很多的時間但是回收到的內(nèi)存卻很少。

4、G1回收可選的過程四:Full GC G1的初衷就是要避免Full GC的出現(xiàn)。但是如果上述方式不能正常工作,G1會停止應用程序的執(zhí)行(Stop-The-World),使用單線程的內(nèi)存回收算法進行垃圾回收,性能會非常差,應用程序停頓時間會很長。

要避免Full GC的發(fā)生,一旦發(fā)生需要進行調(diào)整。什么時候會發(fā)生Full GC呢?比如堆內(nèi)存太小,當G1在復制存活對象的時候沒有空的內(nèi)存分段可用,則會回退到Full GC,這種情況可以通過增大內(nèi)存解決。

導致G1 Full GC的原因可能有兩個:

(1)Evacuation的時候沒有足夠的to-space來存放晉升的對象;(2)并發(fā)處理過程完成之前空間耗盡。

5、G1回收過程:補充 從Oracle官方透露出來的信息可獲知,回收階段(Evacuation)其實本也有想過設計成與用戶程序一起并發(fā)執(zhí)行,但這件事情做起來比較復雜,考慮到G1只回收一部分Region,停頓時間是用戶可控制的,所以并不迫切去實現(xiàn),而選擇把這個特性放到了G1之后出現(xiàn)的低延遲垃圾收集器(即ZGC)中。另外,還考慮到G1不是僅僅面向低延遲,停頓用戶線程能夠最大幅度提高垃圾收集效率,為了保證吞吐量所以才選擇了完全暫停用戶線程的實現(xiàn)方案。

6、G1收集器優(yōu)化建議 針對G1收集器優(yōu)化,我們給出以下建議,大家在學習過程中可以參考:

針對G1收集器優(yōu)化,我們給出以下建議,大家在學習過程中可以參考。(2)停頓時間目標不要太過嚴苛。G1的吞吐量目標是90%的應用程序時間和10%的垃圾回收時間。評估G1的吞吐量時,停頓時間目標不要太嚴苛。目標太過嚴苛表示你愿意承受更多的垃圾回收開銷,而這些會直接影響到吞吐量。

6.5、G1收集器的參數(shù)設置

G1收集器的相關參數(shù)說明如下:

-XX:+UseG1GC:指定使用G1收集器執(zhí)行內(nèi)存回收任務,JDK 9之后G1是默認垃圾收集器。-XX:G1HeapRegionSize:設置每個Region的大小,值是2的冪次方,范圍是1MB到32MB,目標是根據(jù)最小的Java堆大小劃分出約2048個區(qū)域。默認值是堆內(nèi)存的1/2000。-XX:MaxGCPauseMillis:設置期望達到的最大GC停頓時間指標(JVM會盡力實現(xiàn),但不保證達到)。默認值是200ms。-XX:ParallelGCThread:設置STW時并行的GC線程數(shù)量值。最多可以設置為8。-XX:ConcGCThreads:設置并發(fā)標記的線程數(shù)。通常設置為并行垃圾回收線程數(shù)(ParallelGCThreads)的1/4左右。-XX:InitiatingHeapOccupancyPercent:設置觸發(fā)并發(fā)GC周期的Java堆占用率閾值,超過此值,就觸發(fā)GC。默認值是45。

G1的設計原則就是簡化JVM性能調(diào)優(yōu),開發(fā)人員只需要簡單地配置即可完成調(diào)優(yōu)。首先開啟G1垃圾收集器,然后設置堆的最大內(nèi)存,最后設置最大停頓時間即可。

G1中提供了三種垃圾回收模式,它們分別是Young GC、Mixed GC和Full GC,在不同的條件下被觸發(fā)。

7、垃圾收集器的新發(fā)展

垃圾收集器仍然處于飛速發(fā)展之中,目前的默認收集器G1仍在不斷地改進,例如串行的Full GC在JDK 10以后,已經(jīng)改成了并行運行。

即使是Serial GC,雖然比較古老,但是簡單的設計和實現(xiàn)未必就是過時的,它本身的開銷,不管是GC相關數(shù)據(jù)結構的開銷,還是線程的開銷,都是非常小的。隨著云計算的興起,在Serverless等新的應用場景下,Serial GC也有了新的舞臺。

比較不幸的是CMS GC,因為其算法的理論缺陷等原因,雖然現(xiàn)在還有非常大的用戶群體,但在JDK 9中已經(jīng)被標記為廢棄,并在JDK 14版本中移除。

在JDK 11中出現(xiàn)了兩個新的垃圾收集器:Epsilon和ZGC。

在JDK 12中引入了Shenandoah GC。

7.1、Epsilon和ZGC

在JDK 11中出現(xiàn)了兩個新的垃圾收集器:Epsilon和ZGC。Epsilon垃圾收集器是一個無操作的收集器(A No-Op Garbage Collector)。Epsilon垃圾收集器是為不需要或禁止GC的場景提供的最小實現(xiàn),它僅實現(xiàn)了“分配”部分,我們可以在它上面來實現(xiàn)回收功能。

ZGC垃圾收集器是一個可伸縮的低延遲垃圾收集器,處于實驗性階段[A Scalable Low-Latency Garbage Collector(Experimental)]。

ZGC與Shenandoah目標高度相似,在盡可能減小對吞吐量影響的前提下,實現(xiàn)在任意堆內(nèi)存大小下把垃圾回收的停頓時間限制在10ms以內(nèi)的超低延遲?!渡钊肜斫釰ava虛擬機》一書中這樣定義ZGC:“ZGC收集器是一款基于Region內(nèi)存布局的,(暫時)不設分代的,使用了讀屏障、染色指針和內(nèi)存多重映射等技術來實現(xiàn)可并發(fā)的標記–整理算法的,以低延遲為首要目標的一款垃圾收集器?!? ZGC的工作過程可以分為4個階段:并發(fā)標記→并發(fā)預備重分配→并發(fā)重分配→并發(fā)重映射。

ZGC幾乎在所有地方都是并發(fā)執(zhí)行的,除了初始標記的是STW。所以停頓時間幾乎就耗費在初始標記上,這部分的實際時間是非常少的。吞吐量的測試數(shù)據(jù)如下圖所示: 在ZGC的強項停頓時間測試上,它毫不留情地將Parallel、G1拉開了兩個數(shù)量級的差距。無論平均停頓、95%停頓、99%停頓、99.9%停頓,還是最大停頓時間,ZGC都能毫不費勁地控制在10ms以內(nèi)。

雖然ZGC還在試驗狀態(tài),沒有完成所有特性,但此時性能已經(jīng)相當亮眼,用“令人震驚、革命性”來形容,不為過。未來將在服務端、大內(nèi)存、低延遲應用的首選垃圾收集器。JDK14之前,ZGC僅Linux才支持,盡管許多使用ZGC的用戶都使用類Linux的環(huán)境,但在Windows和macOS上,人們也需要ZGC進行開發(fā)部署和測試,在JDK14中ZGC擴展了在macOS和Windows平臺上的應用。許多桌面應用也可以從ZGC中受益。想要使用ZGC,可以通過如下參數(shù)實現(xiàn):

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

7.2、Shenandoah GC

2019年3月,JDK12發(fā)布,另一種實驗性GC被引入,它就是Shenandoah GC,也是一種低停頓時間的GC。

但是Shenandoah無疑是眾多GC中最孤獨的一個。因為它是第一款不由Oracle公司團隊領導開發(fā)的HotSpot垃圾收集器,不可避免地受到官方的排擠。Shenandoah垃圾收集器最初由RedHat進行的一項垃圾收集器研究項目Pauseless GC實現(xiàn),旨在針對JVM上的內(nèi)存回收實現(xiàn)低停頓的需求,在2014年貢獻給OpenJDK,但是OracleJDK目前還未正式接納Shenandoah GC。

Shenandoah GC和ZGC一樣都是強調(diào)低停頓時間的GC。Shenandoah研發(fā)團隊對外宣稱,Shenandoah垃圾收集器的停頓時間與堆大小無關,這意味著無論將堆設置為200 MB還是200GB,99.9%的目標都可以把垃圾收集的停頓時間限制在10ms以內(nèi)。不過實際使用性能將取決于實際工作堆的大小和工作負載。RedHat在2016年使用Elastic Search對200GB的維基百科數(shù)據(jù)進行索引,如下表所示是論文中展示的測試數(shù)據(jù)。 從結果可以發(fā)現(xiàn): (1)Shenandoah停頓時間比其他幾款收集器確實有了質(zhì)的飛躍,但也未實現(xiàn)最大停頓時間控制在10ms以內(nèi)的目標。 (2)Shenandoah吞吐量方面出現(xiàn)了明顯的下降,總運行時間是所有測試收集器里最長的。

Shenandoah的弱項是高運行負擔下的吞吐量下降。Shenandoah的強項是低延遲時間。Shenandoah的工作過程大致分為九個階段,這里就不再過多介紹了。

8、總結

每一款不同的垃圾收集器都有不同的特點,在具體使用的時候,需要根據(jù)具體的情況選用不同的垃圾收集器,目前主流垃圾收集器的特點對比如下表所示:

垃圾收集器從Serial發(fā)展到ZGC,經(jīng)歷了很多不同的版本,Serial→Parallel(并行)→CMS(并發(fā))→G1→ZGC。不同廠商、不同版本的虛擬機實現(xiàn)差別很大。

Java垃圾收集器的配置對于JVM優(yōu)化來說是很重要的,選擇合適的垃圾收集器可以讓JVM的性能有一個很大的提升。怎么選擇垃圾收集器呢?我們可以參考下面的選擇標準:

優(yōu)先讓JVM自適應調(diào)整堆的大小。如果內(nèi)存小于100M,使用串行收集器。如果是單核、單機程序,并且沒有停頓時間的要求,串行收集器。如果是多CPU、需要高吞吐量、允許停頓時間超過1s,選擇并行或者JVM自己選擇。如果是多CPU、追求低停頓時間,需快速響應(比如延遲不能超過1s,如互聯(lián)網(wǎng)應用),使用并發(fā)收集器。

最后需要明確一個觀點,沒有最好的收集器,更沒有萬能的收集器。調(diào)優(yōu)永遠是針對特定場景、特定需求,不存在一勞永逸的收集器。

柚子快報激活碼778899分享:JVM垃圾收集——垃圾收集器

http://yzkb.51969.com/

好文閱讀

評論可見,查看隱藏內(nèi)容
大家都在看:

本文內(nèi)容根據(jù)網(wǎng)絡資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點和立場。

轉載請注明,如有侵權,聯(lián)系刪除。

本文鏈接:http://gantiao.com.cn/post/19307583.html

發(fā)布評論

您暫未設置收款碼

請在主題配置——文章設置里上傳

掃描二維碼手機訪問

文章目錄