Java 垃圾收集之判斷對象是否存活

判斷對象是否存活

垃圾收集器工作前需要判斷對象是否存活,那麼垃圾收集器是如何判斷的?判斷標準又是什麼?前面講了 Java 內存區域各個部分,其中程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅:棧中的棧幀隨着方法進入和退出執行出棧和入棧操作。每個棧幀中分配多少內存基本是在類結構確定下來時就已知的,因此這幾個區域內存分配以及回收都具備確定性,不需要過多考慮回收問題,因爲方法結束或者線程結束時,內存就跟着回收了。在 Java 堆中,類需要的空間不同,內存需要動態分配,垃圾收集器關注的也是這部分的內存。下面介紹兩種判斷對象是否存活的方法。

引用計數法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1;任何時候計數器值爲0的對象就是不可能再被使用的。
引用計數法實現簡單,判斷效率也高,但是主流的 Java 虛擬機中並沒有選用引用計數法來管理內存,最主要的原因就是它很難解決對象之間相互循環引用的問題。
舉例說明:

public class ReferenceCountingGC {
    private Object instance = null;
    /**
     * -XX:+PrintGCDetails 打印GC日誌
     * @param args
     */
    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        System.gc();
    }
}

運行結果:
[GC (System.gc()) [PSYoungGen: 3952K->808K(75776K)] 3952K->816K(249344K), 0.0103407 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 808K->0K(75776K)] [ParOldGen: 8K->710K(173568K)] 816K->710K(249344K), [Metaspace: 3420K->3420K(1056768K)], 0.0042775 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 75776K, used 1951K [0x000000076b580000, 0x0000000770a00000, 0x00000007c0000000)
eden space 65024K, 3% used [0x000000076b580000,0x000000076b767c68,0x000000076f500000)
from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
to space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
ParOldGen total 173568K, used 710K [0x00000006c2000000, 0x00000006cc980000, 0x000000076b580000)
object space 173568K, 0% used [0x00000006c2000000,0x00000006c20b1840,0x00000006cc980000)
Metaspace used 3440K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 374K, capacity 388K, committed 512K, reserved 1048576K
從運行結果可以看出,GC日誌中包含“3952K->808K”,意味着這兩個對象被回收了,這也說明虛擬機不是通過引用計數法判斷對象是否存活的。

可達性分析算法

Java是通過可達性分析來判定對象是否存活的。這個算法的基本思想是通過一系列的 “GC Roots” 的對象作爲起始點,從這些節點向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連(對象不可達)時,則證明對象是不可用的。如圖所示,對象 object5、object6、object7 雖然相互關聯,但它們到 GC Roots 是不可達的,所以會被判定爲可回收對象。
在這裏插入圖片描述
Java 中可作爲 GC Roots 的對象包括下面幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  2. 方法區中類靜態屬性引用的對象。
  3. 方法區中常量引用的對象
  4. 本地方法棧中 JNI(即一般說的 Native 方法)引用的對象。

Java 中的引用

判斷對象是否存活都是和“引用”有關的,在 JDK 1.2 之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  • 強引用就是指在程序代碼中普遍存在的,類似 “Object obj = new Object()” 這類的引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
  • 軟引用是用來描述一些還有用但非必需的對象。對於軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。
  • 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

對象何時死亡

即使在可達性分析算法中不可達的對象,也並非是 “非死不可” 的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少需要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有 GC Roots 相關聯的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。
如果對象被判定爲有必要執行 finalize() 方法,那麼這個對象將會放置在一個叫做 F-Queue 的隊列之中,並在之後由一個虛擬機自動建立的、低優先級的 Finalizer 線程去執行它。稍後會進行二次標記,如果這是對象還沒有被使用,那麼它基本上就真的被回收了。

回收方法區

方法區(HotSpot 中的永久代)中的回收是性價比較低的,永久代的垃圾回收主要回收兩部分內容:廢棄常量和無用的類。對於廢棄常量,只要沒有任何對象引用常量就會被回收,但是要同時滿足以下三個條件才能算是“無用的類”:

  1. 該類所有實例都已被回收,也就是Java堆中不存在該類的任何實例。
  2. 加載該類的 ClassLoader 已被回收。
  3. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述3個條件的無用類進行回收,是否對類進行回收,HotSpot 虛擬機提供了 -Xnoclassgc 參數進行控制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章