文章目錄
對(duì)象是否存活?
垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是 是要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”。
判斷對(duì)象是否存活有以下兩種算法:
- 引用計(jì)數(shù)法
- 可達(dá)性分析法
引用計(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ù)法原理簡(jiǎn)單,判定效率也很高,但單純的引用計(jì)數(shù)就很難解決 對(duì)象之間相互循環(huán)引用的問題 。
例如,存在兩個(gè)對(duì)象象 objA 和 objB ,他們都有字段instance,令 objA.instance=objB;objB.instance=objA 。除此之外,這兩個(gè)對(duì)象再無任何引用,實(shí)際上這兩個(gè)對(duì)象已經(jīng) 經(jīng)不可能再被訪問,但是它們因?yàn)榛ハ嘁弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為零,引用計(jì)數(shù)算法也就無法回收它們。
可達(dá)性分析法
通過 一系列稱為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過 程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GC Roots間沒有任何引用鏈相連, 或者用圖論的話來說就是從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。
固定可作為GC Roots的對(duì)象包括以下幾種:
- 在方法區(qū)中類靜態(tài)屬性引用的對(duì)象;
- 在方法區(qū)中常量引用的對(duì)象;
- 在本地方法棧中(即Native方法)引用的對(duì)象;
- 所有被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象;
- 反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
強(qiáng)、軟、弱、虛
finalize()
即使在可達(dá)性分析算法中判定為不可達(dá)的對(duì)象,也不是“非死不可”的,這時(shí)候它們暫時(shí)還處于“緩 刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:
【注】
- 任何一個(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨 下一次回收,它的finalize()方法不會(huì)被再次執(zhí)行;
- 虛擬機(jī)會(huì)觸發(fā)finalize方法開始運(yùn)行,但并不承諾一定會(huì)等待它運(yùn)行結(jié)束。
垃圾收集算法
分代收集理論
分代收集指的是:垃圾收集器應(yīng)該將Java堆劃分 除去不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即 對(duì)象熬過垃圾收集過程的次數(shù) )分配到不同地區(qū) 域之中存儲(chǔ)。
據(jù)此,一般至少將把Java堆劃分為 新生代 (Young Generation)和 老年代 (Old Generation)兩個(gè)區(qū)域。在新生代中,每次都是垃圾收集 時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,而每次回收后存活的少量對(duì)象,將會(huì)逐步晉升到老年代中存放。
三個(gè)假說:
標(biāo)記—清除算法
標(biāo)記-清除算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回復(fù) 收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過來,標(biāo)記存活的對(duì)象,統(tǒng)一回收所有未被標(biāo)記的對(duì)象。
缺點(diǎn):
- 執(zhí)行效率不穩(wěn)定 ,如果Java堆中包含大量對(duì) 象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過程 程的執(zhí)行效率都隨對(duì)象數(shù)量增長(zhǎng)而降低;
- 內(nèi)存空間的碎片化問題 ,標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找 到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
標(biāo)記-復(fù)制算法
標(biāo)記-復(fù)制算法:將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
如果內(nèi)存中多數(shù)對(duì)象都是存 活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開銷,但對(duì)于多數(shù)對(duì)象都是可回收的情況,算法需要復(fù) 制的就是占少數(shù)的存活對(duì)象,而且每次都是針對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,分配內(nèi)存時(shí)也就不用考慮有 空間碎片的復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配即可。這樣實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效,但 可用內(nèi)存縮小為了原來的一半 。
標(biāo)記-整理算法
標(biāo)記-整理算法:其中的標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可 回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
缺點(diǎn):
- 移動(dòng)存活對(duì)象并更新 所有引用這些對(duì)象的地方將會(huì)是一種極為負(fù)重的操作,而且這種對(duì)象移動(dòng)操作必須全程暫停用戶應(yīng)用 程序才能進(jìn)行。
由以上幾種算法可以看出:是否移動(dòng)對(duì)象都存在弊端,移動(dòng)則內(nèi)存回收時(shí)會(huì)更復(fù)雜,不移動(dòng)則內(nèi)存分配時(shí)會(huì) 更復(fù)雜。從垃圾收集的停頓時(shí)間來看,不移動(dòng)對(duì)象停頓時(shí)間會(huì)更短,甚至可以不需要停頓,但是從整 個(gè)程序的吞吐量來看,移動(dòng)對(duì)象會(huì)更劃算。
此外就出現(xiàn)了另一種解決方案:
- 可以不在內(nèi)存分配和訪問上增加太大額外負(fù)擔(dān),做法是讓虛 擬機(jī)平時(shí)多數(shù)時(shí)間都采用標(biāo)記-清除算法,暫時(shí)容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng) 大到影響對(duì)象分配時(shí),再采用標(biāo)記-整理算法收集一次,以獲得規(guī)整的內(nèi)存空間。
垃圾回收算法細(xì)節(jié)實(shí)現(xiàn)
根節(jié)點(diǎn)枚舉
在可達(dá)性分析中固定可作為GC Roots的節(jié)點(diǎn)主要在 全局性的引用 (例如常量或類靜態(tài)屬性)與 執(zhí)行上下文 (例如 棧幀中的本地變量表)中,但查找過程要做到高效并非一件容易的事情。也會(huì)造成“Stop The World”的問題。
HoeSpot虛擬機(jī)的解決方案是:使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個(gè)目的。一旦類加載動(dòng)作完成的時(shí)候, HotSpot就會(huì)把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來,在即時(shí)編譯過程中,也 會(huì)在特定的位置記錄下棧里和寄存器里哪些位置是引用。這樣收集器在掃描時(shí)就可以直接得知這些信 息了,并不需要真正一個(gè)不漏地從方法區(qū)等GC Roots開始查找。
在OopMap的協(xié)助下,HotSpot可以快速準(zhǔn)確地完成GC Roots枚舉,但如果為每一條指令都生成 對(duì)應(yīng)的OopMap,那將會(huì)需要大量的額外存儲(chǔ)空間。
所以HotSpot虛擬機(jī)并不會(huì)為每條指令都生成OopMap,只是在“特定的位置”記錄 了這些信息,這些位置被稱為 安全點(diǎn) 。
由于安全點(diǎn)的存在決定了用戶程序執(zhí)行時(shí),并非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強(qiáng)制要求必須執(zhí)行到達(dá)安全點(diǎn)后才 能夠暫停。
那么,如何在垃圾收集發(fā)生時(shí)讓所有線程都跑到最近的安全點(diǎn),然后停頓下來呢?這里提供了兩種方案:
安全區(qū)域
安全區(qū)域是指能夠確保在某一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化,因此,在這個(gè)區(qū)域中任 意地方開始垃圾收集都是安全的。我們也可以把安全區(qū)域看作被擴(kuò)展拉伸了的安全點(diǎn)。
當(dāng)用戶線程執(zhí)行到安全區(qū)域里面的代碼時(shí),首先會(huì)標(biāo)識(shí)自己已經(jīng)進(jìn)入了安全區(qū)域,那樣當(dāng)這段時(shí) 間里虛擬機(jī)要發(fā)起垃圾收集時(shí)就不必去管這些已聲明自己在安全區(qū)域內(nèi)的線程了。當(dāng)線程要離開安全 區(qū)域時(shí),它要檢查虛擬機(jī)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉,如果完成了,那線程就當(dāng)作沒事發(fā)生過,繼續(xù)執(zhí)行;否則它就必須一直等待,直到收到可以 離開安全區(qū)域的信號(hào)為止。
記憶集與卡表
記憶集是一種用于記錄從非收集區(qū)域指向收集區(qū)域的指針集合的抽象數(shù)據(jù)結(jié)構(gòu)。
它是為了解決分代收集理論中,對(duì)象跨代引用所帶來的問題,而在新生代中建 立了名為記憶集的數(shù)據(jù)結(jié)構(gòu),用以避免把整個(gè)老年代加進(jìn)GC Roots掃描范圍。
卡表是實(shí)現(xiàn)記憶集的一種方式。
記憶集是一種“抽象”的數(shù)據(jù)結(jié)構(gòu),它只定義了記憶集的行為意圖,并沒有定義其行為的具體實(shí)現(xiàn)??ū砭褪怯洃浖囊环N具體實(shí)現(xiàn),它定義了記憶集的記錄精度、與堆內(nèi)存的映射關(guān)系等。