圖解JVM垃圾內(nèi)存回收算法
前言
首先,我們要講的是JVM的垃圾回收機(jī)制,我默認(rèn)準(zhǔn)備閱讀本篇的人都知道以下兩點(diǎn):
- JVM是做什么的
- Java堆是什么
因?yàn)槲覀兗磳⒁v的就是發(fā)生在JVM的Java堆上的垃圾回收,為了突出核心,其他的一些與本篇不太相關(guān)的東西我就一筆略過(guò)了
眾所周知,Java堆上保存著對(duì)象的實(shí)例,而Java堆的大小是有限的,所以我們只能把一些已經(jīng)用完的,無(wú)法再使用的垃圾對(duì)象從內(nèi)存中釋放掉,就像JVM幫助我們手動(dòng)在代碼中添加一條類似于C++的free語(yǔ)句的行為
然而這些垃圾對(duì)象是怎么回收的,現(xiàn)在不知道沒(méi)關(guān)系,我們馬上就會(huì)講到
怎么判斷對(duì)象為垃圾對(duì)象
在了解具體的GC(垃圾回收)算法之前,我們先來(lái)了解一下JVM是怎么判斷一個(gè)對(duì)象是垃圾對(duì)象的
顧名思義,垃圾對(duì)象,就是沒(méi)有價(jià)值的對(duì)象,用更嚴(yán)謹(jǐn)?shù)恼Z(yǔ)句來(lái)說(shuō),就是沒(méi)有被訪問(wèn)的對(duì)象,也就是說(shuō)沒(méi)有被其他對(duì)象引用,這就牽引出我們的第一個(gè)判斷方案:引用計(jì)數(shù)法
引用計(jì)數(shù)法
這種算法的原理是,每有一個(gè)其他對(duì)象產(chǎn)生對(duì)A對(duì)象的引用,則A對(duì)象的引用計(jì)數(shù)值就+1,反之,每有一個(gè)對(duì)象對(duì)A對(duì)象的引用失效的時(shí)候,A對(duì)象的引用計(jì)數(shù)值就-1,當(dāng)A對(duì)象的引用計(jì)數(shù)值為0的時(shí)候,其就被標(biāo)明為垃圾對(duì)象
這種算法看起來(lái)很美好,了解C++的應(yīng)該知道,C++的智能指針也有類似的引用計(jì)數(shù),但是在這種看起來(lái)“簡(jiǎn)單”的方法,并不能用來(lái)判斷一個(gè)對(duì)象為垃圾對(duì)象,我們來(lái)看以下場(chǎng)景:
在這個(gè)場(chǎng)景中,A對(duì)象有B對(duì)象的引用,B對(duì)象也有A對(duì)象的引用,所以這兩個(gè)對(duì)象的引用計(jì)數(shù)值均不為0,但是,A、B兩個(gè)對(duì)象明明就沒(méi)有任何外部的對(duì)象引用,就像大海上兩個(gè)緊挨著的孤島,即使他們彼此依靠著,但仍然是孤島,其他人過(guò)不去,而且由于引用計(jì)數(shù)不為0,也無(wú)法判斷為垃圾對(duì)象,如果JVM中存在著大量的這樣的垃圾對(duì)象,最終就會(huì)頻繁拋出OOM異常,導(dǎo)致系統(tǒng)頻繁崩潰
總而言之,如果有人問(wèn)你為什么JVM不采用引用計(jì)數(shù)法來(lái)判斷垃圾對(duì)象,只需要記住這一句話:引用計(jì)數(shù)法無(wú)法解決對(duì)象循環(huán)依賴的問(wèn)題
可達(dá)性分析法
引用計(jì)數(shù)法已經(jīng)很接近結(jié)果了,但是其問(wèn)題是,為什么每有一個(gè)對(duì)象來(lái)引用就要給引用計(jì)數(shù)值+1,就好像有人來(lái)敲門(mén)就開(kāi)一樣,我們應(yīng)該只給那些我們認(rèn)識(shí)的、重要的人開(kāi)門(mén),也就是說(shuō),只有重要的對(duì)象來(lái)引用時(shí),才給引用計(jì)數(shù)值+1
但是這樣還不行,因?yàn)橹匾膶?duì)象來(lái)引用只要有一個(gè)就夠了,并不需要每有一個(gè)引用就+1,所以我們可以將引用計(jì)數(shù)法優(yōu)化為以下形式:
給對(duì)象設(shè)置一個(gè)標(biāo)記,每有一個(gè)“重要的對(duì)象”來(lái)引用時(shí),就將這個(gè)標(biāo)記設(shè)為true,當(dāng)沒(méi)有任何“重要的對(duì)象”引用時(shí),就將標(biāo)記設(shè)為false,標(biāo)記為false的對(duì)象為垃圾對(duì)象
這就是可達(dá)性分析法的雛形,我們可以繼續(xù)進(jìn)行修正,我們并不需要主動(dòng)標(biāo)記對(duì)象,而只需要等待垃圾回收時(shí)找到這些“重要的對(duì)象”,然后從它們出發(fā),把我們能找到的對(duì)象都標(biāo)記為非垃圾對(duì)象,其余的自然就是垃圾對(duì)象
我們將上文提到的“重要的對(duì)象”命名為GC Roots,這樣就得到了最終的可達(dá)性分析算法的概念:
創(chuàng)建垃圾回收時(shí)的根節(jié)點(diǎn),稱為GC Roots,從GC Roots出發(fā),不能到達(dá)的對(duì)象就被標(biāo)記為垃圾對(duì)象
其中,可以作為GC Roots的區(qū)域有:
- 虛擬機(jī)棧的棧幀中的局部變量表
- 方法區(qū)的類屬性和常量所引用的對(duì)象
- 本地方法棧中引用的對(duì)象
換句話說(shuō),GC Roots就是方法中的局部變量、類屬性,以及常量
垃圾回收算法
終于到本文的重點(diǎn)了,我們剛剛分析了如何判斷一個(gè)對(duì)象屬于垃圾對(duì)象,接下來(lái)我們就要重點(diǎn)分析如何將這些垃圾對(duì)象回收掉
標(biāo)記-清除算法
標(biāo)記-清除很容易理解,該算法有兩個(gè)過(guò)程,標(biāo)記過(guò)程和清除過(guò)程,標(biāo)記過(guò)程中通過(guò)上文提到的可達(dá)性分析法來(lái)標(biāo)記出所有的非垃圾對(duì)象,然后再通過(guò)清除過(guò)程進(jìn)行清理
比方說(shuō),我們現(xiàn)在有下面的這樣的一個(gè)Java堆,已經(jīng)通過(guò)可達(dá)性分析法來(lái)標(biāo)記出所有的垃圾對(duì)象(用橙色表明,藍(lán)色的是普通對(duì)象):
然后我們通過(guò)清除階段進(jìn)行清理,結(jié)果是下圖:
發(fā)現(xiàn)什么問(wèn)題了嗎,沒(méi)錯(cuò),清理完后的空間是不連續(xù)的,也就是說(shuō),整個(gè)算法最大的缺點(diǎn)就是:
- 會(huì)出現(xiàn)大量的空間碎片,當(dāng)需要分配大對(duì)象時(shí),會(huì)觸發(fā)FGC,非常消耗性能
這里引出一個(gè)FGC的概念,為了避免主題跑偏,本文中暫時(shí)不進(jìn)行深入,只需要知道垃圾回收分為YGC(年輕代垃圾回收)和FGC(完全垃圾回收),可以把YGC理解為掃掃地,倒倒垃圾,把FGC理解為給家里來(lái)個(gè)大掃除
復(fù)制算法
復(fù)制算法將Java堆劃分為兩塊區(qū)域,每次只使用其中的一塊區(qū)域,當(dāng)垃圾回收發(fā)生時(shí),將所有被標(biāo)記的對(duì)象(GC Roots可達(dá),為非垃圾對(duì)象)復(fù)制到另一塊區(qū)域,然后進(jìn)行清理,清理完成后交換兩塊區(qū)域的可用性
這種方式因?yàn)槊看沃恍枰徽麎K一起刪除即可,就不用一個(gè)個(gè)地刪除了,同時(shí)還能保證另一塊區(qū)域是連續(xù)的,也解決了空間碎片的問(wèn)題
整個(gè)流程我們?cè)賮?lái)看一遍
1.首先我們有兩塊區(qū)域S1和S2,標(biāo)記為灰色的區(qū)域?yàn)楫?dāng)前激活可用的區(qū)域:
2.對(duì)Java堆上的對(duì)象進(jìn)行標(biāo)記,其中藍(lán)色的為GC Roots可達(dá)的對(duì)象,其余的均為垃圾對(duì)象:
3.接下來(lái)將所有可用的對(duì)象復(fù)制到另一塊區(qū)域中:
4.將原區(qū)域中所有內(nèi)容刪除,并將另一塊區(qū)域激活
這種方法的優(yōu)缺點(diǎn)也很明顯:
- 優(yōu)點(diǎn):解決了空間不連續(xù)的問(wèn)題
- 缺點(diǎn):空間利用率低(每次只使用一半)
為了解決這一缺點(diǎn),就引出了下面這個(gè)算法
優(yōu)化的復(fù)制算法
至于為什么不另起一個(gè)名字,其實(shí)是因?yàn)檫@個(gè)算法也叫做復(fù)制算法,更確切的說(shuō),剛才介紹的只是優(yōu)化算法的雛形,沒(méi)有虛擬機(jī)會(huì)使用上面的那種復(fù)制算法,所以接下來(lái)要講的,就是真正的復(fù)制算法
這個(gè)算法的思路和剛才講的一樣,不過(guò)這個(gè)算法將內(nèi)存分為3塊區(qū)域:1塊Eden區(qū),和2塊Survivor區(qū),其中,Eden區(qū)要占到80%
這兩塊Survivor區(qū)就可以理解為我們剛才提到的S1和S2兩塊區(qū)域,我們每次只使用整個(gè)Eden區(qū)和其中一塊Survivor區(qū),整個(gè)算法的流程可以簡(jiǎn)要概括為:
1.當(dāng)發(fā)生垃圾回收時(shí),將Eden區(qū)+Survivor區(qū)中仍然存活的對(duì)象一次性復(fù)制到另一塊Survivor區(qū)上
2.清理掉Eden區(qū)和使用的Survivor區(qū)中的所有對(duì)象
3.交換兩塊Survivor的激活狀態(tài)
光看文字描述比較抽象,我們來(lái)看圖像的形式:
1.我們有以下這樣的一塊Java堆,其中灰色的Survivor區(qū)為激活狀態(tài)
2.標(biāo)記所有的GC Roots可達(dá)對(duì)象(藍(lán)色標(biāo)記)
3.將標(biāo)記對(duì)象全部復(fù)制到另一塊Survivor區(qū)域中
4.清理掉Eden區(qū)和激活的Survivor區(qū)中的所有對(duì)象,然后交換兩塊區(qū)域的激活狀態(tài)
以上就是整個(gè)復(fù)制算法的全過(guò)程了,有人可能會(huì)問(wèn)了,為什么Survivor區(qū)這么小,就不怕放不下嗎?其實(shí)平均來(lái)說(shuō),每次垃圾回收的時(shí)候基本都會(huì)回收98%左右的對(duì)象,也就是說(shuō),我們完全可以保證大部分情況下剩余的對(duì)象都小于10%,放在一塊Survivor區(qū)中是沒(méi)問(wèn)題的。當(dāng)然,也可能會(huì)發(fā)生Survivor區(qū)不夠用的問(wèn)題,這時(shí)候就需要依賴其他內(nèi)存給我們提供后備了
這種算法較好地解決了內(nèi)存利用率低的問(wèn)題,但是復(fù)制算法的兩個(gè)問(wèn)題依然沒(méi)有解決:
- 對(duì)象復(fù)制采用深度優(yōu)先的遞歸方式來(lái)實(shí)現(xiàn),會(huì)消耗棧資源(Cheney改進(jìn)的GC復(fù)制算法解決了該問(wèn)題)
- 復(fù)制算法無(wú)法處理長(zhǎng)壽數(shù)據(jù),只會(huì)頻繁地將其復(fù)制來(lái)白白消耗資源(重點(diǎn))
標(biāo)記-整理算法
這種算法可以說(shuō)是專門(mén)針對(duì)對(duì)象存活率高的程序,具體的流程如下:
1.GC發(fā)生時(shí),將所有被標(biāo)記的存活對(duì)象移動(dòng)到內(nèi)存的一端
2.移動(dòng)完成后,清理掉所有移動(dòng)后的邊界以外的對(duì)象
我相信大家在理解了前面幾個(gè)算法之后,這個(gè)算法也能很方便地理解,我就不畫(huà)圖了,用一個(gè)例子來(lái)解釋:
問(wèn)題:對(duì)于一個(gè)長(zhǎng)度為n的數(shù)組,我們想要保留其中所有小于10的數(shù)字,其余的數(shù)字刪掉
方案:可以遍歷一遍數(shù)據(jù),將所有小于10的數(shù)字全部放到數(shù)組的最左側(cè),最終,數(shù)組的0~m(0<=m<=n)位置全部都是小于10的數(shù)字,然后我們只需要?jiǎng)h除m+1~n的所有數(shù)字即可
這種方法的優(yōu)點(diǎn)也顯而易見(jiàn):
- 實(shí)現(xiàn)簡(jiǎn)單,執(zhí)行速度快
- 針對(duì)復(fù)制算法處理不佳的長(zhǎng)壽數(shù)據(jù),標(biāo)記-整理算法可以選擇不去整理這些對(duì)象
- 沒(méi)有空間碎片的問(wèn)題
但是依然還是有缺點(diǎn)的:
- 如果堆內(nèi)存較小,則該算法的速度會(huì)下降
- 遍歷時(shí)需要多次訪問(wèn)類型信息和對(duì)象的指針域,開(kāi)銷很大
- 記錄新的轉(zhuǎn)發(fā)地址需要占用額外的空間,導(dǎo)致吞吐量下降
- 不適合并發(fā)回收器
分代收集算法
別急,我們還沒(méi)說(shuō)完,還有最后一個(gè)分代收集算法,這個(gè)算法將Java堆劃分為兩塊區(qū)域:
- 年輕代:存放朝生夕滅的對(duì)象,即存活率低的對(duì)象,大部分對(duì)象在一次GC后都會(huì)被回收
- 老年代:存放存活率高的對(duì)象
可以看出,分代收集算法按照對(duì)象在GC后的存活率將Java堆分為這樣兩塊區(qū)域,針對(duì)不同區(qū)域采用不同的算法,就能盡可能地做到“揚(yáng)長(zhǎng)補(bǔ)短”,來(lái)提高垃圾回收的效率
- 針對(duì)年輕代朝生夕滅的性質(zhì),我們采用復(fù)制算法
- 針對(duì)老年代存活率高的性質(zhì),我們采用標(biāo)記-整理算法
總結(jié)
最后,垃圾回收的幾種常見(jiàn)算法已經(jīng)為大家介紹完畢,接下來(lái)如果有機(jī)會(huì)我會(huì)再介紹一下幾種常見(jiàn)的垃圾回收器
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
JavaWeb學(xué)習(xí)筆記之Filter和Listener
這篇文章主要給大家介紹了關(guān)于JavaWeb學(xué)習(xí)筆記之Filter和Listener的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Java實(shí)現(xiàn)的DES加密解密工具類實(shí)例
這篇文章主要介紹了Java實(shí)現(xiàn)的DES加密解密工具類,結(jié)合具體實(shí)例形式分析了Java實(shí)現(xiàn)的DES加密解密工具類定義與使用方法,需要的朋友可以參考下2017-09-09servlet監(jiān)聽(tīng)器的學(xué)習(xí)使用(三)
這篇文章主要為大家詳細(xì)介紹了servlet監(jiān)聽(tīng)器學(xué)習(xí)使用的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09Java嵌入式開(kāi)發(fā)的優(yōu)勢(shì)及有點(diǎn)總結(jié)
在本篇內(nèi)容里小編給大家整理了關(guān)于Java嵌入式開(kāi)發(fā)的優(yōu)勢(shì)及相關(guān)知識(shí)點(diǎn)內(nèi)容,有興趣的朋友們學(xué)習(xí)下。2022-11-11