深入理解Java虛擬機---垃圾收集算法
- 如何判定對象是否存活
- 引用計數法
- 可達性分析法
- Java引用類型
- 垃圾回收算法
- 標記-清除算法
- 復制算法
- 標記-整理算法
- 分代收集算法
- HotSpot的算法實現
- 枚舉根節點
- 安全點
- 安全區域
如何判定對象是否存活
引用計數法
引用計數算法利用額外的內存空間來進行計數,在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。
可達性分析法
基本思路就是通過一系列稱為GC Roots的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走過的路徑稱為“引用鏈”, 如果某個對象到GC Roots間沒有任何引用鏈相連,則此對象不可能再被使用。
在可達性分析算法中判定為不可達的對象,至少要經歷兩次標記過程才能被銷毀:(1)如果對象在進行可達性分析后 發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,(2)隨后進行一次篩選, 篩選的條件是此對象是否有必要執行finalize()方法。 假如對象:沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機調用過了一次 ,虛擬機將此情況視為“沒有必要執行”。
Java技術體系中,可作為GC Roots的對象包括以下幾種:
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象 ,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
(2)方法區中類靜態屬性引用的對象 ,如Java類的引用類型靜態變量。
(3)在方法區中常量引用的對象 ,如字符串常量池里的引用。
(4)在本地方法棧中JNI引用的對象。
Java引用類型
Java中引用分為以下四類:
強引用
就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用
用來描述一些還有用,但并非必需的對象。對于軟引用關聯著的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中并進行第二次回收。如果這次回收還是沒有足夠的內存,才會拋出內存溢出異常。在JDK1.2之后,提供了 SoftReference類來實現軟引用。
弱引用
用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在 JDK 1.2之后,提供了WeakReference類來實現弱引用。
虛引用
稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是希望能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2之后,提供了PhantomReference類來實現虛引用。
垃圾回收算法
標記-清除算法
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象。
標記清除算法主要有兩個不足:
(1)效率問題,標記和清除兩個過程的效率都不高;
(2)空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
復制算法
將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
標記-整理算法
標記-整理”算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
分代收集算法
根據對象存活周期的不同將內存劃分為幾塊。一般把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而在老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。
HotSpot的算法實現
枚舉根節點
在HotSpot的實現中,使用一組稱為OopMap的數據結構來得知哪些地方存放著對象引用,在類加載完成的時候,HotSpot就把對象內什么偏移量上是什么類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息。
安全點
在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉,但可能導致引用關系變化或者OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,GC的空間成本將會變高。
HotSpot也的確沒有為每條指令都生成OopMap,只是在“特定的位置”記錄了這些信息,這些位置稱為安全點,即程序執行時并非在所有地方都能停頓下來開始GC,只有在達到安全點時才能暫停。安全點的選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的——因為每條指令執行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執行”的最明顯特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令才會產生安全點。
對于安全點,另一個需要考慮的問題是如何在GC發生時讓所有線程(不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供選擇:搶先式中斷和主動式中斷,其中搶先式中斷不需要線程的執行代碼主動去配合,在GC發生時,首先把所有的線程全部中斷,如果發現有線程中斷的地方不再安全點上,就恢復線程,讓它“跑”到安全點上。現在幾乎沒有虛擬機實現采用搶先式中斷來暫停線程從而響應GC事件的。而主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標志,各個線程執行時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。
安全區域
安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的安全點,但是程序沒有被分配到CPU時間,典型的例子就是線程處于Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。對于這種情況,就需要安全區域來解決。安全區域是指在一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的。在線程執行到安全區域中的代碼時,首先標記自己已經進入了安全區域,那樣,當在這段時間里JVM要發起GC時,就不用管標志自己為安全區域狀態的線程了。在線程要離開安全區域時,它要檢查系統是否已經完成了根節點枚舉,如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開安全區域的信號為止。
來源:《深入理解Java虛擬機》