天天看點

jvm(6) -- 如何判定對象為垃圾對象(是否存活)

文章目錄

  • ​​1、引用計數算法(Reference Counting)​​
  • ​​2、可達性分析算法(GC Roots Analysis):​​
  • ​​3.finalize()方法最終判定對象是否存活​​
  • ​​1).第一次标記并進行一次篩選。​​
  • ​​2).第二次标記​​

堆中幾乎存放着Java世界中所有的對象執行個體,垃圾收集器在對堆回收之前,第一件事情就是要确定這些對象哪些還“存活”着,哪些對象已經“死去”(即不可能再被任何途徑使用的對象)。

1、引用計數算法(Reference Counting)

很多教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器減1;任何時刻計數器都為0的對象就是不可能再被使用的。

引用計數算法的實作簡單,判斷效率也很高,在大部分情況下它都是一個不錯的算法。但是Java語言中沒有選用引用計數算法來管理記憶體,其中最主要的一個原因是它很難解決對象之間互相循環引用的問題。

例如:

在testGC()方法中,對象objA和objB都有字段instance,指派令objA.instance=objB及objB.instance=objA,除此之外這兩個對象再無任何引用,實際上這兩個對象都已經不能再被通路,但是它們因為互相引用着對象方,異常它們的引用計數都不為0,于是引用計數算法無法通知GC收集器回收它們。

列印GC詳細資訊:

-XX:+PrintGCDetails

jvm(6) -- 如何判定對象為垃圾對象(是否存活)

idea配置: Run -> Run configurations -> java應用名 -> arguments ->VM arguments,加入jvm參數就行

輸入VM arguments參數:

​-Xms20m --jvm堆的最小值​

​​ -​

​Xmx20m --jvm堆的最大值​

​​

​-XX:+PrintGCTimeStamps -- 列印出GC的時間資訊​

​ -​

​XX:+PrintGCDetails --列印出GC的詳細資訊​

​ -​

​verbose:gc --開啟gc日志​

​ -​

​Xloggc:d:/gc.log -- gc日志的存放位置​

​ -​

​Xmn10M -- 新生代記憶體區域的大小​

​ -​

​XX:SurvivorRatio=8 --新生代記憶體區域中Eden和Survivor的比例​

/**
 * 執行後,objA和objB會不會被GC呢?
 */
public class ReferenceCountingGC {
 
    public Object instance = null;
 
    /**
     * 這個成員屬性的唯一意義就是占點記憶體,以便能在GC日志中看清楚是否被回收過
     */
    private byte[] bigSize = new byte[2 * 1024 * 1024];
 
    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
 
        objA = null;
        objB = null;
 
        //假設在這行發生了GC,objA和ojbB是否被回收
        System.gc();
        //1.8采用Parallel GC
    }
}      

[GC (Allocation Failure) [DefNew: 3707K->512K(4928K), 0.0041893 secs] 3707K->1214K(15872K), 0.0042359 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

[Full GC (System.gc()) [Tenured: 702K->1216K(10944K), 0.0039473 secs] 5529K->1216K(15872K), [Metaspace: 2223K->2223K(4480K)], 0.0039998 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap

def new generation total 4992K, used 45K [0x04600000, 0x04b60000, 0x09b50000)

eden space 4480K, 1% used [0x04600000, 0x04612f10, 0x04a60000)

from space 512K, 0% used [0x04a60000, 0x04a60000, 0x04ae0000)

to space 512K, 0% used [0x04ae0000, 0x04ae0000, 0x04b60000)

tenured generation total 10944K, used 1216K [0x09b50000, 0x0a600000, 0x14600000)

the space 10944K, 11% used [0x09b50000, 0x09c80108, 0x09c80200, 0x0a600000)

Metaspace used 2228K, capacity 2280K, committed 2368K, reserved 4480K

在運作結果中可以看到:

GC日志中包含"3707K->512K",老年代從3707K(大約3.5M,其實就是objA與objB)變為了512K,意味着虛拟并沒有因為這兩個對象互相引用就不回收它們,這也​​

​證明虛拟機并不是通過通過引用計數算法來判斷對象是否存活的​

​​。

大家可以看到對象進入了老年代,

但是大家都知道,對象剛建立的時候是配置設定在新生代中的,要進入老年代預設年齡要到了new objA才行,但這裡objA與objB卻進入了老年代。

這是因為Java堆區會動态增長,剛開始時堆區較小,對象進入老年代還有一規則,當Survior空間中同一代的對象大小之和超過Survior空間的一半時,對象将直接進行老年代。

2、可達性分析算法(GC Roots Analysis):

​主流用這個判斷。​

在主流的商用程式語言中(Java和C#),都是使用可達性分析算法判斷對象是否存活的。這個算法的基本思路就是通過一系列名為"GC Roots"的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的,下圖對象object5, object6, object7雖然有互相判斷,但它們到GC Roots是不可達的,是以它們将會判定為是可回收對象。

jvm(6) -- 如何判定對象為垃圾對象(是否存活)

在Java語言裡,可作為GC Roots對象的包括如下幾種:

  • a.虛拟機棧(棧桢中的本地變量表)中的引用的對象
  • b.方法區中的類靜态屬性引用的對象
  • c.方法區中的常量引用的對象
  • d.本地方法棧中JNI的引用的對象

3.finalize()方法最終判定對象是否存活

即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經曆再次标記過程。

标記的前提是對象在進行可達性分析後發現沒有與GC Roots相連接配接的引用鍊。

1).第一次标記并進行一次篩選。

篩選的條件是此對象是否有必要執行finalize()方法。

當對象沒有覆寫finalize方法,或者finzlize方法已經被虛拟機調用過,虛拟機将這兩種情況都視為“沒有必要執行”,對象被回收。

2).第二次标記

如果這個對象被判定為有必要執行finalize()方法,那麼這個對象将會被放置在一個名為:F-Queue的隊列之中,并在稍後由一條虛拟機自動建立的、低優先級的Finalizer線程去執行。這裡所謂的“執行”是指虛拟機會觸發這個方法,但并不承諾會等待它運作結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),将很可能會導緻F-Queue隊列中的其他對象永久處于等待狀态,甚至導緻整個記憶體回收系統崩潰。

Finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC将對F-Queue中的對象進行第二次小規模标記,如果對象要在finalize()中成功拯救自己----隻要重新與引用鍊上的任何的一個對象建立關聯即可,譬如把自己指派給某個類變量或對象的成員變量,那在第二次标記時它将移除出“即将回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。

流程圖如下:

jvm(6) -- 如何判定對象為垃圾對象(是否存活)
/**
 * 此代碼示範了兩點
 * 1、對象可以在被GC時自我拯救
 * 2、這種自救的機會隻有一次,因為一個對象的finalize()方法最多隻能被系統自動調用一次。
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
 
    public void isAlive() {
        System.out.println("yes, I am still alive");
    }
 
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
 
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
 
        //對象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
 
        //因為finalize方法優先級很低,所有暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no ,I am dead QAQ!");
        }
 
        //-----------------------
        //以上代碼與上面的完全相同,但這次自救卻失敗了!!!
        SAVE_HOOK = null;
        System.gc();
 
        //因為finalize方法優先級很低,所有暫停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no ,I am dead QAQ!");
        }
    }
}      

輸出:

finalize method executed!

yes, I am still alive

no ,I am dead QAQ!

了解這篇文章,還要有對finalize的認識: