1. 程式人生 > >《深入理解Java虛擬機》——垃圾收集器與內存分配策略

《深入理解Java虛擬機》——垃圾收集器與內存分配策略

特點 兩個 instance 統一 tro 過程 引用計數 分析算法 效率問題

GC需要完成:

  • 哪些內存需要回收
  • 什麽時候回收
  • 如何回收

如何確定對象不再使用

  • 引用計數算法
    給對象添加一個引用計數器,當有一個地方引用它時,計數器值進行加1操作;當引用失效時,計數器值進行減1操作;當計數器值為0,則說明對象不可能再被使用。但是它無法解決循環引用的問題。

    public class ReferenceCountingGC {
      
        public Object instance = null;
    
        public static void testGC(){
    
            ReferenceCountingGC objA = new ReferenceCountingGC ();
            ReferenceCountingGC objB = new ReferenceCountingGC ();
    
            // 對象之間相互循環引用,對象objA和objB之間的引用計數永遠不可能為 0
            objB.instance = objA;
            objA.instance = objB;
    
            objA = null;
            objB = null;
    
            System.gc();
    }
    }
    上述代碼最後面兩句將objA和objB賦值為null,也就是說objA和objB指向的對象已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數器都不為 0,那麽垃圾收集器就永遠不會回收它們。
  • 可達性分析算法
    通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。如下圖,對象Object5、Object6、Object7雖然互相有所關聯,但是它們到GC Roots是不可達的,因此將它們判定為可回收的對象。
    技術分享圖片
    在Java語言中,可作為GC Roots的對象包括:
    • 虛擬機棧(棧幀中的本地變量表)中引用的對象
    • 方法去中類靜態屬性引用的對象
    • 方法區中常量引用的對象
    • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

關於引用

無論是通過引用計數算法還是可達性分析算法,判斷對象是否存活都與“引用”有關。
宣告一個對象真正失效,至少要經歷兩次標記過程,如果對象在進行可達性分析後發現沒有與 GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize() 方法,當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。

關於Java中的引用:

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

永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。相較於廢棄常量,判定一個類是否是“無用的類”的條件則相對苛刻很多,類需要同時滿足下面三個條件:(不滿足一定不回收,滿足不一定回收,區別於對象無效就回收)
? 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例
? 加載該類的ClassLoader已經被回收
? 該類對應的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法


垃圾收集算法

  • 標記-清除(Mark-Sweep)算法
    首先標記出所有需要回收的對象,在標記完成之後統一回收所有被標記的對象。標記過程就是使用引用計數法或可達性分析進行標記。
    不足:
    • 效率問題:標記和清除兩個過程的效率都不高
    • 空間問題:標記清除之後會產生大量不連續的內存碎片,空間碎片太多導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前出發另一次垃圾收集動作。
      技術分享圖片
  • 復制算法
    將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用的內存空間一次清理掉。
    技術分享圖片

  • 標記-整理算法
    標記過程仍然與“標記-清除”算法一樣,但後續步驟不再直接對可回收對象進行清理,二十讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
    技術分享圖片

  • 分代收集算法
    根據對象存活周期的不同將內存劃分為幾塊,一般把Java堆分為新生代和老年代,根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那麽就選用復制算法,而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,必須使用“標記-清理”或者“標記-整理”算法來進行回收。

《深入理解Java虛擬機》——垃圾收集器與內存分配策略