Java虛擬機與垃圾回收知識點梳理(二)

如何判斷一個對象是不是垃圾

引用計數法

原理:在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器爲零的對象就是不可能再被使用的。
缺點:很難解決對象之間相互循環引用的問題

package test33;

public class Person {
    private Person person;
    private int age;
    public Person() {
    }
    public void setPerson(Person person) {
        this.person = person;
    }
    @Override
    public String toString() {
        return "Person{" +
                "person=" + person +
                ", age=" + age +
                '}';
    }
}
package test33;

public class Main {
    public static void main(String[] args) {
        Person pa = new Person();
        Person pb = new Person();
        pa.setPerson(pb);
        pb.setPerson(pa);
        pa = null;
        pb = null;
        System.gc();
        System.out.println(pa);
        System.out.println(pb);
    }
}

在這裏插入圖片描述
該案例可證明hotspot虛擬機沒有采用引用計數法判斷對象是否是垃圾(可以被回收)

可達性分析算法

以被稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲“引用鏈”(Reference Chain),如果某個對象到GCRoots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。

哪些對象可以作爲GC Roots

在Java技術體系裏面,固定可作爲GC Roots的對象包括以下幾種:

  • 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  • 在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用。·
  • 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
  • 所有被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

Java中的對象引用分類

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象是否引用鏈可達,判定**對象是否存活都和“引用”**離不開關係。
關於對象的引用有以下四種類型
在這裏插入圖片描述

判定對象是否可真正回收的細節

存在兩次標記
第一次:該對象沒有與GC Roots相連接的引用鏈,進行標記
第二次:執行該對象的finalize()方法時,該對象是否與引用鏈上的對象還存在聯繫。進行標記
在這裏插入圖片描述
案例如下:

package test34;

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    public void isAlive(){
        System.out.println("我還活着");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
}
package test34;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        FinalizeEscapeGC.SAVE_HOOK = new FinalizeEscapeGC();
        FinalizeEscapeGC.SAVE_HOOK = null;
        System.gc();
        //因爲Finalizer方法優先級較低,暫停等待
        Thread.sleep(500);
        if(FinalizeEscapeGC.SAVE_HOOK!=null){
            FinalizeEscapeGC.SAVE_HOOK.isAlive();
        }else{
            System.out.println("i am dead");
        }
        System.out.println("=============");
        FinalizeEscapeGC.SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if(FinalizeEscapeGC.SAVE_HOOK!=null){
            FinalizeEscapeGC.SAVE_HOOK.isAlive();
        }else{
            System.out.println("i am dead");
        }
    }
}

運行結果如下:
在這裏插入圖片描述
上圖運行結果中說明了一個問題:任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了

垃圾收集算法

分代收集理論(重要)

收集器應該將Java堆劃分出不同的區域,然後將回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數分配到不同的區域之中存儲。顯而易見,如果一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的對象,就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區域,這就同時兼顧了垃圾收集的時間開銷和內存的空間有效利用。
基於分代收集理論,HotSpot虛擬機將java堆劃分爲新生代(Young Generation)和老年代(Old Generation)兩個區域。由於對象可能會產生跨代引用的問題。解決辦法是在新生代上建立一個全局的數據結構(該結構被稱爲“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此後當發生Minor GC時,只有包含了跨代引用的小塊內存裏的對象纔會被加入到GC Roots進行掃描。雖然這種方法需要在對象改變引用關係(如將自己或者某個屬性賦值)時維護記錄數據的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是划算的。
基於分代收集理論,在Java堆劃分出不同的區域之後,垃圾收集器纔可以每次只回收其中某一個或者某些部分的區域——因而纔有了“Minor GC”“Major GC”“Full GC”這樣的回收類型的劃分;也才能夠針對不同的區域安排與裏面存儲對象存亡特徵相匹配的垃圾收集算法——因而發展出了“標記-複製算法”“標記-清除算法”“標記-整理算法”等針對性的垃圾收集算法。

標記-清除算法

算法分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。標記過程就是對象是否屬於垃圾的判定過程。
缺點:

  • 執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過程的執行效率都隨對象數量增長而降低;
  • 內存空間的碎片化問題,標記、清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致當以後在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

標記-複製算法(複製算法,不適用老年代)

“半區複製”(Semispace Copying)的垃圾收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
優點:
算法需要複製的就是佔少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單,運行高效。
缺點:
1.將會產生大量的內存間複製的開銷。
2.將可用內存縮小爲了原來的一半,空間浪費比較嚴重。

優化的半區複製分代策略(Appel式回收)

把新生代分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾蒐集時,將Eden和Survivor中仍然存活的對象一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已用過的那塊Survivor空間。

HotSpot對Appel式回收的實現

HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內存空間爲整個新生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會被“浪費”的。
如果每次回收有多於10%的對象存活,因此Appel式回收還有一個安全設計,當Survivor空間不足以容納一次Minor GC之後存活的對象時,就需要依賴其他內存區域(實際上大多就是老年代)進行分配擔保(Handle Promotion)。這個是此算法不適用老年代的原因。

標記-整理算法

其中的標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存
缺點:
1.如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象並更新所有引用這些對象的地方將會是一種極爲負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行。

吞吐量

吞吐量的實質是賦值器(Mutator,可以理解爲使用垃圾收集的用戶程序,用戶程序或用戶線程”代替)與收集器的效率總和。
移動則內存回收時會更復雜,不移動則內存分配時會更復雜。從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程序的吞吐量來看,移動對象會更划算。
不移動對象會使得收集器的效率提升一些,但因內存分配和訪問相比垃圾收集頻率要高得多,這部分的耗時增加,總吞吐量仍然是下降的。HotSpot虛擬機裏面關注吞吐量的Parallel Scavenge收集器是基於標記-整理算法的,而關注延遲的CMS收集器則是基於標記-清除算法的。

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