【Java學習筆記】垃圾收集器和記憶體分配策略

ray26發表於2020-03-14

垃圾收集器與記憶體分配策略

如何判斷物件的"死活"

  1. 引用計數演算法

    • 在物件中新增一個引用計數器,每有一個地方引用它時,計數器+1;引用失效的時候,計數器值-1;任何時刻計數器為0的物件就是不可能再被使用的
    • 存在問題需要大量額外處理,比如迴圈引用的問題
    • 以下程式碼執行完後從GC日誌中可以看到相關objA和objB被回收,說明Java使用的不是引用計數演算法
    public class ReferenceCountingGC {
        public Object instance = null;
        
        private static final int _1MB = 1024 * 1024;
        
        /**
         *  這個成員屬性的意義是佔點記憶體,以便在GC日誌中看清楚是否有回收過
         */ 
        private byte[] bigSize =new byte[2 * _1MB];
        
        public static void testGC() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objB.instance = objA;
            
            objA = null;
            objB = null;
            
            System.gc();
        }
    }
    複製程式碼
  2. 可達性分析演算法

    可達性演算法分析

    • "GC Roots"根物件作為起始節點集,根據引用關係向下搜尋,搜尋過程走過的路徑為"引用鏈"(Reference Chain),如果某個物件到GC Roots間沒有任何引用鏈相連(GC Roots到物件不可達),此物件不可能再被使用
    • 可以被使用為GC Roots的物件
      • 在虛擬機器棧中引用的物件(如各個執行緒被呼叫的方法堆疊中使用到的引數、區域性變數、臨時變數)(虛擬機器棧中的棧幀都是還沒有執行完的方法形成的棧幀,這些方法中的區域性變數、引數變數、臨時變數肯定不能GC掉)
      • 在方法區中類靜態屬性引用的物件(如Java類中的引用型別靜態變數)
      • 在方法區中常量引用的物件(如字串常量池裡的引用)
      • 在本地方法棧中JNI(Native方法)引用的物件
      • Java虛擬機器內部的引用(基本資料型別對應的Class物件)
      • 所有被同步鎖(synchronized關鍵字)持有的物件
      • 反映Java虛擬機器內部情況的JMXBean、JVMTI中的註冊的回撥、原生程式碼快取等
    • 記憶體區域是虛擬機器自己的實現細節,不是獨立封閉的,進行可達性分析時候需要將關聯區域的物件加入GC Roots集合中去

引用

  • 分類:強引用、軟引用、弱引用、虛引用
    • 強引用:reference型別的資料中儲存的數值代表的是另外一塊記憶體的起始地址(Object obj = new Object(),obj就是強引用,存放在虛擬機器棧中,無論任何情況下,只要強引用關係還存在,垃圾收集器就不會回收掉被引用的物件)
    • 軟引用:還有用,但非必須的物件。在OOM發生之前,會將這些物件放進回收範圍進行二次回收(SoftReference實現軟引用),如果此次回收還沒有足夠記憶體才會丟擲OOM
    • 弱引用:非必須物件,強度比軟引用更弱,當垃圾收集器開始工作時候,無論當前記憶體足夠,都會回收
    • 虛引用最弱的一種引用關係,一個物件是否有虛引用的存在,完全不會對生存時間構成影響,主要作用是為了垃圾收集器回收時收到一個系統通知(PhantomRefernece類實現虛引用)

生存還是死亡判斷?

  • 兩次標記法

    • 當物件被判定為不可達時,並非一定要被清理,要進行兩次標記之後才能宣告一個物件真正“死亡”

    • 當第一次可達性分析後沒有與GC Roots相連線的引用鏈,會被第一次標記,隨後再進行一次篩選

      • 沒必要執行finalize()方法

        • 再次篩選的時候是否有必要執行finalize()方法,假如沒有finalize()或已經被虛擬機器呼叫過,則“沒有必要執行”
      • 有必要執行finalize()方法

        • 放置在F-Quene佇列中,稍後由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行佇列中finalize()方法.執行不代表會等待finalize方法執行完成,防止出現finalize方法執行緩慢或者死迴圈導致整個F-Queue佇列只其他物件處於等待或者整個記憶體回收子系統崩潰
        • finalize方法是物件最後一次逃脫的機會,物件可以在finalize()裡面成功拯救自己如下雨引用鏈上任何一個物件建立關聯,稍後收集器會對F-Queue佇列中的物件進行第二次小規模的標記,如果這時候還沒有逃脫,基本上就是要面臨被回收
        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            FinalizeEscapeGC.SAVE_HOOK = this;
        }
        複製程式碼

        • 任何一個物件的finalize()方法都只會被系統自動呼叫一次,如果物件面臨下一次回收,則finalize()方法不會被再次執行
        • 執行代價大,不確定性高,無法確保各個物件的呼叫順序,不推薦使用

回收方法區

  • 回收內容:廢棄的常量和不再使用的型別
  • 判斷常量是否廢棄:已經沒有任何物件引用常量池中的物件,虛擬機器中也沒有其他地方引用這個字面量
  • 判斷一個型別是否屬於不在使用三個條件
    • 該類所有的例項已經被回收(Java堆中不存在該類及其任何派生子類例項)
    • 載入該類的類載入器已經被回收
    • 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

