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