JVM知識總結(jié)之垃圾收集算法
一、什么是垃圾
本文要講的是垃圾收集算法,那么首先要確定的問題就是什么是垃圾,也就是哪些對象是要被回收的,對此有兩種判斷方式:
1.1 引用計數(shù)算法
什么樣的對象是要被回收的,很明顯,沒有被引用的對象才要被回收。因此在對象中加一個引用計數(shù)器,當(dāng)有一個對象引用該對象的時候,計數(shù)器就加一,當(dāng)引用結(jié)束后,計數(shù)器就減一,當(dāng)計數(shù)器為0的時候,對象就可以被回收了。
1.1.1 優(yōu)點
- 原理簡單
- 判斷效率高
1.1.2 缺點
- 需要花費額外的內(nèi)存空間(引用計數(shù)器)
- 無法回收相互循環(huán)引用的對象:比如有對象A和對象B,A引用B,B引用A,兩對象的引用計數(shù)器都為1,理論上來說,沒有其他對象能夠引用到A和B了,因此這兩個對象應(yīng)該被回收,然后按照引用計數(shù)算法的判斷,這兩個對象無法被回收。(要克服這個缺點,需要在代碼中做很多特殊處理)
1.2 可達性分析算法
因為引用計數(shù)算法的缺陷,各大主流的商用程序語言都采用可達性分析算法來判斷對象是否需要被回收??蛇_性顧名思義,是指對象跟對象之間有引用關(guān)系,此處有兩種引用關(guān)系:
- 對象A引用對象B,則稱對象A到對象B可達
- 對象A引用對象B,對象B引用對象C,對象A可以通過若干個對象(此處為對象B)引用到對象C,則稱對象A到對象C可達。
要判斷一個對象是否可達,首先要有一個根對象,在Java中有一系列被稱為“GC Roots”的根對象作為起始節(jié)點集,任何從“GC Roots”不可達的對象都是需要被垃圾收集器回收的垃圾。
1.2.1 優(yōu)點
可以有效解決引用計數(shù)算法的相互循環(huán)引用問題
二、什么是引用
在討論什么是垃圾的時候,多次提到引用一詞,那么什么是引用呢?
2.1 JDK1.2以前
按照書中的說法,在JDK1.2以前,引用的意思是:如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱該reference數(shù)據(jù)是代表某塊內(nèi)存、某個對象的引用。
在這種定義下可以發(fā)現(xiàn),對于一個對象來說,就只有未被引用和被引用兩種狀態(tài)了,但其實可以發(fā)現(xiàn),在實際應(yīng)用中,并不是一定要把對象回收掉的,書中有個詞就很貼切,“食之無味,棄之可惜”,我們想要的是當(dāng)內(nèi)存空間足夠的時候,把這部分本該回收的對象留著不回收,當(dāng)內(nèi)存不夠的時候,就將其回收。
2.2 JDK1.2之后
因此在JDK1.2之后,引用的概念就擴張到了以下四種:
- 強引用:指傳統(tǒng)意義上的引用,有強引用的對象是肯定不被回收的;
- 軟引用:用于描述一些還有用,但非必須的對象,當(dāng)要發(fā)生內(nèi)存溢出的時候,就會回收軟引用對象;
- 弱引用:用于描述非必須對象,強度比軟引用弱一點,當(dāng)垃圾收集器開始工作,無論內(nèi)存夠不夠都會回收弱引用對象;
- 虛引用:虛引用意思就是這個引用跟沒有一樣,對對象完全沒有印象,其存在的唯一作用就是在對象被垃圾收集器回收時能收到一個系統(tǒng)通知。
三、垃圾判斷全流程
按照書中所述,我畫了個流程圖,如下:
一個對象在被回收前,需要進行兩次標(biāo)記,第一次進行可達性分析后,對象被垃圾收集器認(rèn)為是垃圾,則對對象進行第一次標(biāo)記,然后垃圾收集器會給予對象一次自救的機會,不然就沒必要兩次標(biāo)記了,一次標(biāo)記直接回收就好了。
我們都知道對象有個finalize()方法,自救的機會就在這個方法中,當(dāng)?shù)谝淮螛?biāo)記后,垃圾收集器會對對象做一次篩選,篩選條件是要不要執(zhí)行對象的finalize()方法,如果開發(fā)者未對finalize()方法進行覆寫或者虛擬機已經(jīng)執(zhí)行過該對象的finalize()方法了,那么自然就不用再執(zhí)行了,反之則需要執(zhí)行。
將篩選出來的需要執(zhí)行finalize()方法的對象放入一個特定的隊列中,由虛擬機統(tǒng)一執(zhí)行,如果finalize()方法中使得對象被別的對象引用了,導(dǎo)致可達性分析認(rèn)為對象是可用的,那么自救就成功了。
根據(jù)篩選的條件可以知道,對象的自救機會在整個程序中只有一次,因為finalize()方法只會被執(zhí)行一次。
需要注意的是,官方明確申明不推薦使用finalize()方法,因為使用它的不確定性太大。對于資源清理等操作,try…catch語法可以做的更好。
四、垃圾收集算法
大多數(shù)虛擬機的垃圾收集都采用了分代收集的形式,這是因為三條經(jīng)驗法則:
- 弱分代假說:絕大多數(shù)對象都是朝生夕死的;
- 強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡
- 跨代引用假說:跨代引用相對于同代引用來說僅占極少數(shù)
因為對象的生存周期是不一樣的,所以我們不能對所有對象采用同一種垃圾收集算法,采用分代收集,將有共性的對象放在一個集合里,會大大地提高垃圾收集效率。
按照上述經(jīng)驗法則,可以將堆內(nèi)存分為兩代:
- 新生代:對應(yīng)弱分代假說
- 老年代:對應(yīng)強分代假說
下面分別介紹三種垃圾收集算法:
4.1 標(biāo)記-清除算法
標(biāo)記-清除算法是最基礎(chǔ)的垃圾收集算法,顧名思義,標(biāo)記就是判斷對象是否是垃圾,也就是前面第四節(jié)講到的內(nèi)容,清除就是統(tǒng)一回收垃圾,該算法有兩種執(zhí)行過程:
- 標(biāo)記所有需要被回收的對象,統(tǒng)一回收所有標(biāo)記的對象;
- 標(biāo)記所有存活對象,統(tǒng)一回收所有未被標(biāo)記的對象。
標(biāo)記-清除算法示意圖:
標(biāo)記-清除算法有兩大缺點:
- 執(zhí)行效率不穩(wěn)定:標(biāo)記和清除兩個過程的執(zhí)行效率隨對象數(shù)量的增加而降低
- 內(nèi)存空間碎片化:執(zhí)行完標(biāo)記和清除后,會產(chǎn)生大量的不連續(xù)的內(nèi)存碎片,當(dāng)分配大對象的時候,如果找不到足夠的連續(xù)內(nèi)存,那么會提前觸發(fā)下一次垃圾收集。
4.2 標(biāo)記-復(fù)制算法
基于標(biāo)記-清除算法的缺點,標(biāo)記-復(fù)制算法將內(nèi)存空間一分為二,兩塊內(nèi)存空間等大,每次只使用其中一塊內(nèi)存空間,當(dāng)這一塊內(nèi)存空間用完了,就把存活的對象復(fù)制到另一塊內(nèi)存空間中,然后一次性清理所有已使用的內(nèi)存空間。
標(biāo)記-復(fù)制算法示意圖:
標(biāo)記-復(fù)制算法解決了標(biāo)記-清除算法面對大量可回收對象場景下的不足之處,面對這種情況,標(biāo)記-復(fù)制算法只需要將內(nèi)存空間中的存活對象復(fù)制到另一半內(nèi)存空間中,可以有效解決內(nèi)存碎片的問題,在給對象分配內(nèi)存的時候,只需要移動堆頂指針按順序分配即可,不過這個算法也有缺點:
- 面對大量不可回收對象的時候,會產(chǎn)生大量內(nèi)存間對象復(fù)制的開銷;
- 原先的內(nèi)存空間縮小了一半,會造成嚴(yán)重的空間浪費
4.3 標(biāo)記-整理算法
標(biāo)記-復(fù)制算法不足以應(yīng)對有大量存活對象的場景,因此就有了標(biāo)記-整理算法,該算法的執(zhí)行流程如下:
- 與其他算法一樣,首先對對象進行標(biāo)記;
- 將所有存活對象往內(nèi)存的一個方向移動;
- 直接清理掉邊界以外的內(nèi)存
標(biāo)記-整理算法示意圖:
標(biāo)記-整理算法同樣可以解決內(nèi)存碎片化問題,并且不會造成空間浪費,不過它也有缺點:
在大量對象存活的情況下,移動對象并更新引用也會花費大量時間
4.4 應(yīng)用
不同的場景適用不同的垃圾收集算法,像標(biāo)記-復(fù)制算法就適用于存活對象少的情況下,也就是新生代區(qū)域,像標(biāo)記-整理算法就適用于存活對象多的情況下,也就是老年代。
這里有點需要注意的是,標(biāo)記-整理算法對于老年代來說也不是完美的,在5.3節(jié)我們說過,在大量對象存活的情況下,移動對象和更新引用也是要花費大量時間的,不過算法這個東西吧,它比的是誰更適合,對于標(biāo)記-復(fù)制算法來說,我把區(qū)域一分為二,如果大量對象存活,我要把對象全部復(fù)制到另一塊內(nèi)存區(qū)域,這個開銷不見得比標(biāo)記-整理算法少,并且它還有個缺點就是可用內(nèi)存一下子少了一半,這個問題在標(biāo)記-整理算法中是沒有的。也有的虛擬機采用標(biāo)記-清除算法與標(biāo)記-整理算法協(xié)作的垃圾收集方案,沒有最適合,只有更適合。
4.5 優(yōu)化
前面講標(biāo)記-復(fù)制算法的時候說到要把內(nèi)存區(qū)域等半分,這是在沒有規(guī)定場景的情況下,在新生代中采用該垃圾收集算法可以做更好的優(yōu)化。
眾所周知,新生代中的對象都是朝生夕死的,因此當(dāng)標(biāo)記完成后的存活對象肯定是少量的,根據(jù)這個現(xiàn)象,可以將內(nèi)存區(qū)域非等半分,比如說9:1的分法,這里我們將90%的內(nèi)存區(qū)域稱為Eden空間,將10%的內(nèi)存區(qū)域稱為Survivor空間,一開始使用Eden空間的內(nèi)存,當(dāng)垃圾收集時,將Eden空間的存活對象復(fù)制到Survivor空間中。
這里肯定有人要問了,那下一次使用Survivor空間不是就只有10%的內(nèi)存了嗎?
對的,所以這里有兩種解決方案:
- 將存活對象從Eden空間復(fù)制到Survivor空間后,再從Survivor空間復(fù)制回Eden空間;
- 從Eden空間再分離出一個Survivor空間,每次可使用的內(nèi)存為一個Eden空間和一個Survivor空間,當(dāng)垃圾收集時,將使用的內(nèi)存區(qū)域中的存活對象復(fù)制到另一個Survivor空間中,下一次的可用內(nèi)存則為Eden空間和這個Survivor空間,如此循環(huán)往復(fù)。
第二種方法就是大名鼎鼎的半?yún)^(qū)復(fù)制分代策略,現(xiàn)在叫Appel式回收,因為提出這個策略的人叫Apple,目前很多虛擬機在新生代的垃圾收集算法中采用這個策略。
4.5.1 缺點
半?yún)^(qū)復(fù)制分代策略也是有缺點的,從上面的敘述中我們可以知道,Eden空間和Survivor空間的內(nèi)存占比為8:1:1,如果當(dāng)垃圾收集后的存活對象所需要的內(nèi)存空間大于一個Survivor空間時,那就難辦了。
4.5.2 補丁
既然Survivor空間的內(nèi)存不夠放存活對象了,那就去借內(nèi)存區(qū)域,這個借當(dāng)然不能跟Eden空間和Survivor空間借,不然會影響到整個算法,增加算法的復(fù)雜度。新生代不能借,那就跟老年代借,這里就有一個所謂的內(nèi)存分配擔(dān)保,放不下的存活對象將直接通過分配擔(dān)保機制進入到老年代中。有了這個“逃生門”一樣的設(shè)計,這個策略才算是沒有漏洞。
五、寫在后面
幾個垃圾收集算法的圖是我直接截了書里面的圖,因為我覺得它講的很詳細了,第四節(jié)垃圾判斷過程在書中實際上是一長串的代碼,看懂不難,不過我想畫個流程圖可能更清楚點,這個流程圖是用plantUML畫出來的,這個工具可以用代碼畫出各種圖,功能強大,有興趣的可以百度搜搜,下面是這個流程圖的代碼:
@startuml start :對對象進行可達性分析; if (對象是否為垃圾?) then (是) :進行第一次標(biāo)記; if (對象沒有覆蓋finalize()方法 或 finalize()方法已經(jīng)被虛擬機調(diào)用) then(是) :沒必要執(zhí)行對象的finalize()方法; else (否) :將對象放入隊列F-Queue中; :等待虛擬機的Finalizer線程執(zhí)行對象的finalize()方法; :執(zhí)行對象的finalize()方法; endif if (對象是否為垃圾?) then (是) :進行第二次標(biāo)記; :垃圾回收; else (否) stop endif else (否) stop endif stop @enduml
到此這篇關(guān)于JVM知識總結(jié)之垃圾收集算法的文章就介紹到這了,更多相關(guān)JVM垃圾收集算法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
數(shù)組重排序(如何將所有奇數(shù)都放在所有偶數(shù)前面)的深入分析
本篇文章是對數(shù)組重排序(如何將所有奇數(shù)都放在所有偶數(shù)前面)的方法進行了詳細的分析介紹,需要的朋友參考下2013-06-06