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