一、GC概述
我們經(jīng)過(guò)細(xì)說(shuō)JVM(Java內(nèi)存區(qū)域劃分AND初探對(duì)象的創(chuàng)建)的學(xué)習(xí)牍白,已經(jīng)知道Java在運(yùn)行時(shí)內(nèi)存分為了五個(gè)部分:程序計(jì)數(shù)器、虛擬機(jī)棧抖棘、本地方法棧茂腥、堆狸涌、方法區(qū)。其中程序計(jì)數(shù)器最岗、虛擬機(jī)棧帕胆、本地方法棧所占用的內(nèi)存是不需要垃圾收集的,這三個(gè)區(qū)域的內(nèi)存隨著線(xiàn)程生般渡,隨著線(xiàn)程死懒豹,我們需要關(guān)注的其實(shí)只有堆和方法區(qū)這兩塊內(nèi)存的垃圾收集。
二驯用、對(duì)象的生與死
我們?cè)?a href="http://www.reibang.com/p/0888f13c71f2" target="_blank">細(xì)說(shuō)JVM(Java內(nèi)存區(qū)域劃分AND初探對(duì)象的創(chuàng)建)中已經(jīng)學(xué)習(xí)過(guò)了JVM中對(duì)象的創(chuàng)建脸秽,那么我們現(xiàn)在就來(lái)學(xué)習(xí)一下如何判斷一個(gè)堆中的對(duì)象是否已經(jīng)死亡,畢竟我們只有在確定一個(gè)對(duì)象已經(jīng)死了以后蝴乔,我們才可以把這個(gè)對(duì)象占用的內(nèi)存回收记餐。
我們需要先定義對(duì)象什么時(shí)候算是死亡:不可能再被任何途徑使用的對(duì)象
然后我們來(lái)看一下判斷對(duì)象是否死亡的相關(guān)算法:
1、引用計(jì)數(shù)算法
思想:給對(duì)象中添加一個(gè)引用計(jì)數(shù)器薇正,每當(dāng)有一個(gè)地方引用它時(shí)片酝,計(jì)數(shù)器加1;當(dāng)引用失效時(shí)挖腰,計(jì)數(shù)器減1雕沿;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的。
使用狀況:引用計(jì)數(shù)算法是在JVM中被摒棄的一種對(duì)象存活判定算法猴仑,不過(guò)它也有一些知名的應(yīng)用場(chǎng)景(如Python晦炊、FlashPlayer)。
優(yōu)點(diǎn)和缺點(diǎn):引用計(jì)數(shù)算法的實(shí)現(xiàn)簡(jiǎn)單宁脊,判定效率也很高断国,大部分情況下是一個(gè)不錯(cuò)的算法。它沒(méi)有被JVM采用的原因是它很難解決對(duì)象之間循環(huán)引用的問(wèn)題榆苞。例如如下代碼:
/** * testGC()方法執(zhí)行后稳衬,objA和objB會(huì)不會(huì)被GC呢? */
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/** * 這個(gè)成員屬性的唯一意義就是占點(diǎn)內(nèi)存坐漏,以便在能在GC日志中看清楚是否有回收過(guò) */
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假設(shè)在這行發(fā)生GC薄疚,objA和objB是否能被回收?
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
在上面這段代碼中赊琳,對(duì)象objA
和對(duì)象objB
都有字段instance
街夭,賦值令objA.instance = objB;、objB.instance = objA;
躏筏,除此之外板丽,這兩個(gè)對(duì)象再無(wú)引用。如果JVM采用引用計(jì)數(shù)算法來(lái)管理內(nèi)存,這兩個(gè)對(duì)象不可能再被訪(fǎng)問(wèn)埃碱,但是他們互相引用著對(duì)方猖辫,導(dǎo)致它們引用計(jì)數(shù)不為0,所以引用計(jì)數(shù)器無(wú)法通知GC收集器回收它們砚殿。
但是如果運(yùn)行這段代碼啃憎,分析一下這段代碼的GC日志
我們就可以發(fā)現(xiàn)其實(shí)這兩個(gè)對(duì)象所占用的內(nèi)存已經(jīng)被回收了,所以在JVM的垃圾收集器中并沒(méi)有使用這種算法來(lái)判斷對(duì)象是否死亡似炎。(這里可以通過(guò)虛擬機(jī)參數(shù)-XX:+PrintGC
來(lái)打印簡(jiǎn)單的GC日志)
2辛萍、可達(dá)性分析算法
思想:
通過(guò)一系列的稱(chēng)為“GC Roots”的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)向下搜索羡藐,搜索所走過(guò)的路徑稱(chēng)為引用鏈(Reference Chain)贩毕,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(用圖論的話(huà)來(lái)說(shuō),就是GC Roots 到這個(gè)對(duì)象不可達(dá))時(shí)传睹,則證明此對(duì)象時(shí)不可用的耳幢。用下圖來(lái)加以說(shuō)明:
上圖中岸晦,對(duì)象object 5欧啤、object 6、object 7雖然互有關(guān)聯(lián)启上,但是它們到GC Roots是不可達(dá)的邢隧,所以它們將會(huì)被判定為是可回收的對(duì)象。
首先你可能不太懂“GC Roots”是什么東西冈在,在Java中倒慧,可作為GC Roots的對(duì)象包括以下幾種:
- 虛擬機(jī)棧(棧幀中的局部變量表,Local Variable Table)中引用的對(duì)象包券。
- 方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象纫谅。
- 方法區(qū)中常量引用的對(duì)象。
- 本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象溅固。
然后你可能會(huì)疑惑為啥為啥要用這些對(duì)象作為"GC Roots"付秕,這里我找了一些資料,初步得出的結(jié)論是因?yàn)檫@些對(duì)象是存活的侍郭,我們只能通過(guò)一個(gè)活的對(duì)象來(lái)判斷和它有關(guān)系的對(duì)象是存活的询吴,因此選用這些區(qū)域內(nèi)引用的對(duì)象作為GC Roots。其中虛擬機(jī)棧和本地方法棧都是線(xiàn)程私有的內(nèi)存區(qū)域亮元,只要線(xiàn)程沒(méi)有終止猛计,就能確保它們中引用的對(duì)象的存活。而方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象是顯然存活的爆捞。常量引用的對(duì)象在當(dāng)前可能存活奉瘤,因此,也可能是GC roots的一部分煮甥。
三毛好、在Java中的引用分類(lèi)
從JDK1.2版本開(kāi)始望艺,加入了對(duì)象的幾種引用級(jí)別,從而使程序能夠更好的控制對(duì)象的生命周期肌访,幫助開(kāi)發(fā)者能夠更好的緩解和處理內(nèi)存泄露的問(wèn)題找默。
這幾種引用級(jí)別由高到低分別為:強(qiáng)引用、軟引用吼驶、弱引用和虛引用惩激。
強(qiáng)引用:
平時(shí)我們編程的時(shí)候例如:Object object=new Object();
那object就是一個(gè)強(qiáng)引用了蟹演。如果一個(gè)對(duì)象具有強(qiáng)引用风钻,那就類(lèi)似于必不可少的生活用品,垃圾回收器絕不會(huì)回收它酒请。當(dāng)內(nèi)存空 間不足骡技,Java虛擬機(jī)寧愿拋出OutOfMemoryError錯(cuò)誤,使程序異常終止羞反,也不會(huì)靠隨意回收具有強(qiáng)引用的對(duì)象來(lái)解決內(nèi)存不足問(wèn)題布朦。
軟引用(SoftReference):
如果一個(gè)對(duì)象只具有軟引用,那就類(lèi)似于可有可物的生活用品昼窗。如果內(nèi)存空間足夠是趴,垃圾回收器就不會(huì)回收它,如果內(nèi)存 空間不足了澄惊,就會(huì)回收這些對(duì)象的內(nèi)存唆途。只要垃圾回收器沒(méi)有回收它,該對(duì)象就可以被程序使用掸驱。軟引用可用來(lái)實(shí)現(xiàn)內(nèi)存敏感的高速緩存肛搬。 軟引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果軟引用所引用的對(duì)象被垃圾回收毕贼,Java虛擬機(jī)就會(huì)把這個(gè)軟引用加入到與之關(guān)聯(lián) 的引用隊(duì)列中温赔。
弱引用(WeakReference):
如果一個(gè)對(duì)象只具有弱引用,那就類(lèi)似于可有可物的生活用品帅刀。弱引用與軟引用的區(qū)別在于:只具有弱引用的對(duì)象擁有更短暫的生命周期让腹。在垃圾回收器線(xiàn)程掃描它所管轄的內(nèi)存區(qū)域的過(guò)程中,一旦發(fā)現(xiàn)了只具有弱引用的對(duì)象扣溺,不管當(dāng)前內(nèi)存空間足夠與否骇窍,都會(huì)回收它的內(nèi)存。不過(guò)锥余,由于垃圾回收器是一個(gè)優(yōu)先級(jí)很低的線(xiàn)程腹纳, 因此不一定會(huì)很快發(fā)現(xiàn)那些只具有弱引用的對(duì)象。 弱引用可以和一個(gè)引用隊(duì)列(ReferenceQueue)聯(lián)合使用,如果弱引用所引用的對(duì)象被垃圾回收嘲恍,Java虛擬機(jī)就會(huì)把這個(gè)弱引用加入到與之關(guān)聯(lián)的引用隊(duì)列中足画。
虛引用(PhantomReference):
“虛引用”顧名思義,就是形同虛設(shè)佃牛,與其他幾種引用都不同淹辞,虛引用并不會(huì)決定對(duì)象的生命周期。如果一個(gè)對(duì)象 僅持有虛引用俘侠,那么它就和沒(méi)有任何引用一樣象缀,在任何時(shí)候都可能被垃圾回收。 虛引用主要用來(lái)跟蹤對(duì)象被垃圾回收的活動(dòng)爷速。虛引用與軟引用和弱引用的一個(gè)區(qū)別在于:虛引用必須和引用隊(duì)列 (ReferenceQueue)聯(lián)合使用央星。當(dāng)垃圾回收器準(zhǔn)備回收一個(gè)對(duì)象時(shí),如果發(fā)現(xiàn)它還有虛引用惫东,就會(huì)在回收對(duì)象的內(nèi)存之前莉给,把這個(gè)虛引用加入到與之 關(guān)聯(lián)的引用隊(duì)列中。程序可以通過(guò)判斷引用隊(duì)列中是否已經(jīng)加入了虛引用廉沮,來(lái)了解被引用的對(duì)象是否將要被垃圾回收颓遏。程序如果發(fā)現(xiàn)某個(gè)虛引用已經(jīng)被加入到引用隊(duì) 列,那么就可以在所引用的對(duì)象的內(nèi)存被回收之前采取必要的行動(dòng)废封。
四州泊、對(duì)象的死亡過(guò)程
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象丧蘸,也不是一定會(huì)死亡的漂洋,它們暫時(shí)都處于“緩刑”階段,要真正宣告一個(gè)對(duì)象“死亡”力喷,至少要經(jīng)歷兩次標(biāo)記過(guò)程:
如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi)有與 GC Roots相連接的引用鏈刽漂,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行finaliza()
方法弟孟。當(dāng)對(duì)象沒(méi)有覆蓋finaliza()
方法贝咙,或者finaliza()
方法已經(jīng)被虛擬機(jī)調(diào)用過(guò),虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”拂募。
如果這個(gè)對(duì)象被判定為有必要執(zhí)行finaliza()
方法庭猩,那么此對(duì)象將會(huì)放置在一個(gè)叫做F-Queue
的隊(duì)列中,并在稍后由一個(gè)虛擬機(jī)自動(dòng)建立的陈症、低優(yōu)先級(jí)的Finalizer
線(xiàn)程去執(zhí)行它蔼水。這里所謂的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)此方法,但并不承諾會(huì)等待它運(yùn)行結(jié)束录肯,原因是:如果一個(gè)對(duì)象在finaliza()
方法中執(zhí)行緩慢趴腋,或者發(fā)生了死循環(huán)(更極端的情況),將很可能導(dǎo)致F-Queue
隊(duì)列中的其它對(duì)象永久處于等待,甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰优炬。
finaliza()
方法是對(duì)象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)颁井,稍后GC將對(duì)F-Queue
隊(duì)列中的對(duì)象進(jìn)行第二次小規(guī)模的標(biāo)記。如果對(duì)象想在finaliza()
方法中成功拯救自己蠢护,只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可雅宾,例如把自己(this關(guān)鍵字)賦值給某個(gè)類(lèi)變量或者對(duì)象的成員變量,這樣在第二次標(biāo)記時(shí)它將被移出“即將回收”的集合葵硕;如果對(duì)象這時(shí)候還沒(méi)有逃脫秀又,基本上它就真的被回收了。下面是《深入理解Java虛擬機(jī)》中的一個(gè)代碼示例:
/**
* 此代碼演示了兩點(diǎn):
* 1.對(duì)象可以在被GC時(shí)自我拯救贬芥。
* 2.這種自救的機(jī)會(huì)只有一次吐辙,因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次
* @author zzm
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//對(duì)象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停0.5秒蘸劈,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面這段代碼與上面的完全相同昏苏,但是這次自救卻失敗了
SAVE_HOOK = null;
System.gc();
// 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停0.5秒威沫,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
運(yùn)行結(jié)果
值得注意的是贤惯,如果代碼中有兩段一模一樣的代碼段,執(zhí)行結(jié)果卻是一次逃脫成功棒掠,一次失敗孵构。這是因?yàn)槿魏我粋€(gè)對(duì)象的finalize()
方法都只會(huì)被系統(tǒng)調(diào)用一次,如果對(duì)象面臨下一次回收烟很,它的finalize()
方法不會(huì)再被執(zhí)行颈墅,因此第二次逃脫行動(dòng)失敗。
需要說(shuō)明的是雾袱,使用finalize()
方法來(lái)“拯救”對(duì)象是不值得提倡的恤筛,因?yàn)樗皇荂/C++中的析構(gòu)函數(shù),而是Java剛誕生時(shí)為了使C/C++程序員更容易接受它所做的一個(gè)妥協(xié)芹橡。它的運(yùn)行代價(jià)高昂毒坛,不確定性大,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序林说。finalize()
能做的工作煎殷,使用try-finally
或者其它方法都更適合、及時(shí)腿箩,所以筆者建議大家可以忘掉此方法存在豪直。
五、回收方法區(qū)
很多人以為方法區(qū)(或者HotSopt VM中的永久代)是沒(méi)有垃圾收集的度秘,Java虛擬機(jī)規(guī)范中確實(shí)說(shuō)過(guò)可以不要求虛擬機(jī)在方法區(qū)實(shí)現(xiàn)垃圾收集顶伞,而且性?xún)r(jià)比一般較低饵撑,在對(duì)的新生代生一般能回收70%~95%的空間,而永久代遠(yuǎn)低于此唆貌。
永久代的垃圾手機(jī)主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類(lèi)滑潘。 回收廢棄常量與回收J(rèn)ava堆中的對(duì)象非常相似。以常量池中字面量的回收為例锨咙,若字符串“abc”已經(jīng)進(jìn)入常量池中语卤,但當(dāng)前系統(tǒng)沒(méi)有任何String對(duì)象引用常量池中的“abc”常量,也沒(méi)有其他地方引用該字面量酪刀,若發(fā)生內(nèi)存回收粹舵,且必要的話(huà),該“abc”就會(huì)被系統(tǒng)清理出常量池骂倘。常量池中其他的類(lèi)(接口)眼滤、方法、字段的符號(hào)引用與此類(lèi)似历涝。
無(wú)用的類(lèi)需要滿(mǎn)足3個(gè)條件:
(1)該類(lèi)所有的實(shí)例都已經(jīng)被回收诅需,即Java堆中不存在該類(lèi)的任何實(shí)例;
(2)加載該類(lèi)的ClassLoader已經(jīng)被回收荧库;
(3)該類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有在任何地方被引用堰塌,無(wú)法在任何地方通過(guò)反射訪(fǎng)問(wèn)該類(lèi)的方法。
虛擬機(jī)可以對(duì)滿(mǎn)足上述3個(gè)條件的無(wú)用類(lèi)進(jìn)行回收分衫,此處僅僅是“可以”场刑,而并不是和對(duì)象一樣(不使用了就必然回收)