你是不是垃圾,心里沒點(diǎn)數(shù)嗎?
這一篇就來(lái)聊一聊GC,聊聊我們的垃圾回收器,我們知道Java的垃圾回收機(jī)制與C++的有所不同,作為Java程序員不用在程序中自己釋放內(nèi)存,自己去管理內(nèi)存,對(duì)于內(nèi)存使用似乎是“肆無(wú)忌憚”一樣。
然而,這背后一切的原因就是JVM的GC已經(jīng)幫我們做了這些事,能夠幫我們自動(dòng)管理這些事,當(dāng)內(nèi)存緊張的時(shí)候,就會(huì)觸發(fā)垃圾回收機(jī)制,騰出足夠的空間來(lái)供我們程序使用。
但是,JVM的GC也不是萬(wàn)能的,也有翻車的時(shí)候,比如碰到過(guò)內(nèi)存泄露的時(shí)候,就會(huì)導(dǎo)致GC的內(nèi)存回收的效率低下,甚至出現(xiàn)OOM的異常。
作為Java程序員,我們要做的就是要保證GC正常工作,基于這種情況下對(duì)于GC的工作的原理和GC的使用的場(chǎng)景就得有所了解。
下面就開始我們的正文,這里我畫了一個(gè)介紹這篇文章主要內(nèi)容的思維導(dǎo)圖:
首先先來(lái)聊聊哪些對(duì)象應(yīng)該被GC,JVM是怎么判斷一個(gè)對(duì)象是否存活的呢?
判斷對(duì)象存活
想要判斷對(duì)象是否存活,有兩種方法:
- 引用計(jì)數(shù)法
- 可達(dá)性分析算法
引用計(jì)數(shù)法
第一個(gè)引用計(jì)數(shù)法實(shí)現(xiàn)簡(jiǎn)單,效率高。它的原理就是在對(duì)象內(nèi)部維護(hù)一個(gè)計(jì)數(shù)器,當(dāng)有地方引用它的時(shí)候,計(jì)數(shù)器+1,當(dāng)有地方不再引用它的時(shí)候,計(jì)數(shù)器-1。
就這樣,當(dāng)計(jì)數(shù)器為零的時(shí)候,表示沒有地方引用它,那么這個(gè)對(duì)應(yīng)就應(yīng)該要被GC回收了。
但是這種算法卻很少被Java虛擬機(jī)使用,主要原因是它有漏洞:無(wú)法解決循環(huán)引用的問(wèn)題:
可達(dá)性分析算法
第二種就是可達(dá)性分析算法,它是以一組GCRoots為起點(diǎn),根據(jù)引用鏈的關(guān)系向下搜索,若是某個(gè)對(duì)象與GCRoot之間沒有任何的引用鏈,則這個(gè)對(duì)象是不可達(dá)的,也是將來(lái)會(huì)被回收掉的。
這種算法被引用在主流的Java虛擬機(jī)中,比如HotSpot,那哪些對(duì)象可以作為GCRoots呢?主要有以下的對(duì)象可以作為GCRoots:
- 虛擬棧中引用的對(duì)象。
- 方法區(qū)中的靜態(tài)變量。
- 方法區(qū)中的常量。
- 以及本地方法棧JNI引用的對(duì)象(這個(gè)可以忽略,我們幾乎沒有接觸)。
以上比較常見的就是方法區(qū)中的靜態(tài)變量和常量的引用對(duì)象,知道了怎么判斷對(duì)象是否存活,下面就是用可達(dá)性分析算法,用到具體的垃圾回收算法上。
垃圾回收算法
對(duì)于垃圾回收算法,我這里就不做過(guò)于詳細(xì)的介紹,就簡(jiǎn)單介紹一下,因?yàn)橹耙呀?jīng)寫過(guò)一篇比較詳細(xì)的文章了,大家可以參考一下:還在學(xué)JVM?我都幫你總結(jié)好了(附腦圖)
常見的垃圾回收算法就這三種:
- 標(biāo)記-清除
- 復(fù)制算法
- 標(biāo)記-整理(壓縮算法)
我們知道年輕代基本都是朝生夕死,所以都是使用復(fù)制算法,復(fù)制的成本低,基于這種分代模型理論,也就出現(xiàn)了后面垃圾回收器的Eden區(qū)、From Survivor區(qū)、To Survivor空間(默認(rèn)8:1:1)。
新生代中每次都只有Eden和其中的一個(gè)S區(qū)可用,當(dāng)Eden區(qū)滿了,就會(huì)將存活的對(duì)象復(fù)制到其中的一個(gè)S區(qū)中,若是S區(qū)也滿了,此區(qū)域不滿足晉升條件的對(duì)象就會(huì)被復(fù)制到到另一個(gè)S區(qū)中。就這樣對(duì)象每經(jīng)歷一次Minor GC年齡就會(huì)+1,當(dāng)達(dá)到晉升年齡的閾值,對(duì)象還沒被垃圾回收掉,就會(huì)被放入;老年代。
而老年代使用的是標(biāo)記-清除或者標(biāo)記-整理,對(duì)于標(biāo)記-清除我們知道它最大的缺點(diǎn)就是會(huì)產(chǎn)生內(nèi)存碎片,但是他也有自己的好處(相比標(biāo)記-整理),就是不用移動(dòng)對(duì)象,所以效率相比標(biāo)記-整理要高。
而標(biāo)記-整理完整的過(guò)程應(yīng)該是標(biāo)記-整理-清除三個(gè)步驟,需要將存活的對(duì)象向一邊移動(dòng),然后清理掉不可達(dá)的對(duì)象,所以它的效率也會(huì)比較低,尤其是老年代這種區(qū)域,有大量的對(duì)象存活,那么對(duì)于移動(dòng)對(duì)象所耗費(fèi)的性能也是可觀的。
還有一點(diǎn)比較重要的是:新生對(duì)象的內(nèi)存分配的角度。這個(gè)是很多技術(shù)博文都忽略的一點(diǎn),這一點(diǎn)也是比較重要的,在《深入JVM虛擬機(jī) 第三版》中也有特別強(qiáng)調(diào)這一點(diǎn)。
從內(nèi)存分配的角度來(lái)看:對(duì)于標(biāo)記-整理和復(fù)制算法,都是整理的規(guī)整空間,所以他們倆對(duì)于新產(chǎn)生的對(duì)象進(jìn)行分配內(nèi)存的時(shí)候,是比較簡(jiǎn)單高效的,特別是對(duì)于一些大對(duì)象的分配以及連續(xù)內(nèi)存對(duì)象的分配(數(shù)組)。
標(biāo)記-整理和復(fù)制算法只需要內(nèi)存地址指針移動(dòng)與對(duì)象一樣大小的位置,便可完成內(nèi)存分配,這樣高效簡(jiǎn)單。
而對(duì)于標(biāo)記-清除法,因?yàn)楫a(chǎn)生了內(nèi)存碎片,所以它必須要記住哪些地方是可用的,哪些地方是不可用的,這樣內(nèi)存分配的效率就會(huì)低很多。
知道了具體的垃圾回收算法,下面就來(lái)聊聊具體的垃圾回收器。
垃圾回收器
根據(jù)分離代理模型,對(duì)于不同的區(qū)域設(shè)計(jì)出了不同的垃圾回收器,對(duì)于經(jīng)典的垃圾回收器主要有這么幾種:
- Serial(新生代)
- SerialOld(老年代)
- PS(新生代)
- PO(老年代)
- ParNew(新生代)
- CMS(老年代)
- G1
對(duì)于以上幾種的垃圾回收器,可以選擇不同的老年代和年輕代進(jìn)行搭配使用,主要有以下的搭配方式:
Serial
Serial系列的垃圾回收器,現(xiàn)在也基本沒人用了,它的使用原理就是使用單線程來(lái)進(jìn)行垃圾回收,所以STW的時(shí)間也會(huì)比較長(zhǎng),實(shí)現(xiàn)簡(jiǎn)單。老年代的SerialOld使用的是標(biāo)記-整理的算法來(lái)回收垃圾。
來(lái)源于深入JVM虛擬機(jī)
在若是你的服務(wù)器還處在單核時(shí)代,內(nèi)存只有那么幾十M到百來(lái)M,可能Serial是最優(yōu)的搭配選擇。
對(duì)于Serial的相關(guān)JVM參數(shù)有:-XX:+UseSerialGC(使用Serial垃圾回收器)。
Parallel
當(dāng)發(fā)展到多線程時(shí)代,PS和PO的搭配就出現(xiàn)了,PS和PO相對(duì)于Serial比較來(lái)說(shuō),就是垃圾回收的時(shí)候是使用的是多線程,其它的一樣,包括使用的垃圾回收算法也一樣,所以在多核時(shí)代,它相比Serial STW的時(shí)間變得更短了:
這里涉及到一個(gè)吞吐量的概念:吞吐量 = 用戶應(yīng)用程序運(yùn)行的時(shí)間 / (應(yīng)用程序運(yùn)行的時(shí)間 + 垃圾回收的時(shí)間),因?yàn)镻S+PO的搭配是追求吞吐量的垃圾回收器。
因此PS+PO的組合比較適用于后臺(tái)快速完成計(jì)算任務(wù),不需要太多的與用戶交互的場(chǎng)景。
與PS+PO有關(guān)的JVM參數(shù)如下所示:
- -XX: +UseParallelGC:開啟ParallelGC。
- -XX: +UseParallelOldGC:開啟老年代的ParallelGC,和上面的任意開啟一個(gè)就行。
- -XX: ParallelGCThreads:指定線程數(shù)。
- -XX:MaxGCPauseMillis:控制最大垃圾收集停頓時(shí)間(毫秒數(shù))
- -XX:GCTimeRatio:直接設(shè)置吞吐量大小(大于0小于100的整數(shù))
- -XX:+UseAdaptiveSizePolicy:當(dāng)這個(gè)參數(shù)被激活后,就不需要制定新生代的大小(-Xmn)、Eden和S區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對(duì)象的大小(-XX:PretenureSizeThreshold)等參數(shù),虛擬機(jī)會(huì)自己動(dòng)態(tài)的調(diào)整。
PS和PO是Java 8默認(rèn)的垃圾回收器,不知道各位讀者的Java的版本是多少,已經(jīng)使用Java 8好久了。
ParNew
ParNew實(shí)際上和Parallel的實(shí)現(xiàn)原理基本相同,唯一不同的是它可以和CMS搭配使用,而PS是沒辦法與CMS搭配使用,這也使得ParNew火起來(lái),當(dāng)JVM中設(shè)置了使用CMS作為老年代的回收器的時(shí)候,新生代的垃圾回收器默認(rèn)就是ParNew。
CMS
CMS可以說(shuō)是跨時(shí)代的一款垃圾回收器,它實(shí)現(xiàn)了垃圾回收與用戶線程并發(fā)進(jìn)行,在它是一種以獲取最短垃圾停頓時(shí)間為目的的垃圾回收器。
特別適用于用戶頻繁交互的場(chǎng)景,它的實(shí)現(xiàn)過(guò)程分為以下四個(gè)階段:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清理
其中初始標(biāo)記和重新標(biāo)記是需要STW的,而并發(fā)標(biāo)記和并發(fā)清理垃圾回收線程與用戶線程并發(fā)執(zhí)行。
初始標(biāo)記階段僅僅是標(biāo)記GC Root直接關(guān)聯(lián)的對(duì)象,并不會(huì)遍歷整個(gè)對(duì)象圖,所以速度很快。
并發(fā)標(biāo)記階段就是從GC Root開始遍歷整個(gè)對(duì)象圖的過(guò)程,這個(gè)過(guò)程是四個(gè)階段最耗時(shí)的過(guò)程,因此此階段也是與用戶線程并發(fā)執(zhí)行的,不需要停頓用戶線程。
重新標(biāo)記階段是修正在并發(fā)標(biāo)記階段因用戶線程運(yùn)行產(chǎn)生一些對(duì)象的引用關(guān)系變動(dòng)的標(biāo)記記錄,因?yàn)樵诓l(fā)階段用戶線程與垃圾回收線程是并發(fā)執(zhí)行的,那么就有可能之前已經(jīng)標(biāo)記的,它的引用關(guān)系又被改變了,這需要在這個(gè)階段重新修正。
并發(fā)清理因?yàn)椴挥靡苿?dòng)用戶對(duì)象,因此可以與用戶線程一起并發(fā)執(zhí)行,最后清理掉不可達(dá)的對(duì)象。
四個(gè)階段中其中最復(fù)雜的就是第三階段并發(fā)標(biāo)記,其中涉及到的一個(gè)重要概念就是三色標(biāo)記法,第三階段在標(biāo)記的過(guò)程有可能對(duì)對(duì)象產(chǎn)生漏標(biāo)或者多標(biāo)的現(xiàn)象,那CMS又是怎么來(lái)解決這兩個(gè)問(wèn)題的呢?
我們先來(lái)詳細(xì)了解一下三色標(biāo)記法。三色標(biāo)記法中將對(duì)象分為白色、灰色、黑色的過(guò)程。
- 白色:白色是對(duì)象默認(rèn)的顏色,從GC Root開始掃描,如果不可達(dá)的對(duì)象的就是白色,在并發(fā)清理階段就會(huì)被清理掉。
- 灰色:灰色表示當(dāng)前對(duì)象已經(jīng)被掃描,但是當(dāng)前對(duì)象所依賴的其他對(duì)象還沒有被掃描。
- 黑色:黑色表示當(dāng)前對(duì)象和它所依賴的對(duì)象都已經(jīng)被掃描過(guò)。
那它又是怎么產(chǎn)生多標(biāo)和漏標(biāo)的呢?下面來(lái)畫圖看看:
開始有三個(gè)對(duì)象,分別是對(duì)象1和對(duì)象2以及對(duì)象3,三個(gè)對(duì)象與GC Root之間都存在引用鏈,當(dāng)開始進(jìn)行標(biāo)記,就會(huì)從GC Root開始掃描。
當(dāng)掃描了對(duì)象1和對(duì)象2的時(shí)候,因?yàn)閷?duì)象2沒有再依賴的引用,所以它會(huì)變成黑色,而對(duì)象1還引用著對(duì)象3,并且對(duì)象3還沒有掃描,所以對(duì)象1變成灰色。
若是,此時(shí)用戶線程將對(duì)象3與對(duì)象1之間的引用關(guān)系改變了,變成了對(duì)象2與對(duì)象3之間有引用關(guān)系,因?yàn)閷?duì)象2已經(jīng)掃描完了,對(duì)象3還沒掃描,此時(shí)應(yīng)該是對(duì)象2是灰色的狀態(tài),并且對(duì)象3是白色的狀態(tài),對(duì)象3就會(huì)被回收掉,這就出現(xiàn)了漏標(biāo)的情況。
多標(biāo)的情況就是當(dāng)對(duì)象1和對(duì)象3之間開始有引用鏈,并且都已經(jīng)標(biāo)記為黑色,此時(shí)用戶線程又把對(duì)象3設(shè)置為null,那么此時(shí)按理來(lái)說(shuō)對(duì)象3應(yīng)該被回收的,但是因?yàn)槭呛谏⒉粫?huì)被回收掉,所以出現(xiàn)了多標(biāo),多標(biāo)的情況可以在下次垃圾回收的時(shí)候,進(jìn)行重新標(biāo)記,被重新回收,所以多標(biāo)并不會(huì)是GC回收的過(guò)程出現(xiàn)bug。
而漏標(biāo)就需要解決了,不然GC回收就會(huì)出現(xiàn)bug,對(duì)于漏標(biāo)CMS給出的解決方案是增量更新的方法。它的原理就是假如對(duì)象3的引用變成了對(duì)象2,那么對(duì)象2就會(huì)變成灰色,并且對(duì)象2會(huì)被集合里面,在重新標(biāo)記的階段以對(duì)象2為根節(jié)點(diǎn)向下掃描。
這樣CMS就解決漏標(biāo)的問(wèn)題,并且實(shí)現(xiàn)了整個(gè)GC Root對(duì)象圖的時(shí)候,能夠與用戶線程并發(fā)執(zhí)行,大大減少了STW的時(shí)間。
那為什么CMS又選擇標(biāo)記-清除算法呢?因?yàn)榧偃邕x擇標(biāo)記-整理算法,在并發(fā)清理階段因?yàn)橐M(jìn)行整理,涉及對(duì)象的移動(dòng),此時(shí)就不能與用戶線程一起并發(fā)操作,這樣清理階段就必須STW,就違背了CMS設(shè)計(jì)初衷:獲取最短回收停頓時(shí)間。
與CMS有關(guān)的JVM參數(shù)如下所示:
- -XX:+UseConcMarkSweepGC:使用CMS垃圾收集器(當(dāng)設(shè)置這個(gè)參數(shù)后,年輕代默認(rèn)會(huì)開啟ParNew)。
- -XX:+UseCMSCompactAtFullCollection:用于在CMS收集器不得不進(jìn)行FullGC時(shí)開啟內(nèi)存碎片的合并整理過(guò)程,由于這個(gè)內(nèi)存整理必須移動(dòng)存活對(duì)象,清理階段是無(wú)法并發(fā)的,此參數(shù)從JDK9開始廢棄。
- -XX:CMSFullGCsBefore-Compaction:多少次FullGC之后壓縮一次,默認(rèn)值為0,表示每次進(jìn)入FullGC時(shí)都進(jìn)行碎片整理,此參數(shù)從JDK9開始廢棄。
- -XX:CMSInitiatingOccupancyFraction:當(dāng)老年代使用達(dá)到該比例時(shí)會(huì)觸發(fā)FullGC,默認(rèn)是92。
- -XX:+UseCMSInitiatingOccupancyOnly:這個(gè)參數(shù)搭配上面那個(gè)用,表示是不是要一直使用上面的比例觸發(fā)FullGC,如果設(shè)置則只會(huì)在第一次FullGC的時(shí)候使用-XX:CMSInitiatingOccupancyFraction的值,之后會(huì)進(jìn)行自動(dòng)調(diào)整。
- -XX:+CMSScavengeBeforeRemark:在FullGC前啟動(dòng)一次MinorGC,目的在于減少老年代對(duì)年輕代的引用,降低CMSGC的標(biāo)記階段時(shí)的開銷,一般CMS的GC耗時(shí)80%都在標(biāo)記階段。
- -XX:+CMSParallellnitialMarkEnabled:默認(rèn)情況下初始標(biāo)記是單線程的,這個(gè)參數(shù)可以讓他多線程執(zhí)行,可以減少STW。
- -XX:+CMSParallelRemarkEnabled:使用多線程進(jìn)行重新標(biāo)記,目的也是為了減少STW。
CMS的出現(xiàn)是有著非常重要的意義,它為后面更加智能的垃圾回收器G1、ZGC的出現(xiàn)奠定了基礎(chǔ),首次實(shí)現(xiàn):用戶線程與垃圾回收線程并發(fā)執(zhí)行,但是慢慢的CMS也退出了舞臺(tái)。
你會(huì)發(fā)現(xiàn)關(guān)于CMS的很多相關(guān)的JVM參數(shù)在jdk9已經(jīng)廢棄,并且在jdk9,默認(rèn)的垃圾回收器已經(jīng)不再是PS+PO了,已經(jīng)變成了G1,說(shuō)明JVM設(shè)計(jì)團(tuán)隊(duì)認(rèn)為G1已經(jīng)可以取代以前的垃圾回收器了(我還停留在jdk8,手動(dòng)狗頭),我相信應(yīng)該還有很多人在jdk8吧,哈哈哈。
G1
最后聊得就是G1了,G1因?yàn)樗膬?yōu)勢(shì)也成為了jdk9的默認(rèn)垃圾回收器。它與其他的回收器不同的是,它將整個(gè)堆劃分為很多個(gè)Region,每個(gè)Region的大小大概在1M-32M之間。
它不在像以前的垃圾回收器一樣將整個(gè)堆劃分為年輕代和老年代,它的衡量標(biāo)準(zhǔn)是以哪塊Region回收的利益最大,這就是G1的MixedGC模式。
G1的階段過(guò)程和CMS有異曲同工之妙,也是分為四個(gè)階段:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 最終標(biāo)記
- 篩選回收
初始標(biāo)記和CMS一樣,需要STW,只是標(biāo)記GC Roots直接關(guān)聯(lián)的對(duì)象,時(shí)間會(huì)非常的短。
并發(fā)標(biāo)記也是與用戶線程一起并發(fā)執(zhí)行,需要從GC Root開始遍歷整個(gè)對(duì)象圖,也是消耗時(shí)間最長(zhǎng)的階段。
最終標(biāo)記階段用于處理并發(fā)階段結(jié)束后仍遺留下來(lái)的最后那少量的SATB記錄,也就是并發(fā)標(biāo)記階段引用關(guān)系重新改變的對(duì)象。
最后就是回收階段,因?yàn)镚1使用的是標(biāo)記-整理算法,所以涉及到對(duì)象的移動(dòng),所以這個(gè)階段是需要STW的,必須暫停用戶線程,由多條線程來(lái)執(zhí)行垃圾回收。
最后來(lái)聊一聊G1實(shí)現(xiàn)的一些小細(xì)節(jié),一個(gè)是在并發(fā)標(biāo)記階段,它是怎么解決新對(duì)象內(nèi)存分配的問(wèn)題?另外一個(gè)最重要的細(xì)節(jié)就是它是怎么建立起可預(yù)估的停頓時(shí)間模型?在G1和CMS之間如何做選擇呢?
先來(lái)看看第一個(gè)問(wèn)題,在并發(fā)標(biāo)記階段用戶線程也是在執(zhí)行的,在執(zhí)行就會(huì)產(chǎn)生新的對(duì)象,G1是為每一個(gè)Region設(shè)計(jì)了兩個(gè)名為TAMS(TopatMarkStart)的指針。
并且把Region中的一部分空間劃分出來(lái)用于新對(duì)象的內(nèi)存分配,在并發(fā)回收時(shí)新分配的對(duì)象地址都必須要在這兩個(gè)指針位置以上。
然后第二個(gè)細(xì)節(jié)就是G1在垃圾回收的過(guò)程中會(huì)記錄每一個(gè)Region的回收耗時(shí),花費(fèi)的成本,并且根據(jù)多次計(jì)算出平均值,這樣能夠預(yù)估每一個(gè)Region的垃圾耗時(shí),然后根據(jù)程序中設(shè)定的最短垃圾回收時(shí)間,估算回收哪一些Region是利益最大的。
那么在G1和CMS之間是如何進(jìn)行選擇的呢?對(duì)于小內(nèi)存的(1G-4G)CMS的可能會(huì)優(yōu)于G1,而對(duì)于大內(nèi)存的(6G-8G)的可能G1就會(huì)顯現(xiàn)出自己的優(yōu)勢(shì)。
最后與G1有關(guān)的JVM參數(shù)如下:
- -XX:G1HeapRegionSize:設(shè)置每個(gè)Region的大小,取值范圍為1MB~32MB。
- -XX:MaxGCPauseMillis:設(shè)置垃圾收集器的停頓時(shí)間,默認(rèn)值是200毫秒。
好了有關(guān)于垃圾器的就聊到這里,還有一個(gè)也是比較經(jīng)典的就是ZGC,有興趣的可以自行去了解一下,限于篇幅原因,這一片關(guān)于JVM的垃圾,我們就聊到這里,下一篇繼續(xù)深入聊JVM,我是黎杜,我們下一期見。
本文轉(zhuǎn)載自微信公眾號(hào)「黎杜編程」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系黎杜編程小熊公眾號(hào)。