辣雞收集演算法

分代收集理論

  • 弱分代假說:絕大多數物件都是朝生夕滅的
  • 強分代假說:熬過越多次辣雞手機過程的物件就難以消亡
  • 跨代引用假說:跨代引用相對於同代來說僅佔極少數(存在引用關係的物件應該傾向於同時生存或者同時消亡的,例如某個新生代被老年代所引用,該引用會使新生代物件在收集時同樣存活,進而進入老年代)
    • 在新生代上建立一個全域性的資料結構(記憶集),將老年代劃分成若干小塊,標識出老年代哪一塊記憶體存在跨代引用,Minor GC時,在跨代引用的記憶體裡的物件才會加入到GC Roots進行掃描
  • 辣雞收集器一致的設計原則:
    • 收集器應將Java堆劃分出不同的區域,然後將回收物件依據其年齡(年齡是物件熬過辣雞手機過程的次數)分配到不同的區域之中儲存
    • 如果一個區域中大多數物件都是朝生夕滅,將他們集中到一起,每次回收時只關注少量存貨,能以較低代價回收到大量的空間
    • 如果否是難以消亡的物件,把他們集中放在一起,虛擬機器用較低評率來回收這個區域,同時兼顧辣雞收集的時間開銷和記憶體的空間

標記-清除演算法

首先標記處所有需要回收的物件,在標記完成後,統一回收掉所有被標記的物件或者反過來標記所有存活的相對性,統計回收被標記的物件

缺點:

  • 執行效率不穩定:如果堆中有大量的物件,大部分需要回收,會進行大量標記和清除,效率隨物件數量的增長而降低
  • 記憶體空間的碎片化問題:清楚後產生大量不連續的記憶體碎片,空間碎片太多導致後期需要分配較大物件時無法找到足夠的連續記憶體導致再次觸發另一次辣雞收集

標記-複製演算法

將可用記憶體容量劃分為大小相當的兩塊,每次使用其中的一塊,當這一塊記憶體使用完後,將存貨的物件複製到另一塊上,再把已使用過的記憶體空間一次全部清理

缺點:

  • 對於多數物件都是存活的,這種演算法會產生大量的記憶體間複製開銷,但對於多數物件都是可回收的,這種演算法可以有效解決標記-清除演算法帶來的記憶體空間碎片問題
  • 可用記憶體縮小為了原來的一半,空間浪費太大

多數商用Java虛擬機器使用這種收集演算法去回收新生代

Appel式回收(建立在新生代有百分之98熬不過第一輪收集假設基礎上)

  • 把新生代分為一塊較大的Eden空間和兩塊較小的Survivor空間(8:1)
  • 每次分配記憶體只使用Eden和其中一塊Survivor空間上,將Eden和Survivor中仍然存活的物件一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已用過的那塊Survivor空間
  • 特殊情況:不能保證每次只有不多於百分之10的物件存活,使用逃生門設計,當Survivor空間不足以容納一次Minor GC之後存活的物件,使用其他記憶體區域進行分配擔保(如果另外一塊Survivor空間沒有足夠空間存放上一次新生代手記下來的存活物件,便通過分配擔保機制直接進入老年代)

標記-整理演算法

標記過程和“標記-清除”演算法一樣,但後續非將所有可回收物件進行清理,而是讓所有存活物件向記憶體空間一端移動,然後直接清理邊界以外的記憶體

移動和不移動的弊端

  • 移動在老年代每次回收都存在大量物件存活區域,必須暫停使用者應用程式才能進行
  • 不移動就會產生“標記-清除”演算法大量的碎片化空間,需要使用複雜的記憶體分配器和記憶體訪問器(如“分割槽空閒分配連結串列”)
  • 吞吐量:賦值器(“使用者程式”或“使用者執行緒”)和收集器的效率總和
  • 關注吞吐量使用標記-整理演算法,關注延時使用標記-清理演算法:因為記憶體分配會比垃圾收集頻率更高,總吞吐量會下降,如果進行移動會造成應用程式的停頓,造成延時增長

和稀泥式解決方法:大部分時間使用標記-清楚演算法,當記憶體空間的碎片程度影響到記憶體分配,再使用標記-整理演算法進行收集

記憶體分配與回收策略

  • 物件優先在Eden分配,當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次Minor GC
  • 大物件直接進入老年代
  • 長期存活的物件將進入老年代(虛擬機器會設定一個年齡計數器,儲存在物件頭中,設定一個年齡上限,在survivor區待到了這個年齡上限就會從survivor區進入老年代)
  • 動態物件年齡判定(如果Survivor空間中相同年齡所有物件大小綜合大於Survivor空間的一般,年齡大於或等於該年齡的物件就可以直接進入老年代)
  • 空間分配擔保
    • 首先檢查老年代最大可用的連續空間是否大於新生代所有空間,條件成立賊此次Minor GC可以保證是安全的
    • 不成立檢查-XX:HandlePromotionFailuer是否允許擔保失敗,允許則繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,大於則會嘗試進行一次Minor GC,但存在風險
    • 小於或者不允許冒險,則進行Full GC
    • 冒險:新生代複製收集過後出現大量Minor GC之後仍然存活的情況,需要老年代進行分配擔保吧把Survivor無法容納的物件直接送入老年代

相關文章