前言
Java相較于其他編程語言更加容易學(xué)習(xí),這其中很大一部分原因要?dú)w功于JVM的自動內(nèi)存管理機(jī)制。
對于從事C語言的開發(fā)者來說,他們擁有每一個(gè)對象的「所有權(quán)」,更大的權(quán)力也意味著更多的職責(zé),C開發(fā)者需要維護(hù)每一個(gè)對象「從生到死」的過程,當(dāng)對象廢棄不用時(shí)必須手動釋放其內(nèi)存,否則就會發(fā)生內(nèi)存泄漏。而對于Java開發(fā)者來說,JVM的自動內(nèi)存管理機(jī)制解決了這個(gè)讓人頭疼的問題,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出的問題了,GC讓開發(fā)者更加專注于程序本身,而不用去關(guān)心內(nèi)存何時(shí)分配、何時(shí)回收、以及如何回收。
1. JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)
在聊GC前,有必要先了解一下JVM的內(nèi)存模型,知道JVM是如何規(guī)劃內(nèi)存的,以及GC的主要作用區(qū)域。

如圖所示,JVM運(yùn)行時(shí)會將內(nèi)存劃分為五大塊區(qū)域,其中「方法區(qū)」和「堆」隨著JVM的啟動而創(chuàng)建,是所有線程共享的內(nèi)存區(qū)域。虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器則是隨著線程的創(chuàng)建被創(chuàng)建,線程運(yùn)行結(jié)束后也就被銷毀了。
1.1 程序計(jì)數(shù)器
程序計(jì)數(shù)器(Program Counter Register)是一塊非常小的內(nèi)存空間,幾乎可以忽略不計(jì)。
它可以看作是線程所執(zhí)行字節(jié)碼的行號指數(shù)器,指向當(dāng)前線程下一條應(yīng)該執(zhí)行的指令。對于:條件分支、循環(huán)、跳轉(zhuǎn)、異常等基礎(chǔ)功能都依賴于程序計(jì)數(shù)器。
對于CPU的一個(gè)核心來說,任意時(shí)刻只能跑一個(gè)線程。如果線程的CPU時(shí)間片用完就會被掛起,等待OS重新分配時(shí)間片再繼續(xù)執(zhí)行,那線程如何知道上次執(zhí)行到哪里了呢?就是通過程序計(jì)數(shù)器來實(shí)現(xiàn)的,每個(gè)線程都需要維護(hù)一個(gè)私有的程序計(jì)數(shù)器。
如果線程在執(zhí)行Java方法,計(jì)數(shù)器記錄的是JVM字節(jié)碼指令地址。如果執(zhí)行的是Native方法,計(jì)數(shù)器值則為Undefined。
程序計(jì)數(shù)器是唯一一個(gè)沒有規(guī)定任何OutOfMemoryError情況的內(nèi)存區(qū)域,意味著在該區(qū)域不可能發(fā)生OOM異常,GC不會對該區(qū)域進(jìn)行回收!
1.2 虛擬機(jī)棧
虛擬機(jī)棧(Java Virtual Machine Stacks)也是線程私有的,生命周期和線程相同。
虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型,JVM要執(zhí)行一個(gè)方法時(shí),首先會創(chuàng)建一個(gè)棧幀(Stack Frame)用于存放:局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。棧幀創(chuàng)建完畢后開始入棧執(zhí)行,方法執(zhí)行結(jié)束后即出棧。
方法執(zhí)行的過程就是一個(gè)個(gè)棧幀從入棧到出棧的過程。
局部變量表主要用來存放編譯器可知的各種基本數(shù)據(jù)類型、對象引用、returnAddress類型。局部變量表所需的內(nèi)存空間在編譯時(shí)就已經(jīng)確認(rèn),運(yùn)行期間不會修改局部變量表的大小。
在JVM規(guī)范中,虛擬機(jī)棧規(guī)定了兩種異常:
- StackOverflowError
線程請求的棧深度大于JVM所允許的棧深度。
棧的容量是有限的,如果線程入棧的棧幀超過了限制就會拋出StackOverflowError異常,例如:方法遞歸。 - OutOfMemoryError
虛擬機(jī)棧是可以動態(tài)擴(kuò)展的,如果擴(kuò)展時(shí)無法申請到足夠的內(nèi)存,則會拋出OOM異常。
1.3. 本地方法棧
本地方法棧(Native Method Stack)也是線程私有的,與虛擬機(jī)棧的作用非常類似。
區(qū)別是虛擬機(jī)棧是為執(zhí)行Java方法服務(wù)的,而本地方法棧是為執(zhí)行Native方法服務(wù)的。
與虛擬機(jī)棧一樣,JVM規(guī)范中對本地方法棧也規(guī)定了StackOverflowError和OutOfMemoryError兩種異常。
1.4. Java堆
Java堆(Java Heap)是線程共享的,一般來說也是JVM管理最大的一塊內(nèi)存區(qū)域,同時(shí)也是垃圾收集器GC的主要管理區(qū)域。
Java堆在JVM啟動時(shí)創(chuàng)建,作用是:存放對象實(shí)例。
幾乎所有的對象都在堆中創(chuàng)建,但是隨著JIT編譯器的發(fā)展和逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)使得“所有對象都分配在堆上”不那么絕對了。
由于是GC主要管理的區(qū)域,所以也被稱為:GC堆。
為了GC的高效回收,Java堆內(nèi)部又做了如下劃分:
JVM規(guī)范中,堆在物理上可以是不連續(xù)的,只要邏輯上連續(xù)即可。通過-Xms -Xmx參數(shù)可以設(shè)置最小、最大堆內(nèi)存。
1.5. 方法區(qū)
方法區(qū)(Method Area)與Java堆一樣,也是線程共享的一塊內(nèi)存區(qū)域。
它主要用來存儲:被JVM加載的類信息,常量,靜態(tài)變量,即時(shí)編譯器產(chǎn)生的代碼等數(shù)據(jù)。
也被稱為:非堆(Non-Heap),目的是與Java堆區(qū)分開來。
JVM規(guī)范對方法區(qū)的限制比較寬松,JVM甚至可以不對方法區(qū)進(jìn)行垃圾回收。這就導(dǎo)致在老版本的JDK中,方法區(qū)也別稱為:永久代(PermGen)。
使用永久代來實(shí)現(xiàn)方法區(qū)不是個(gè)好主意,容易導(dǎo)致內(nèi)存溢出,于是從JDK7開始有了“去永久代”行動,將原本放在永久代中的字符串常量池移出。到JDK8中,正式去除永久代,迎來元空間。
2. GC概述
垃圾收集(Garbage Collection)簡稱為「GC」,它的歷史遠(yuǎn)比Java語言本身久遠(yuǎn),在1960年誕生于麻省理工學(xué)院的Lisp是第一門開始使用內(nèi)存動態(tài)分配和垃圾收集技術(shù)的語言。
要想實(shí)現(xiàn)自動垃圾回收,首先需要思考三件事情:
前面介紹了JVM的五大內(nèi)存區(qū)域,程序計(jì)數(shù)器占用內(nèi)存極少,幾乎可以忽略不計(jì),而且永遠(yuǎn)不會內(nèi)存溢出,GC不需要對其進(jìn)行回收。虛擬機(jī)棧、本地方法棧隨線程“同生共死”,棧中的棧幀隨著方法的運(yùn)行有條不紊的入棧、出棧,每個(gè)棧幀分配多少內(nèi)存在編譯期就已經(jīng)基本確定,因此這兩塊區(qū)域內(nèi)存的分配和回收都具備確定性,不太需要考慮如何回收的問題。
方法區(qū)就不一樣了,一個(gè)接口到底有多少個(gè)實(shí)現(xiàn)類?每個(gè)類占用的內(nèi)存是多少?你甚至可以在運(yùn)行時(shí)動態(tài)的創(chuàng)建類,因此GC需要針對方法區(qū)進(jìn)行回收。
Java堆也是如此,堆中存放著幾乎所有的Java對象實(shí)例,一個(gè)類到底會創(chuàng)建多少個(gè)對象實(shí)例,只有在程序運(yùn)行時(shí)才知道,這部分內(nèi)存的分配和回收是動態(tài)的,GC需要重點(diǎn)關(guān)注。
2.1 哪些對象需要回收
實(shí)現(xiàn)自動垃圾回收的第一步,就是判斷到底哪些對象是可以被回收的。一般來說有兩種方式:引用計(jì)數(shù)算法和可達(dá)性分析算法,商用JVM幾乎采用的都是后者。
2.1.1 引用計(jì)數(shù)算法
在對象中添加一個(gè)引用計(jì)數(shù)器,每引用一次計(jì)數(shù)器就加1,每取消一次引用計(jì)數(shù)器就減1,當(dāng)計(jì)數(shù)器為0時(shí)表示對象不再被引用,此時(shí)就可以將對象回收了。
引用計(jì)數(shù)算法(Reference Counting)雖然占用了一些額外的內(nèi)存空間,但是它原理簡單,也很高效,在大多數(shù)情況下是一個(gè)不錯(cuò)的實(shí)現(xiàn)方案,但是它存在一個(gè)嚴(yán)重的弊端:無法解決循環(huán)引用。
例如一個(gè)鏈表,按理只要沒有引用指向鏈表,鏈表就應(yīng)該被回收,但是很遺憾,由于鏈表中所有的元素引用計(jì)數(shù)器都不為0,因此無法被回收,造成內(nèi)存泄漏。
2.1.2 可達(dá)性分析算法
目前主流的商用JVM都是通過可達(dá)性分析來判斷對象是否可以被回收的。
這個(gè)算法的基本思路是:
通過一系列被稱為「GC Roots」的根對象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,通過引用關(guān)系向下搜尋,搜尋走過的路徑稱為「引用鏈」,如果某個(gè)對象到GC Roots沒有任何引用鏈相連,就說明該對象不可達(dá),即可以被回收。
對象可達(dá)指的就是:雙方存在直接或間接的引用關(guān)系。
根可達(dá)或GC Roots可達(dá)就是指:對象到GC Roots存在直接或間接的引用關(guān)系。
可以作為GC Roots的對象有以下幾類:
可達(dá)性分析就是JVM首先枚舉根節(jié)點(diǎn),找到一些為了保證程序能正常運(yùn)行所必須要存活的對象,然后以這些對象為根,根據(jù)引用關(guān)系開始向下搜尋,存在直接或間接引用鏈的對象就存活,不存在引用鏈的對象就回收。
關(guān)于可達(dá)性分析的詳細(xì)描述,可以看筆者的文章:《大白話理解可達(dá)性分析算法》。
2.2 何時(shí)回收
JVM將內(nèi)存劃分為五大塊區(qū)域,不同的GC會針對不同的區(qū)域進(jìn)行垃圾回收,GC類型一般有以下幾大類:
-
Minor GC
也被稱為“Young GC”、“輕GC”,只針對新生代進(jìn)行的垃圾回收。 -
Major GC
也被稱為“Old GC”,只針對老年代進(jìn)行的垃圾回收。 -
Mixed GC
混合GC,針對新生代和部分老年代進(jìn)行垃圾回收,部分垃圾收集器才支持。 -
Full GC
整堆GC、重GC,針對整個(gè)Java堆和方法區(qū)進(jìn)行的垃圾回收,耗時(shí)最久的GC。
什么時(shí)候觸發(fā)GC,以及觸發(fā)什么類型的GC呢?不同的垃圾收集器實(shí)現(xiàn)不一樣,你還可以通過設(shè)置參數(shù)來影響JVM的決策。
一般來說,新生代會在Eden區(qū)用盡后才會觸發(fā)GC,而Old區(qū)卻不能這樣,因?yàn)橛械牟l(fā)收集器在清理過程中,用戶線程可以繼續(xù)運(yùn)行,這意味著程序仍然在創(chuàng)建對象、分配內(nèi)存,這就需要老年代進(jìn)行「空間分配擔(dān)保」,新生代放不下的對象會被放入老年代,如果老年代的回收速度比對象的創(chuàng)建速度慢,就會導(dǎo)致「分配擔(dān)保失敗」,這時(shí)JVM不得不觸發(fā)Full GC,以此來獲取更多的可用內(nèi)存。
2.3 如何回收
定位到需要回收的對象以后,就要開始進(jìn)行回收了。如何回收對象又成了一個(gè)問題。
什么樣的回收方式會更加的高效呢?回收后是否需要對內(nèi)存進(jìn)行壓縮整理,避免碎片化呢?針對這些問題,GC的回收算法大致分為以下三類:
- 標(biāo)記-清除算法
- 標(biāo)記-復(fù)制算法
- 標(biāo)記-整理算法
具體算法的回收細(xì)節(jié),下面會介紹到。
3. GC回收算法
JVM將堆劃分成不同的代,不同的代中存放的對象特點(diǎn)不一樣,針對不同的代使用不同的GC回收算法進(jìn)行回收可以提升GC的效率。
3.1 分代收集理論
目前大多數(shù)JVM的垃圾收集器都遵循“分代收集”理論,分代收集理論建立在三個(gè)假說之上。
3.1.1 弱分代假說
絕大多數(shù)對象都是朝生夕死的。
想想看我們寫的程序是不是這樣,絕大多數(shù)時(shí)候,我們創(chuàng)建一個(gè)對象,只是為了進(jìn)行一些業(yè)務(wù)計(jì)算,得到計(jì)算結(jié)果后這個(gè)對象也就沒什么用了,即可以被回收了。
再例如:客戶端要求返回一個(gè)列表數(shù)據(jù),服務(wù)端從數(shù)據(jù)庫查詢后轉(zhuǎn)換成JSON響應(yīng)給前端后,這個(gè)列表的數(shù)據(jù)就可以被回收了。
諸如此類,都可以被稱為「朝生夕死」的對象。
3.1.2 強(qiáng)分代假說
熬過越多次GC的對象就越難以回收。
這個(gè)假說完全是基于概率學(xué)統(tǒng)計(jì)來的,經(jīng)歷過多次GC都無法被回收的對象,可以假定它下次GC時(shí)仍然無法被回收,因此就沒必要高頻率的對其進(jìn)行回收,將其挪到老年代,減少回收的頻率,讓GC去回收效益更高的新生代。
3.1.3 跨代引用假說
跨代引用相對于同代引用是極少的。
這是根據(jù)前兩條假說邏輯推理得出的隱含推論:存在互相引用關(guān)系的兩個(gè)對象,應(yīng)該傾向于同時(shí)生存或者同時(shí)消亡的。
舉個(gè)例子,如果某個(gè)新生代對象存在跨代引用,由于老年代對象難以消亡,該引用會使得新生代對象在收集時(shí)同樣得以存活,進(jìn)而在年齡增長之后晉升到老年代中,這時(shí)跨代引用也隨即被消除了。
3.2 解決跨代引用
跨代引用雖然極少,但是它還是可能存在的。如果為了極少的跨代引用而去掃描整個(gè)老年代,那每次GC的開銷就太大了,GC的暫停時(shí)間會變得難以接受。如果忽略跨代引用,會導(dǎo)致新生代的對象被錯(cuò)誤的回收,導(dǎo)致程序錯(cuò)誤。
3.2.1 Remembered Set
JVM是通過記憶集(Remembered Set)來解決的,通過在新生代建立記憶集的數(shù)據(jù)結(jié)構(gòu),來避免回收新生代時(shí)把整個(gè)老年代也加進(jìn)GC Roots的掃描范圍,減少GC的開銷。
記憶集是一種由「非收集區(qū)域」指向「收集區(qū)域」的指針集合的抽象數(shù)據(jù)結(jié)構(gòu),說白了就是把「年輕代中被老年代引用的對象」給標(biāo)記起來。記憶集可以有以下三種記錄精度:
- 字長精度:記錄精確到一個(gè)機(jī)器字長,也就是處理器的尋址位數(shù)。
- 對象精度:精確到對象,對象的字段是否存在跨代引用指針。
- 卡精度:精確到一塊內(nèi)存區(qū)域,該區(qū)域內(nèi)的對象是否存在跨代引用。
字長精度和對象精度太精細(xì)化了,需要花費(fèi)大量的內(nèi)存來維護(hù)記憶集,因此許多JVM都是采用的「卡精度」,也被稱作:“卡表”(Card Table)。卡表是記憶集的一種實(shí)現(xiàn),也是目前最常用的一種形式,它定義了記憶集的記錄精度、與對內(nèi)存的映射關(guān)系等。
HotSpot使用一個(gè)字節(jié)數(shù)組來實(shí)現(xiàn)卡表,它將堆空間劃分成一系列2次冪大小的內(nèi)存區(qū)域,這個(gè)內(nèi)存區(qū)域就被稱作「卡頁」(Card Page),卡頁的大小一般都是2的冪次方數(shù),HotSpot采用2的9次冪,即512字節(jié)。字節(jié)數(shù)組的每一個(gè)元素都對應(yīng)著一個(gè)卡頁,如果某個(gè)卡頁內(nèi)的對象存在跨代引用,JVM就會將這個(gè)卡頁標(biāo)記為「Dirty」臟的,GC時(shí)只需要掃描臟頁對應(yīng)的內(nèi)存區(qū)域即可,避免掃描整個(gè)堆。
卡表的結(jié)構(gòu)如下圖所示:
3.2.2 寫屏障
卡表只是用來標(biāo)記哪一塊內(nèi)存區(qū)域存在跨代引用的數(shù)據(jù)結(jié)構(gòu),JVM如何來維護(hù)卡表呢?什么時(shí)候?qū)⒖撟兣K呢?
HotSpot是通過「寫屏障」(Write Barrier)來維護(hù)卡表的,JVM攔截了「對象屬性賦值」這個(gè)動作,類似于AOP的切面編程,JVM可以在對象屬性賦值前后介入處理,賦值前的處理叫作「寫前屏障」,賦值后的處理叫作「寫后屏障」,偽代碼如下:
void setField(Object o){
before();//寫前屏障
this.field = o;
after();//寫后屏障
}
開啟寫屏障后,JVM會為所有的賦值操作生成相應(yīng)的指令,一旦出現(xiàn)老年代對象的引用指向了年輕代的對象,HotSpot就會將對應(yīng)的卡表元素置為臟的。
請將這里的「寫屏障」和并發(fā)編程中內(nèi)存指令重排序的「寫屏障」區(qū)分開,避免混淆。
除了寫屏障本身的開銷外,卡表在高并發(fā)場景下還面臨著「偽共享」的問題,現(xiàn)代CPU的緩存系統(tǒng)是以「緩存行」(Cache Line)為單位存儲的,Intel的CPU緩存行的大小一般是64字節(jié),多線程修改互相獨(dú)立的變量時(shí),如果這些變量在同一個(gè)緩存行中,就會導(dǎo)致彼此的緩存行無故失效,線程不得不頻繁發(fā)起load指令重新加載數(shù)據(jù),而導(dǎo)致性能降低。
一個(gè)Cache Line是64字節(jié),每個(gè)卡頁是512字節(jié),64??512字節(jié)就是32KB,如果不同的線程更新的對象處在這32KB之內(nèi),就會導(dǎo)致更新卡表時(shí)正好寫入同一個(gè)緩存行而影響性能。為了避免這個(gè)問題,HotSpot支持只有當(dāng)元素未被標(biāo)記時(shí),才將其置為臟的,這樣會增加一次判斷,但是可以避免偽共享的問題,設(shè)置-XX:+UseCondCardMark來開啟這個(gè)判斷。
3.3 標(biāo)記清除
標(biāo)記清除算法分為兩個(gè)過程:標(biāo)記、清除。
收集器首先標(biāo)記需要被回收的對象,標(biāo)記完成后統(tǒng)一清除。也可以標(biāo)記存活對象,然后統(tǒng)一清除沒有被標(biāo)記的對象,這取決于內(nèi)存中存活對象和死亡對象的占比。
缺點(diǎn):
- 執(zhí)行效率不穩(wěn)定
標(biāo)記和清除的時(shí)間消耗隨著Java堆中的對象不斷增加而增加。 - 內(nèi)存碎片
標(biāo)記清除后內(nèi)存會產(chǎn)生大量不連續(xù)的空間碎片,不利于后續(xù)繼續(xù)為新生對象分配內(nèi)存。
3.4 標(biāo)記復(fù)制
為了解決標(biāo)記清除算法產(chǎn)生的內(nèi)存碎片問題,標(biāo)記復(fù)制算法進(jìn)行了改進(jìn)。
標(biāo)記復(fù)制算法會將內(nèi)存劃分為兩塊區(qū)域,每次只使用其中一塊,垃圾回收時(shí)首先進(jìn)行標(biāo)記,標(biāo)記完成后將存活的對象復(fù)制到另一塊區(qū)域,然后將當(dāng)前區(qū)域全部清理。
缺點(diǎn)是:如果大量對象無法被回收,會產(chǎn)生大量的內(nèi)存復(fù)制開銷??捎脙?nèi)存縮小為一半,內(nèi)存浪費(fèi)也比較大。
由于絕大多數(shù)對象都會在第一次GC時(shí)被回收,需要被復(fù)制的往往是極少數(shù)對象,那么就完全沒必要按照1:1去劃分空間。
HotSpot虛擬機(jī)默認(rèn)Eden區(qū)和Survivor區(qū)的大小比例是8:1,即Eden區(qū)80%,F(xiàn)rom Survivor區(qū)10%,To Survivor區(qū)10%,整個(gè)新生代可用內(nèi)存為Eden區(qū)+一個(gè)Survivor區(qū)即90%,另一個(gè)Survivor區(qū)10%用于分區(qū)復(fù)制。
如果Minor GC后仍存活大量對象,超出了一個(gè)Survivor區(qū)的范圍,那么就會進(jìn)行分配擔(dān)保(Handle Promotion),將對象直接分配進(jìn)老年代。
3.5 標(biāo)記整理
標(biāo)記復(fù)制算法除了在對象大量存活時(shí)需要進(jìn)行較多的復(fù)制操作外,還需要額外的內(nèi)存空間老年代來進(jìn)行分配擔(dān)保,所以在老年代中一般不采用這種回收算法。
能夠在老年代中存活的對象,一般都是歷經(jīng)多次GC后仍無法被回收的對象,基于“強(qiáng)分代假說”,老年代中的對象一般很難被回收。針對老年代對象的生存特征,引入了標(biāo)記整理算法。
標(biāo)記整理算法的標(biāo)記過程與標(biāo)記清除算法一致,但是標(biāo)記整理算法不會像標(biāo)記清除算法一樣直接清理標(biāo)記的對象,而是將存活的對象都向內(nèi)存區(qū)域的一端移動,然后直接清理掉邊界外的內(nèi)存空間。
標(biāo)記整理算法相較于標(biāo)記清除算法,最大的區(qū)別是:需要移動存活的對象。
GC時(shí)移動存活的對象既有優(yōu)點(diǎn),也有缺點(diǎn)。
缺點(diǎn)
基于“強(qiáng)分代假說”,大部分情況下老年代GC后會存活大量對象,移動這些對象需要更新所有reference引用地址,這是一項(xiàng)開銷極大的操作,而且該操作需要暫停所有用戶線程,即程序此時(shí)會阻塞停頓,JVM稱這種停頓為:Stop The World(STW)。
優(yōu)點(diǎn)
移動對象對內(nèi)存空間進(jìn)行整理后,不會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,利于后續(xù)為對象分配內(nèi)存。
由此可見,不管是否移動對象都有利弊。移動則內(nèi)存回收時(shí)負(fù)責(zé)、內(nèi)存分配時(shí)簡單,不移動則內(nèi)存回收時(shí)簡單、內(nèi)存分配時(shí)復(fù)雜。從整個(gè)程序的吞吐量來考慮,移動對象顯然更劃算一些,因?yàn)閮?nèi)存分配的頻率比內(nèi)存回收的頻率要高的多的多。
還有一種解決方式是:平時(shí)不移動對象,采用標(biāo)記清除算法,當(dāng)內(nèi)存碎片影響到大對象分配時(shí),才啟用標(biāo)記整理算法。
4. 垃圾收集器
按照《Java虛擬機(jī)規(guī)范》實(shí)現(xiàn)的JVM就不勝枚舉,且每個(gè)JVM平臺都有N個(gè)垃圾收集器供用戶選擇,這些不是一篇文章可以說的清楚的。當(dāng)然,開發(fā)者也沒必要了解所有的垃圾收集器,以Hotspot JVM為例,主流的垃圾收集器主要有以下幾大類:
串行:單線程收集,用戶線程暫停。
并行:多線程收集,用戶線程暫停。
并發(fā):用戶線程和GC線程同時(shí)運(yùn)行。
前面已經(jīng)說過,大多數(shù)JVM的垃圾收集器都遵循“分代收集”理論,不同的垃圾收集器回收的內(nèi)存區(qū)域會有所不同,大多數(shù)情況下,JVM需要兩個(gè)垃圾收集器配合使用,下圖有虛線連接的代表兩個(gè)收集器可以配合使用。
4.1 新生代收集器
4.1.1 Serial
最基礎(chǔ),最早的垃圾收集器,采用標(biāo)記復(fù)制算法,僅開啟一個(gè)線程完成垃圾回收,回收時(shí)會暫停所有用戶線程(STW)。
使用
-XX:+UseSerialGC參數(shù)開啟Serial收集器,由于是單線程回收,因此Serial的應(yīng)用范圍很受限制:
- 應(yīng)用程序很輕量,堆空間不到百M(fèi)B。
- 服務(wù)器CPU資源緊張。
4.1.2 Parallel Scavenge
使用標(biāo)記復(fù)制算法,多線程的新生代收集器。
使用參數(shù)
-XX:+UseParallelGC開啟,ParallelGC的特點(diǎn)是非常關(guān)注系統(tǒng)的吞吐量,它提供了兩個(gè)參數(shù)來由用戶控制系統(tǒng)的吞吐量:-XX:MaxGCPauseMillis:設(shè)置垃圾回收最大的停頓時(shí)間,它必須是一個(gè)大于0的整數(shù),ParallelGC會朝著這個(gè)目標(biāo)去努力,如果這個(gè)值設(shè)置的過小,ParallelGC就不一定能保證了。如果用戶希望GC停頓的時(shí)間很短,ParallelGC就會嘗試減小堆空間,因?yàn)榛厥找粋€(gè)較小的堆肯定比回收一個(gè)較大的堆耗時(shí)短嘛,但是這樣會更頻繁的觸發(fā)GC,從而降低系統(tǒng)的吞吐量。
-XX:GCTimeRatio:設(shè)置吞吐量的大小,它的值是一個(gè)0~100的整數(shù)。假設(shè)GCTimeRatio為n,那么ParallelGC將花費(fèi)不超過1/(1+n)的時(shí)間進(jìn)行垃圾回收,默認(rèn)值為19,意味著ParallelGC用于垃圾回收的時(shí)間不會超過5%。
ParallelGC是JDK8的默認(rèn)垃圾收集器,它是一款吞吐量優(yōu)先的垃圾收集器,用戶可以通過-XX:MaxGCPauseMillis和-XX:GCTimeRatio來設(shè)置GC最大的停頓時(shí)間和吞吐量。但這兩個(gè)參數(shù)是互相矛盾的,更小的停頓時(shí)間就意味著GC需要更頻繁進(jìn)行回收,從而增加GC回收的整體時(shí)間,導(dǎo)致吞吐量下降。
4.1.3 ParNew
ParNew也是一個(gè)使用標(biāo)記復(fù)制算法,多線程的新生代垃圾收集器。它的回收策略、算法、及參數(shù)都和Serial一樣,只是簡單的將單線程改為多線程而已,它的誕生只是為了配合CMS收集器使用而存在的。CMS是老年代的收集器,但是Parallel Scavenge不能配合CMS一起工作,Serial是串行回收的,效率又太低了,因此ParNew就誕生了。
使用參數(shù)-XX:+UseParNewGC開啟,不過這個(gè)參數(shù)已經(jīng)在JDK9之后的版本中刪除了,因?yàn)镴DK9默認(rèn)G1收集器,CMS已經(jīng)被取代,而ParNew就是為了配合CMS而誕生的,CMS廢棄了,ParNew也就沒有存在價(jià)值了。
4.2 老年代收集器
4.2.1 Serial Old
使用標(biāo)記整理算法,和Serial一樣,單線程獨(dú)占式的針對老年代的垃圾收集器。老年代的空間通常比新生代要大,而且標(biāo)記整理算法在回收過程中需要移動對象來避免內(nèi)存碎片化,因此老年代的回收要比新生代更耗時(shí)一些。
Serial Old作為最早的老年代垃圾收集器,還有一個(gè)優(yōu)勢,就是它可以和絕大多數(shù)新生代垃圾收集器配合使用,同時(shí)它還可以作為CMS并發(fā)失敗的備用收集器。
使用參數(shù)-XX:+UseSerialGC開啟,新生代老年代都將使用串行收集器。和Serial一樣,除非你的應(yīng)用非常輕量,或者CPU的資源十分緊張,否則都不建議使用該收集器。
4.2.2 Parallel Old
ParallelOldGC是一款針對老年代,多線程并行的獨(dú)占式垃圾收集器,和Parallel Scavenge一樣,屬于吞吐量優(yōu)先的收集器,Parallel Old的誕生就是為了配合Parallel Scavenge使用的。
ParallelOldGC使用的是標(biāo)記整理算法,使用參數(shù)-XX:+UseParallelOldGC開啟,參數(shù)-XX:ParallelGCThreads=n可以設(shè)置垃圾收集時(shí)開啟的線程數(shù)量,同時(shí)它也是JDK8默認(rèn)的老年代收集器。
4.2.3 CMS
CMS(Concurrent Mark Sweep)是一款里程碑式的垃圾收集器,為什么這么說呢?因?yàn)樵谒?,GC線程和用戶線程是無法同時(shí)工作的,即使是Parallel Scavenge,也不過是GC時(shí)開啟多個(gè)線程并行回收而已,GC的整個(gè)過程依然要暫停用戶線程,即Stop The World。這帶來的后果就是Java程序運(yùn)行一段時(shí)間就會卡頓一會,降低應(yīng)用的響應(yīng)速度,這對于運(yùn)行在服務(wù)端的程序是不能被接收的。
GC時(shí)為什么要暫停用戶線程?
首先,如果不暫停用戶線程,就意味著期間會不斷有垃圾產(chǎn)生,永遠(yuǎn)也清理不干凈。
其次,用戶線程的運(yùn)行必然會導(dǎo)致對象的引用關(guān)系發(fā)生改變,這就會導(dǎo)致兩種情況:漏標(biāo)和錯(cuò)標(biāo)。
- 漏標(biāo)
原本不是垃圾,但是GC的過程中,用戶線程將其引用關(guān)系修改,導(dǎo)致GC Roots不可達(dá),成為了垃圾。這種情況還好一點(diǎn),無非就是產(chǎn)生了一些浮動垃圾,下次GC再清理就好了。 - 錯(cuò)標(biāo)
原本是垃圾,但是GC的過程中,用戶線程將引用重新指向了它,這時(shí)如果GC一旦將其回收,將會導(dǎo)致程序運(yùn)行錯(cuò)誤。
為了實(shí)現(xiàn)并發(fā)收集,CMS的實(shí)現(xiàn)比前面介紹的幾種垃圾收集器都要復(fù)雜的多,整個(gè)GC過程可以大概分為以下四個(gè)階段:
1、初始標(biāo)記
初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快。初始標(biāo)記的過程是需要觸發(fā)STW的,不過這個(gè)過程非常快,而且初試標(biāo)記的耗時(shí)不會因?yàn)槎芽臻g的變大而變慢,是可控的,因此可以忽略這個(gè)過程導(dǎo)致的短暫停頓。
2、并發(fā)標(biāo)記
并發(fā)標(biāo)記就是將初始標(biāo)記的對象進(jìn)行深度遍歷,以這些對象為根,遍歷整個(gè)對象圖,這個(gè)過程耗時(shí)較長,而且標(biāo)記的時(shí)間會隨著堆空間的變大而變長。不過好在這個(gè)過程是不會觸發(fā)STW的,用戶線程仍然可以工作,程序依然可以響應(yīng),只是程序的性能會受到一點(diǎn)影響。因?yàn)镚C線程會占用一定的CPU和系統(tǒng)資源,對處理器比較敏感。CMS默認(rèn)開啟的GC線程數(shù)是:(CPU核心數(shù)+3)/4,當(dāng)CPU核心數(shù)超過4個(gè)時(shí),GC線程會占用不到25%的CPU資源,如果CPU數(shù)不足4個(gè),GC線程對程序的影響就會非常大,導(dǎo)致程序的性能大幅降低。
3、重新標(biāo)記
由于并發(fā)標(biāo)記時(shí),用戶線程仍在運(yùn)行,這意味著并發(fā)標(biāo)記期間,用戶線程有可能改變了對象間的引用關(guān)系,可能會發(fā)生兩種情況:一種是原本不能被回收的對象,現(xiàn)在可以被回收了,另一種是原本可以被回收的對象,現(xiàn)在不能被回收了。針對這兩種情況,CMS需要暫停用戶線程,進(jìn)行一次重新標(biāo)記。
4、并發(fā)清理
重新標(biāo)記完成后,就可以并發(fā)清理了。這個(gè)過程耗時(shí)也比較長,且清理的開銷會隨著堆空間的變大而變大。不過好在這個(gè)過程也是不需要STW的,用戶線程依然可以正常運(yùn)行,程序不會卡頓,不過和并發(fā)標(biāo)記一樣,清理時(shí)GC線程依然要占用一定的CPU和系統(tǒng)資源,會導(dǎo)致程序的性能降低。
CMS開辟了并發(fā)收集的先河,讓用戶線程和GC線程同時(shí)工作成為了可能,但是缺點(diǎn)也很明顯:
1、對處理器敏感
并發(fā)標(biāo)記、并發(fā)清理階段,雖然CMS不會觸發(fā)STW,但是標(biāo)記和清理需要GC線程介入處理,GC線程會占用一定的CPU資源,進(jìn)而導(dǎo)致程序的性能下降,程序響應(yīng)速度變慢。CPU核心數(shù)多的話還稍微好一點(diǎn),CPU資源緊張的情況下,GC線程對程序的性能影響非常大。
2、浮動垃圾
并發(fā)清理階段,由于用戶線程仍在運(yùn)行,在此期間用戶線程制造的垃圾就被稱為“浮動垃圾”,浮動垃圾本次GC無法清理,只能留到下次GC時(shí)再清理。
3、并發(fā)失敗
由于浮動垃圾的存在,因此CMS必須預(yù)留一部分空間來裝載這些新產(chǎn)生的垃圾。CMS不能像Serial Old收集器那樣,等到Old區(qū)填滿了再來清理。在JDK5時(shí),CMS會在老年代使用了68%的空間時(shí)激活,預(yù)留了32%的空間來裝載浮動垃圾,這是一個(gè)比較偏保守的配置。如果實(shí)際引用中,老年代增長的不是太快,可以通過-XX:CMSInitiatingOccupancyFraction參數(shù)適當(dāng)調(diào)高這個(gè)值。到了JDK6,觸發(fā)的閾值就被提升至92%,只預(yù)留了8%的空間來裝載浮動垃圾。
如果CMS預(yù)留的內(nèi)存無法容納浮動垃圾,那么就會導(dǎo)致「并發(fā)失敗」,這時(shí)JVM不得不觸發(fā)預(yù)備方案,啟用Serial Old收集器來回收Old區(qū),這時(shí)停頓時(shí)間就變得更長了。
4、內(nèi)存碎片
由于CMS采用的是「標(biāo)記清除」算法,這就意味這清理完成后會在堆中產(chǎn)生大量的內(nèi)存碎片。內(nèi)存碎片過多會帶來很多麻煩,其一就是很難為大對象分配內(nèi)存。導(dǎo)致的后果就是:堆空間明明還有很多,但就是找不到一塊連續(xù)的內(nèi)存區(qū)域?yàn)榇髮ο蠓峙鋬?nèi)存,而不得不觸發(fā)一次Full GC,這樣GC的停頓時(shí)間又會變得更長。
針對這種情況,CMS提供了一種備選方案,通過-XX:CMSFullGCsBeforeCompaction參數(shù)設(shè)置,當(dāng)CMS由于內(nèi)存碎片導(dǎo)致觸發(fā)了N次Full GC后,下次進(jìn)入Full GC前先整理內(nèi)存碎片,不過這個(gè)參數(shù)在JDK9被棄用了。
4.2.3.1 三色標(biāo)記算法
介紹完CMS垃圾收集器后,我們有必要了解一下,為什么CMS的GC線程可以和用戶線程一起工作。
JVM判斷對象是否可以被回收,絕大多數(shù)采用的都是「可達(dá)性分析」算法,關(guān)于這個(gè)算法,可以查看筆者以前的文章:大白話理解可達(dá)性分析算法。
從GC Roots開始遍歷,可達(dá)的就是存活,不可達(dá)的就回收。
CMS將對象標(biāo)記為三種顏色:
標(biāo)記的過程大致如下:
- 剛開始,所有的對象都是白色,沒有被訪問。
- 將GC Roots直接關(guān)聯(lián)的對象置為灰色。
- 遍歷灰色對象的所有引用,灰色對象本身置為黑色,引用置為灰色。
- 重復(fù)步驟3,直到?jīng)]有灰色對象為止。
- 結(jié)束時(shí),黑色對象存活,白色對象回收。
這個(gè)過程正確執(zhí)行的前提是沒有其他線程改變對象間的引用關(guān)系,然而,并發(fā)標(biāo)記的過程中,用戶線程仍在運(yùn)行,因此就會產(chǎn)生漏標(biāo)和錯(cuò)標(biāo)的情況。
漏標(biāo)
假設(shè)GC已經(jīng)在遍歷對象B了,而此時(shí)用戶線程執(zhí)行了A.B=null的操作,切斷了A到B的引用。
本來執(zhí)行了
A.B=null之后,B、D、E都可以被回收了,但是由于B已經(jīng)變?yōu)榛疑?,它仍會被?dāng)做存活對象,繼續(xù)遍歷下去。最終的結(jié)果就是本輪GC不會回收B、D、E,留到下次GC時(shí)回收,也算是浮動垃圾的一部分。
實(shí)際上,這個(gè)問題依然可以通過「寫屏障」來解決,只要在A寫B(tài)的時(shí)候加入寫屏障,記錄下B被切斷的記錄,重新標(biāo)記時(shí)可以再把他們標(biāo)為白色即可。
錯(cuò)標(biāo)
假設(shè)GC線程已經(jīng)遍歷到B了,此時(shí)用戶線程執(zhí)行了以下操作:
B.D=null;//B到D的引用被切斷
A.xx=D;//A到D的引用被建立
B到D的引用被切斷,且A到D的引用被建立。
此時(shí)GC線程繼續(xù)工作,由于B不再引用D了,盡管A又引用了D,但是因?yàn)锳已經(jīng)標(biāo)記為黑色,GC不會再遍歷A了,所以D會被標(biāo)記為白色,最后被當(dāng)做垃圾回收。
可以看到錯(cuò)標(biāo)的結(jié)果比漏表嚴(yán)重的多,浮動垃圾可以下次GC清理,而把不該回收的對象回收掉,將會造成程序運(yùn)行錯(cuò)誤。
錯(cuò)標(biāo)只有在滿足下面兩種情況下才會發(fā)生:
- 灰色指向白色的引用全部斷開。
- 黑色指向白色的引用被建立。
只要打破任一條件,就可以解決錯(cuò)標(biāo)的問題。
原始快照和增量更新
原始快照打破的是第一個(gè)條件:當(dāng)灰色對象指向白色對象的引用被斷開時(shí),就將這條引用關(guān)系記錄下來。當(dāng)掃描結(jié)束后,再以這些灰色對象為根,重新掃描一次。相當(dāng)于無論引用關(guān)系是否刪除,都會按照剛開始掃描時(shí)那一瞬間的對象圖快照來掃描。
增量更新打破的是第二個(gè)條件:當(dāng)黑色指向白色的引用被建立時(shí),就將這個(gè)新的引用關(guān)系記錄下來,等掃描結(jié)束后,再以這些記錄中的黑色對象為根,重新掃描一次。相當(dāng)于黑色對象一旦建立了指向白色對象的引用,就會變?yōu)榛疑珜ο蟆?/p>
CMS采用的方案就是:寫屏障+增量更新來實(shí)現(xiàn)的,打破的是第二個(gè)條件。
當(dāng)黑色指向白色的引用被建立時(shí),通過寫屏障來記錄引用關(guān)系,等掃描結(jié)束后,再以引用關(guān)系里的黑色對象為根重新掃描一次即可。
偽代碼大致如下:
class A{
private D d;
public void setD(D d) {
writeBarrier(d);// 插入一條寫屏障
this.d = d;
}
private void writeBarrier(D d){
// 將A -> D的引用關(guān)系記錄下來,后續(xù)重新掃描
}
}
4.3 混合收集器
4.3.1 G1
G1的全稱是「Garbage First」垃圾優(yōu)先的收集器,JDK7正式使用,JDK9默認(rèn)使用,它的出現(xiàn)是為了替代CMS收集器。
既然要替代CMS,那么毫無疑問,G1也是并發(fā)并行的垃圾收集器,用戶線程和GC線程可以同時(shí)工作,關(guān)注的也是應(yīng)用的響應(yīng)時(shí)間。
G1最大的一個(gè)變化就是,它只是邏輯分代,物理結(jié)構(gòu)上已經(jīng)不分代了。它將整個(gè)Java堆劃分成多個(gè)大小不等的Region,每個(gè)Region可以根據(jù)需要扮演Eden區(qū)、Survivor區(qū)、或者是老年代空間,G1可以對扮演不同角色的Region采用不同的策略去處理。
G1之前的所有垃圾收集器,回收的范圍要么是整個(gè)新生代(Minor GC)、要么是整個(gè)老年代(Major GC)、再就是整個(gè)Java堆(Full GC)。而G1跳出了這個(gè)樊籠,它可以面向堆內(nèi)任何部分來組成回收集(Collection Set,簡稱CSet)進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪個(gè)分代,而是判斷哪個(gè)Region垃圾最多,選擇回收價(jià)值最高的Region回收,這也是「Garbage First」名稱的由來。
雖然G1仍然保留了分代的概念,但是新生代和老年代不再是固定不變的兩塊連續(xù)的內(nèi)存區(qū)域了,它們都是由一系列Region組成的,而且每次GC時(shí),新生代和老年代的空間大小會動態(tài)調(diào)整。G1之所以能控制GC的停頓時(shí)間,建立可預(yù)測的停頓時(shí)間模型,就是因?yàn)樗鼘egion作為單次回收的最小單元,每次回收的內(nèi)存空間都是Region大小的整數(shù)倍,這樣就可以避免在整個(gè)Java堆內(nèi)進(jìn)行全區(qū)域的垃圾收集。
G1會跟蹤每個(gè)Region的垃圾數(shù)量,計(jì)算每個(gè)Region的回收價(jià)值,在后臺維護(hù)一個(gè)優(yōu)先級列表,然后根據(jù)用戶設(shè)置的允許GC停頓的時(shí)間來優(yōu)先回收“垃圾最多”的Region,這樣就保證了G1能夠在有限的時(shí)間內(nèi)回收盡可能多的可用內(nèi)存。
G1的整個(gè)回收周期大概可以分為以下幾個(gè)階段:
- Eden區(qū)內(nèi)存耗盡,觸發(fā)新生代GC開始回收Eden區(qū)和Survivor區(qū)。新生代GC后,Eden區(qū)會被清空,Survivor區(qū)至少會保留一個(gè),其余的對象要么被清理,要么被晉升到老年代。這個(gè)過程中,新生代的大小可能會被調(diào)整。
- 并發(fā)標(biāo)記周期
2.1 初始標(biāo)記:僅標(biāo)記GC Roots直接關(guān)聯(lián)的對象,會伴隨一次新生代GC,且會導(dǎo)致STW。
2.2 根區(qū)域掃描:初始標(biāo)記時(shí)觸發(fā)的新生代GC會將Eden區(qū)清空,存活對象會移動到Survivor區(qū),這時(shí)就需要掃描由Survivor區(qū)直接可達(dá)的老年代區(qū)域,并標(biāo)記這些對象,這個(gè)過程可以并發(fā)執(zhí)行。
2.3 并發(fā)標(biāo)記:和CMS類似會掃描并查找整個(gè)堆內(nèi)存活的對象并標(biāo)記,不會觸發(fā)STW。
2.4 重新標(biāo)記:觸發(fā)STW,修正并發(fā)標(biāo)記期間因?yàn)橛脩艟€程繼續(xù)執(zhí)行而導(dǎo)致對象間的引用被改變。
2.5 獨(dú)占清理:觸發(fā)STW,計(jì)算各個(gè)Region的回收價(jià)值,對Region進(jìn)行排序,識別可供混合回收的區(qū)域。
2.6 并發(fā)清理:識別并清理完全空閑的Region,不會造成停頓。 - 混合回收:并發(fā)標(biāo)記周期中的并發(fā)清理階段,G1雖然也回收了部分空間,但是比例還是相當(dāng)?shù)偷?。但是在這之后,G1已經(jīng)明確知道各個(gè)Region的回收價(jià)值了。在混合回收階段G1會優(yōu)先回收垃圾最多的Region,這些Region既包含了新生代,也包含了老年代,故稱之為“混合回收”。被清理的Region內(nèi)的存活對象會被移動到其他Region,這也避免了內(nèi)存碎片。
和CMS一樣,因?yàn)椴l(fā)回收時(shí)用戶線程仍然在運(yùn)行,即分配內(nèi)存,因此如果回收速度跟不上內(nèi)存分配的速度,G1也會在必要的時(shí)候觸發(fā)一個(gè)Full GC來獲取更多的可用內(nèi)存。
使用參數(shù)-XX:+UseG1GC來開啟G1收集器,-XX:MaxGCPauseMillis來設(shè)置目標(biāo)最大停頓時(shí)間,G1會朝著這個(gè)目標(biāo)去努力,如果GC停頓時(shí)間超過了目標(biāo)時(shí)間,G1就會嘗試調(diào)整新生代和老年代的比例、堆大小、晉升年齡等一系列參數(shù)來企圖達(dá)到預(yù)設(shè)目標(biāo)。
-XX:ParallelGCThreads用來設(shè)置并行回收時(shí)GC的線程數(shù)量,-XX:InitiatingHeapOccupancyPercent用來指定整個(gè)Java堆的使用率達(dá)到多少時(shí)觸發(fā)并發(fā)標(biāo)記周期的執(zhí)行,默認(rèn)值是45。
4.3.2 面向未來的ZGC
ZGC是在JDK11才加入的具有實(shí)現(xiàn)性質(zhì)的低延遲垃圾收集器,它的目標(biāo)是希望在盡可能對吞吐量影響不大的前提下,實(shí)現(xiàn)在任意堆內(nèi)存大小下都可以把GC的停頓時(shí)間控制在十毫秒以內(nèi)。
ZGC面向的是超大堆,最大支持4TB的堆空間,它和G1一樣,也是采用Region的內(nèi)存布局形式。
ZGC最大的一個(gè)特點(diǎn)就是它采用著色指針Colored Pointer技術(shù)來標(biāo)記對象。以往,如果JVM需要在對象上存儲一些額外的、只供GC或JVM本身使用的數(shù)據(jù)時(shí)(如GC年齡、偏向線程ID、哈希碼),通常會在對象的對象頭上增加額外的字段來記錄。ZGC就厲害了,直接把標(biāo)記信息記錄在對象的引用指針上。
Colored Pointer是什么?為什么對象引用的指針本身也可以存儲數(shù)據(jù)呢?
在64位系統(tǒng)中,理論上可以訪問的內(nèi)存大小為2的64次冪字節(jié),即16EB。但是實(shí)際上,目前遠(yuǎn)遠(yuǎn)用不到這么大的內(nèi)存,因此基于性能和成本的考慮,CPU和操作系統(tǒng)都會施加自己的約束。例如AMD64架構(gòu)只支持54位(4PB)的地址總線,Linux只支持46位(64TB)的物理地址總線,Windows只支持44位(16TB)的物理地址總線。
在Linux系統(tǒng)下,高18位不能用來尋址,剩余的46位能支持最大64TB的內(nèi)存大小。事實(shí)上,64TB的內(nèi)存大小在目前來說也遠(yuǎn)遠(yuǎn)超出了服務(wù)器的需要。于是ZGC就盯上了這剩下的46位指針寬度,將其高4位提取出來存儲四個(gè)標(biāo)志信息。通過這些標(biāo)志位,JVM可以直接從指針中看到其引用對象的三色標(biāo)記狀態(tài)、是否進(jìn)入了重分配集(即被移動過)、是否只能通過finalize()方法才能被訪問到。這就導(dǎo)致JVM能利用的物理地址總線只剩下42位了,即ZGC能管理的最大內(nèi)存空間為2的42次冪字節(jié),即4TB。
目前ZGC還處于實(shí)驗(yàn)階段,能查到的資料也不多,筆者以后再整理更新吧。
5. 讀懂GC日志
待寫......
6. GC的調(diào)優(yōu)
待寫......