JVM垃圾回收演算法

Awecoder發表於2021-12-28

1 什麼時候回收垃圾?

1、什麼場景下該使用什麼垃圾回收策略?

在對記憶體要求苛刻的場景:想辦法提高物件的回收效率,多回收掉一些物件,騰出更多記憶體。

在CPU使用率高的情況下:降低高併發時垃圾回收的頻率,讓CPU更多的去執行你的業務而不是垃圾回收。

2、垃圾回收發生在哪些區域?

堆(回收物件)、方法區(不用的常量、類)

3、物件什麼時候被回收?

引用計數法:通過物件的引用計數器來判斷物件是否被引用。無法處理迴圈引用問題。

A --> B --> C --> D --> B 此時B被引用了兩次
當A不再引用B
A     B --> C --> D --> B BCD迴圈引用無法被回收。

可達性分析法:以根物件(GC Roots)為起點向下搜尋,形成引用鏈。如果物件不在引用鏈上,便視作物件不可達,可以回收。

image

哪些物件可以作為GC Roots物件?

  • 虛擬機器棧(棧幀中的本地變數表)中引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中JNI(即 Native方法)引用的物件

注意的是,一個物件不可達,也不一定會被回收。

image

public class GCTest {
    private static GCTest obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize方法被呼叫了");
        obj = this; // 重新引用物件,從FQ佇列中出隊
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new GCTest();
        obj = null;
        System.gc();

        TimeUnit.MILLISECONDS.sleep(1000);
        extracted();

        TimeUnit.MILLISECONDS.sleep(1000);
        obj = null; // finalize()方法已被呼叫過,物件會被直接回收
        System.gc();
        extracted();
    }

    private static void extracted() {
        if (obj == null) {
            System.out.println("obj == null");
        } else {
            System.out.println("obj可用");
        }
    }
}
finalize方法被呼叫了
obj可用
obj == null

finalize()的建議

  • 避免使用finalize()方法,操作不當可能會導致問題。上面的例子中,第二次回收時如果不設定obj==null,物件很難被回收掉。

  • finalize()優先順序低,何時會被呼叫無法確定,因為什麼時間發生GC不確定。

  • 建議使用try...catch...finally來替代finalize()。

4、四種引用

強引用,例如new物件,永遠不會回收物件。

軟引用,描述有用但是非必需物件,只有記憶體不足時被回收。

弱引用,描述非必需物件,無論記憶體情況都會被回收。

虛引用,主要用來跟蹤物件被垃圾回收器回收的活動,結合引用佇列使用。不影響物件的生命週期。

Object obj = new Object();
SoftReference<String> sr = new SoftReference<>("helllo");
WeakReference<String> sr = new WeakReference<>("helllo");
ReferenceQueue< String> queue = new ReferenceQueue<>();
PhantomReference< String> pr= new PhantomReference<>(“hello“queue)

2 垃圾回收演算法

垃圾回收演算法

基礎垃圾回收演算法

  • 標記清除法(Mark-Sweep):標記後直接刪除。

    • 容易產生記憶體碎片;分配大記憶體時速度受到影響,需要找到適合它大小的空間。
  • 標記整理法(Mark-Compact):標記,記憶體整理到一側,刪除三步。

    • 整理存在開銷,沒有碎片。
  • 複製(Copy):記憶體分為兩塊,每次只使用其中一塊;將存活物件複製到另一塊記憶體,然後清空當前記憶體;交換使用另一塊記憶體。

    • 記憶體利用率低,效能好,沒有碎片。

綜合垃圾回收演算法

  • 分代回收演算法:把記憶體分為多個區域,不同區域使用不同的回收演算法。
  • 增量演算法:每次只收集一小塊記憶體區域的垃圾。

分代收集演算法

商業虛擬機器普遍採用分代收集演算法,根據物件的存活週期,把記憶體分為多個區域,不同區域使用不同的回收演算法。

分代的好處是更有效的清除不再需要的物件,提升垃圾回收的效率。

  • 新生代回收(Minor GC | Young GC)
  • 老年代回收(Major GC),通常伴隨Full GC,Major GC≈ Full GC
  • 清理整個堆(Full GC)

典型的物件分配流程如下。
image

新生代採用的是複製演算法,存放存活週期短的物件。老年代物件存活時間長,採用標記-清除或標記-整理演算法。

堆的年輕代和老年代,記憶體比例1:2。年輕代分為Eden和Survivor,記憶體比例8:1:1。
1. 物件首先存放到Eden中,當Eden記憶體滿後,垃圾收集執行緒執行minor gc,通過GC Root進行可達性分析,找到非垃圾物件,複製到Survivor S0區(物件頭中分代年齡標記為1),清空Eden區域,Eden又可以儲存新物件。
2. 當Eden再次滿後,同時對Eden和S0執行minor gc,將非垃圾物件複製到S1(分代年齡加1),清空Eden和S0。S0和S1,同一時間只有一個被使用。
3. 當分代年齡達到15後,物件會被複制到老年代。
4. 當老年代滿了,會對年輕代和老年代執行full GC。full GC不起作用,則會OOM。

命令列輸入jvisualvm,開啟Java VisualVM工具的Visual GC外掛可以看到程式執行過程中堆容量變化。每次Eden滿進行GC,調整堆區域。

image

非典型的情況:

  1. 新建的物件不一定分配到伊甸園。
  • 物件大於-XX:PretenureSizeThreshold,就會直接分配到老年代

  • 新生代空間不夠。例如大陣列。

  1. 物件也不一定要達到年齡才進入老年代

動態年齡:如果Survivor空間中所有相同年齡物件大小的總和大於Survivor空間的一半,那麼年齡大於等於該年齡的物件就可以直接進入老年代。

觸發垃圾回收的條件

Minor GC觸發條件:伊甸園空間不足

Full GC觸發條件:

  1. 老年代空間不足(老年代已滿;空間碎片太多,沒有連續記憶體分配物件)
  2. 元空間不足
  3. 要轉成老年代物件所需的空間大於老年代剩餘空間。
  4. 顯示呼叫System.gc(),建議垃圾回收器執行垃圾回收。設定-XX:+DisableExplicitGC 引數忽略該操作。
分代收集調優思路
  • 讓GC儘量發生在新生代,儘量減少Full GC的發生。
  • 合理設定Survior區域的大小,避免記憶體浪費。(複製演算法)

相關文章