Java虛擬機(jī)-什么樣的對(duì)象能被判“死刑”
在Java堆中存放著幾乎所有的對(duì)象實(shí)例,在程序運(yùn)行時(shí),堆中的對(duì)象不斷地變多,這樣下去遲早會(huì)將整個(gè)堆放滿。所以要對(duì)堆進(jìn)行垃圾回收,回收“已死”的對(duì)象,釋放內(nèi)存空間。
那在回收它們之前,如何判斷他們“已死”呢?(即該對(duì)象不可能再被任何途徑使用)
引用計(jì)數(shù)算法
這個(gè)算法是這樣的:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它,計(jì)數(shù)器就+1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就-1;自然,當(dāng)計(jì)數(shù)器值為0時(shí),就說(shuō)明沒(méi)有任何地方會(huì)使用到該對(duì)象,那么這個(gè)對(duì)象就是“已死”的。
引用計(jì)數(shù)器算法實(shí)現(xiàn)簡(jiǎn)單,判定高效,也有一些著名的應(yīng)用案例,但是主流的Java虛擬機(jī)并沒(méi)有采用這個(gè)算法來(lái)管理內(nèi)存------它很難解決對(duì)象之間的循環(huán)引用問(wèn)題。
public class ReferenceCountGC {
? //?占據(jù)內(nèi)存
? private byte[] memory = new byte[1 << 20];
? public ReferenceCountGC instance;
? public static void main(String[] args) {
? ? ?ReferenceCountGC rca = new ReferenceCountGC(),
? ? ? ? ? ?rcb = new ReferenceCountGC();
? ? ?
? ? ?rca.instance = rcb;
? ? ?rcb.instance = rca;
? ? ?
? ? ?rca = null;
? ? ?rcb = null;
? ? ?System.gc();
? }
}
在上面這個(gè)例子中,我們已經(jīng)沒(méi)有任何途徑調(diào)用到cirA和cirB對(duì)象,但他們的引用計(jì)數(shù)器值都為1,無(wú)法被虛擬機(jī)回收。

可達(dá)性分析算法
對(duì)于上一個(gè)例子,實(shí)際的結(jié)果是虛擬機(jī)回收了這部分內(nèi)存,而虛擬機(jī)使用的正是可達(dá)性分析算法。

這個(gè)算法的基本思路就是通過(guò)一系列的“GC Roots”作為起始點(diǎn),從這些點(diǎn)向下搜索,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈(從GC Roots下搜索走過(guò)的路徑)時(shí),則說(shuō)明該對(duì)象不可達(dá)。不可達(dá)的對(duì)象將被判定為“死亡”。

