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