GC前,判斷對象是否死亡

本文基於jdk 1.7,參考《深入了理解java虛擬機》一書

一、概述

垃圾自動回收機制是java語言相比c++的一大特性,但垃圾收集並不是java語言的伴生物,GC的歷史比java更加久遠。爲什麼我們要了解GC和內存分配呢,當需要排查各種內存溢出、內存泄露問題時,當垃圾收集成爲系統達到更大併發量的瓶頸時,我們就需要對垃圾自動回收機制進行調優。

二、判斷對象是否需要回收

1、強軟弱虛引用

判斷對象是否存活與“引用有關”,在jdk 1.2之前,引用是reference類型的數據中存儲的代表另一塊內存的起始地址的數值。jdk 1.2之後,java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這四種引用強度依次減弱。

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

2、對象已死嗎

java堆裏存放着幾乎所有的對象實例,垃圾收集器對堆進行回收前,第一件事就是判斷那些對象還活着,哪些已死去(即不可能再被任何途徑使用的對象)。
判斷對象是否死亡理論上有兩種方法:引用計數法、可達性分析。

(1)引用計數法

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值加1,當引用失效時,計數器值減1;任何時候計數器值爲0的對象就是不可能在被使用的。
引用計數法優點是實現簡單、判斷效率高,但缺陷是沒有解決對象循環引用的問題。比如兩個對象相互引用,除此之外沒有任何地方引用到這兩個對象,這兩個對象應當被回收,但是按照引用計數法它們的引用計數器值不爲0,無法通知GC回收它們。
java虛擬機不採用引用計數法判斷對象是否存活。

(2)可達性分析算法

可達性分析算法是主流java虛擬機判斷對象是否存活採用的手段。這個算法的基本思路是通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索通過的路徑稱爲引用鏈,當一個對象沒有任何引用鏈與其相連時(也叫對象不可達),則證明此對象是不可用的。
在java語言中,可作爲GC Roots的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(Native方法)引用的對象。

3、起死回生:finalize()方法

要真正宣告一個對象死亡,至少需要經歷兩次標記過程。
通過可達性分析,不可達的對象將會被第一次標記並進行一次篩選,篩選的條件是判斷對象是否需要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者該對象的finalize方法已經被虛擬機調用過,虛擬機將這兩種情況視爲沒有必要執行。(任何對象的finalize()方法都只會被系統調用一次!)
如果篩選結果是有必要執行finalize()方法,那麼這個對象將被放置到一個叫做F-Queue的隊列之中,並在稍後由虛擬機建立的低優先級的Finalizer線程去執行finalize()方法。
稍後GC將對F-Queue隊列中的對象進行小規模標記,如果該對象在finalize()方法中重新與引用鏈上的任一對象關聯上,則對象成功在finalize方法中拯救了自己,GC會在第二次標記過程中將此對象移出“即將回收”的集合。
代碼示例:

public class TestFinalize {
    public static TestFinalize testFinalize = null;
    public void isAlive(){
        System.out.println("I am alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method excuted");
        testFinalize = this;
    }

    public static void main(String[] args) throws InterruptedException {
        testFinalize = new TestFinalize();

        testFinalize =null;
        System.gc();//此方法會調用finalize方法
        //因爲執行finalize方法的線程優先級很低,停頓0.5秒等它執行
        Thread.sleep(500);
        if (testFinalize!=null){
            testFinalize.isAlive();
        }else{
            System.out.println("I am dead");
        }

        //與上面代碼完全相同,但是這次自救失敗了,原因是任何對象的finalize方法只會被執行一次
        testFinalize =null;
        System.gc();//此方法會調用finalize方法
        //因爲執行finalize方法的線程優先級很低,停頓0.5秒等它執行
        Thread.sleep(500);
        if (testFinalize!=null){
            testFinalize.isAlive();
        }else{
            System.out.println("I am dead");
        }
    }
}

執行結果:
在這裏插入圖片描述

4、方法區回收

jdk1.8以前存在方法區,HotSpot虛擬機中也叫永久代,java的垃圾收集主要針對堆內存,但是永久代的內存也是可以發生垃圾回收的,永久代垃圾回收的效率遠低於堆內存,HotSpot虛擬機提供了 -Xnoclassgc參數控制。
永久代的垃圾收集主要有兩部分內容:回收廢棄常量、類的卸載。
常量池中的廢棄常量和類、方法、字段的符號引用沒有在任何地方引用到,而且有必要的情況下,會被回收。
類的卸載要同時滿足一下3個條件纔會被回收:

  • 該類的所有實例都被回收
  • 加載該類的類加載器已被回收
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

滿足以上條件僅僅說明類可以被回收,要不要回收還需要參數控制以及看虛擬機是否支持。

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