那么,哪些對(duì)象可以作為“GC Roots”呢?固定作為GC Roots包括以下幾種:
虛擬機(jī)棧中引用的對(duì)象(在棧幀中本地變量表里)
方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
本地方法中的JNI(Native方法)引用的對(duì)象
虛擬機(jī)內(nèi)部的引用,如基本類(lèi)型對(duì)應(yīng)的Class對(duì)象,一些常駐的異常對(duì)象(NullPointExcepition、OutOfMemoryError)
被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象
反應(yīng)Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等
除了這些固定集合以外,根據(jù)用戶所選的垃圾收集器一季當(dāng)前回收的內(nèi)存區(qū)域的不同,還可以有其他對(duì)象“臨時(shí)性”地加入。
被判“死刑”對(duì)象的“掙扎”
一個(gè)對(duì)象被判斷“已死”(不可達(dá)),并不代表它不能繼續(xù)存活,要真正宣布一個(gè)對(duì)象的“死亡”,至少要經(jīng)歷兩次標(biāo)記過(guò)程??最多經(jīng)歷兩次標(biāo)記過(guò)程(書(shū)籍作者已將該問(wèn)題加入勘誤中)。
至少要經(jīng)歷兩次標(biāo)記過(guò)程,對(duì)于這句話我對(duì)書(shū)中的說(shuō)法有些疑慮。如果一個(gè)對(duì)象沒(méi)有覆蓋finalize()方法,那么在回收前,它只會(huì)經(jīng)歷第一次標(biāo)記過(guò)程,它不會(huì)進(jìn)入F-Queue隊(duì)列中,自然也不會(huì)經(jīng)歷第二次標(biāo)記過(guò)程。不知道作者為什么要說(shuō)至少要經(jīng)歷兩次標(biāo)記過(guò)程,可能是我哪里有理解錯(cuò)誤。
對(duì)象在進(jìn)行可達(dá)性分析后,發(fā)現(xiàn)沒(méi)有任何引用鏈與之鏈接(不可達(dá))將會(huì)進(jìn)行第一次標(biāo)記,然后會(huì)對(duì)標(biāo)記過(guò)的對(duì)象進(jìn)行一次篩選,篩選條件為是否有必要執(zhí)行finalize()方法, 當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法或者該方法已經(jīng)被調(diào)用過(guò),則會(huì)被判定為沒(méi)有必要執(zhí)行。如果被判定為有必要執(zhí)行,那么這個(gè)對(duì)象會(huì)被放入F-Queue隊(duì)列中,并在稍后由一個(gè)虛擬機(jī)建立的低優(yōu)先級(jí)的線程去執(zhí)行(這里的執(zhí)行指的是出發(fā)對(duì)象的finalize()方法,并不會(huì)等待方法結(jié)束,避免執(zhí)行時(shí)間過(guò)長(zhǎng)或者死循環(huán))。之后GC會(huì)對(duì)F-Queue中的對(duì)象進(jìn)行第二次標(biāo)記。
那么對(duì)象如何利用finalize()方法進(jìn)行最后的掙扎呢?只要對(duì)象在finalize()方法中與任何一個(gè)GC Roots可達(dá)的對(duì)象建立聯(lián)系(可以被它引用),那么它將在第二次標(biāo)記時(shí)被移出“即將回收”的集合,死里逃生。
再談引用
無(wú)論是通過(guò)引用計(jì)數(shù)法還是可達(dá)性分析法,判定對(duì)象是否存活都與引用有關(guān)。在JDK1.2以前,Java中對(duì)引用的定義很傳統(tǒng):如果reference類(lèi)型的數(shù)據(jù)中存儲(chǔ)的是另一塊內(nèi)存的起始地址,就稱這塊內(nèi)存為代表著一個(gè)引用。在這種定義下,對(duì)象只有被引用或者沒(méi)有被引用兩種狀態(tài)。
在JDK1.2以后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用,軟引用,弱引用,虛引用。
強(qiáng)引用:指代碼中普遍存在的,類(lèi)似"Object obj = new Object()"這類(lèi)引用。只要強(qiáng)引用還在,垃圾收集器就不會(huì)回收這個(gè)對(duì)象。
軟引用:用來(lái)描述一些有用,但并非必須的對(duì)象。對(duì)于軟引用關(guān)聯(lián)的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)對(duì)這些對(duì)象進(jìn)行二次回收。(JDK1.2之后提供了SoftReference類(lèi)實(shí)現(xiàn)軟引用)
弱引用:用來(lái)描述非必須的對(duì)象,被弱引用關(guān)聯(lián)的對(duì)象,只能生存到下一次垃圾收集發(fā)生之前。(JDK1.2之后,提供了WeakReference類(lèi)來(lái)實(shí)現(xiàn)弱引用)
虛引用:也稱為“幽靈引用”或“幻影引用”,是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用存在,完全不會(huì)對(duì)其生存時(shí)間造成影響,也無(wú)法通過(guò)虛引用來(lái)獲取一個(gè)對(duì)象的實(shí)例。為對(duì)象設(shè)置虛引用的唯一目的是能在這個(gè)對(duì)象被回收前收到一個(gè)系統(tǒng)通知。(JDK1.2之后,提供了PhantomReference類(lèi)實(shí)現(xiàn)虛引用)