1. 程式人生 > >學習一下 JVM (三) -- 瞭解一下 垃圾回收

學習一下 JVM (三) -- 瞭解一下 垃圾回收

一、簡單瞭解幾個概念

1、什麼是垃圾(Garbage)?什麼是垃圾回收(Garbage Collection,簡稱 GC)?

(1)什麼是垃圾(Garbage)?
  這裡的垃圾 指的是 在程式執行過程中沒有任何指標指向的物件,即不再被使用的物件。
  如果不及時清理這些物件(垃圾),這些物件將會佔用程式記憶體,無法被其他物件使用,嚴重時可能導致記憶體溢位。

(2)什麼是垃圾回收(Garbage Collection,簡稱 GC)?
  一般程式佔用的記憶體都是有限的,如果不斷分配記憶體空間而不進行記憶體回收,記憶體遲早會被消耗完。所以要對 這些沒用的垃圾物件進行記憶體空間的回收(即 GC),釋放沒用的物件、整理記憶體碎片,使整理出來的記憶體可以分配給新物件使用。

  隨著程式業務的龐大、複雜,GC 操作尤為重要,沒有 GC 即意味著系統不能正常執行,而經常導致 STW(Stop The World,GC 回收導致程式暫停直至 GC 結束) 的 GC 也逐漸跟不上實際需求,所以後續出現各式各樣的 GC 優化後的 GC 收集器(比如:CMS、G1 等)。

2、記憶體自動管理?

(1)C/C++ 手動管理記憶體
  在早期的 C/C++ 時代,都是手動進行 記憶體分配 以及 記憶體回收(GC)。這種方式可以靈活控制記憶體釋放時間,但是開發人員需要 頻繁操作 記憶體申請以及記憶體釋放,此時若開發人員疏忽而漏掉了某處的記憶體回收,可能會造成 記憶體洩露。由於此時垃圾物件無法被清除,隨著系統執行可能會持續消耗記憶體,最終導致記憶體溢位使程式崩潰。

(2)Java 自動管理記憶體
  Java 使用 記憶體自動管理思想,其 記憶體動態分配 以及 記憶體自動回收 機制 使開發人員減輕了 記憶體操作的壓力(無需手動參與記憶體分配與回收,可以專注於業務開發),降低了 記憶體洩露 與 記憶體溢位的 風險。但是 過於依賴記憶體自動管理,將會弱化 開發人員 定位、解決 程式中 記憶體溢位、記憶體洩露 等問題的能力。
  所以瞭解 JVM 自動記憶體分配 以及 記憶體回收 等原理是非常重要的,便於排查各種 記憶體溢位、記憶體洩露 等問題,當系統因 GC 出現瓶頸,可以對其進行適當的監控與調優。

3、簡單瞭解下 記憶體洩露、記憶體溢位(OOM)

(1)記憶體洩露(Memory Leak):

  記憶體洩露 指的是程式在申請記憶體執行後,無法釋放已經申請的記憶體空間,從而導致程式執行速度變慢甚至崩潰。
  簡單的理解就是,你開闢了一個空間使用,使用完之後卻不釋放該空間,導致該空間一直被佔用(記憶體洩露次數過多,佔用記憶體也就更多,此時可能導致記憶體溢位)。

導致記憶體洩露出現的情況一般為:物件生命週期過長,無法被垃圾回收器收集。
  物件生命週期過長:
    比如單例模式產生的物件,生命週期與應用程式一樣長,如果該物件內部持有 外部物件的引用,那麼這個外部物件是不會被垃圾收集器回收的,從而導致記憶體洩露。
    一些資源未關閉,比如 資料庫連線、IO 連線 未關閉,是不會被回收的,從而導致記憶體洩露。

(2)記憶體溢位(Out Of Memory):
  記憶體溢位 指的是程式執行時 申請記憶體 大於 系統剩餘記憶體空間,導致記憶體分配失敗使系統崩潰。
  簡單的理解就是,你現在需要開闢 10M 的記憶體空間,但是系統只剩餘 9M 記憶體空間,最終系統無法分配所需的記憶體導致記憶體分配失敗。

  一般情況下,除非應用程式佔用記憶體增長速度非常快,造成垃圾回收速度跟不上記憶體消耗的速度,否則不太容易出現 記憶體溢位(OOM)的情況。
導致 OOM 出現情況一般為:空閒記憶體不足 且 垃圾回收器不能提供更多的記憶體。

  空閒記憶體不足:
    Java 虛擬機器設定的 堆記憶體 不夠。可通過 -Xms、-Xmx 引數調整。
    程式碼中建立了大量大物件且長時間不能被垃圾回收器收集。

4、垃圾回收的目標區域

(1)垃圾回收區域:
  JVM 執行時資料區 包括 程式計數器、虛擬機器棧、本地方法棧、堆、方法區。
其中:
  程式計數器、虛擬機器棧、本地方法棧 隨著執行緒產生、結束,而棧的棧幀 也是隨著方法進入、退出而執行入棧、出棧操作,即 方法結束或者執行緒結束,其記憶體就可以跟著回收,所以這些可以不需要過多考慮 記憶體回收問題。
  而 堆、方法區 需要執行時才能確定記憶體大小,比如 執行時才可以確定 會建立哪些物件、物件需要消耗多少空間等。這樣的區域 記憶體分配 與 回收是動態的,也即垃圾回收的重點關注物件,其中,堆 是重點中的重點。

(2)垃圾回收目標:
  GC 根據不同區域又可劃分為:年輕代回收、老年代回收、全堆回收、方法區回收。
  但從回收頻率上:頻繁收集年輕代、較少收集老年代、基本不動方法區。

5、理解一下 System.gc()

(1)System.gc() 作用
  預設情況下,呼叫 Runtime.getRuntime().gc() 或者 System.gc() 會顯示觸發 Full GC,同時對老年代、新生代進行垃圾回收,並嘗試釋放被丟棄物件佔用的記憶體。
  但是 System.gc() 不能保證立即進行垃圾回收,甚至不一定會執行垃圾回收。垃圾回收一般是自動進行的,不推薦手動觸發(會導致 STW)。

(2)System.gc() 回收舉例

【舉例:】
public class JVMDemo {

    public static void main(String[] args) {
        JVMDemo jvmDemo = new JVMDemo();
//        jvmDemo.testGC1();
//        jvmDemo.testGC2();
//        jvmDemo.testGC3();
//        jvmDemo.testGC4();
        jvmDemo.testGC5();
    }

    public void testGC1() {
        // GC 不回收 有用的物件
        byte[] buffer = new byte[20 * 1024 * 1024];
        System.gc();
    }

    public void testGC2() {
        // GC 回收 無用的物件,此時物件置 null,即引用失效,為垃圾物件
        byte[] buffer = new byte[20 * 1024 * 1024];
        buffer = null;
        System.gc();
    }

    public void testGC3() {
        // 引用仍存在棧幀的 區域性變量表中,不會被 GC 回收
        {
            byte[] buffer = new byte[20 * 1024 * 1024];
        }
        System.gc();
    }

    public void testGC4() {
        // 新的區域性變數佔用 過期的區域性變數 在區域性變量表的位置,即引用失效,可以被 GC 回收
        {
            byte[] buffer = new byte[20 * 1024 * 1024];
        }
        int value = 10;
        System.gc();
    }

    public void testGC5() {
        // 方法作用域結束,引用失效,可被 GC 回收
        testGC1();
        System.gc();
    }
}

 

 

 

6、Stop The World(STW)、並行(Parallel)、併發(Concurrent)

(1)Stop The World
  簡稱 STW,指的是 GC 發生時程式會停頓(停頓產生使整個應用程式執行緒都被暫停,沒有任何響應),在 GC 完成後,應用程式將被恢復。要減少 STW 的發生。
  一般來說 任何 GC 均會產生 STW,只能說 GC 回收器越來越優秀,回收效率越來越高,從而導致 STW 時間縮短(體會不到停頓的發生)。

(2)並行(Parallel)
  當系統有多個 CPU 時(或者 多核 CPU 時),某個 CPU 執行一個程序時,其他 CPU 可以執行其他的程序,各 CPU 之前互不搶佔 CPU 資源,即真正的同時執行。

(3)併發(Concurrent)
  在作業系統中,某一時間段上 同一個處理器 執行多個執行緒。通過 CPU 排程演算法,使各個執行緒在時間段內快速切換,使程式看上去是同時執行的,不是真正的同時執行。

(4)並行、併發區別
  併發:強調多個事情 在同一時間段內 同時發生了。多個任務之間相互搶佔資源。
  並行:強調多個事情 在同一時間點上 同時發生了。多個任務之間不相互搶佔資源。

(5)垃圾回收中的並行與併發
  序列:指單個垃圾回收執行緒執行,此時使用者執行緒暫停。
  並行:指多個垃圾回收執行緒同時執行,此時使用者執行緒暫停。
  併發:指使用者執行緒、垃圾回收執行緒交替執行,使用者執行緒不暫停。

7、安全點(SafePoint)、安全區域(SafeRegion)

(1)安全點:
  程式執行時並非可以在任意地方停頓並 GC,強制要求在特定位置才能停頓並 GC,而這些位置稱為 安全點。
  安全點設定太少 可能導致 GC 等待時間長,設定過多 可能導致 執行時效能下降。
  大部分指令執行時間非常短暫,但也有一部分指令執行時間會較長,為了避免程式長時間無法進入安全點導致 GC 等待時間長,所以一般安全點選擇標準為:是否具有使程式長時間執行能力的指令作為安全點,比如:方法呼叫之後、迴圈末尾、方法返回前等。

(2)如何使執行緒在安全點完成停頓?
搶佔式中斷(一般 JVM 都不採用):
  先中斷所有執行緒,如果有執行緒不在安全點,則恢復該執行緒,過一會再中斷直至執行緒達到安全點。

主動式中斷(一般 JVM 採用):
  設定一箇中斷標誌位,各個執行緒執行到安全點 時主動輪詢該標誌,如果中斷標誌為真,則執行緒掛起。

(3)安全區域:
  安全點是針對 正在執行的執行緒 設定的,如果執行緒處於 sleep 或者 block 等中斷狀態,其不能響應 JVM 的中斷請求、執行到 安全點 進行中斷。為了解決這個問題,產生了安全區域。
  安全區域指的是一段程式碼片段中,物件的引用關係不會發生變化,此時這個區域中任何位置開始的 GC 均是安全的。

(4)安全區域執行:
  當執行緒執行到 安全區域時,先標記自己進入 安全區域,如果這段時間內發生 GC,則 JVM 會忽略被標識為 安全區域 狀態的執行緒。
  當執行緒即將離開 安全區域 時,先檢查是否完成 GC,如果完成 GC 則繼續執行,否則執行緒必須等待、直到收到可以離開 安全區域 的訊號。

8、強引用、軟引用、弱引用、虛引用

(1)引用
  JDK1.2 後,Java 對引用概念進行了補充,將引用分為:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference) 四種,且引用強度依次減弱。

  垃圾收集器回收物件時,回收目標一般為 未被引用的物件。若想回收一些被引用的物件(比如類似於快取的存在,當記憶體空間足夠時,保留物件,若垃圾回收後記憶體空間依舊不夠,則回收物件增大記憶體空間),可以使用弱引用、軟引用等去實現。

(2)Reference
  java.lang.ref 包下定義了 Reference 抽象類,其有不同的子類可以實現不同的引用效果,其中除 FinalReference (default,包內可見)外,其餘三種引用型別均為 public,可以直接使用。

 

 

 

(3)強引用 -- 不回收(可能導致 OOM)
  最基本的引用,最常見的引用賦值操作。強引用一般指通過 new 關鍵字建立物件並賦值給變數,此時變數成為指向物件的強引用。比如: Object object = new Object(); 。
  只要強引用存在,垃圾回收器不會回收被引用的物件。
注:
  強引用是造成 記憶體洩露、記憶體溢位 的主要原因之一。
  若想回收一個強引用物件,可以顯示將其強引用賦值為 null,或者超出強引用的作用域。

【舉例:(強引用存在就不會回收記憶體)】
public class JVMDemo {

    public static void main(String[] args) {
        // 申請 10M 記憶體空間
        byte[] buffer = new byte[10 * 1024 * 1024];
        // 第一次強引用存在,不會回收
        System.gc();

        // 消除強引用
        buffer = null;
        // 第二次強引用不存在,回收記憶體空間
        System.gc();
    }
}

 

 

 

(4)軟引用 -- 記憶體不足就回收
  軟引用 一般用來 描述 還有用但是非必須的 物件。
  在系統發生 OOM 之前,會對軟引用物件進行 二次回收,若此次回收仍沒有足夠記憶體,才會丟擲 OOM 異常。
  軟引用類似於快取的存在,記憶體不足時才會去清理。

【舉例:(當記憶體不足時,觸發垃圾回收,並回收軟引用所佔記憶體)】
import java.lang.ref.SoftReference;

/**
 * JVM 引數設定 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        /**
         *  Test test = new Test();
         *  SoftReference<Test> softReference = new SoftReference<>(test);
         *  test = null;
         *
         *   上面三行程式碼等價於下面一行程式碼
         *  SoftReference<Test> softReference = new SoftReference<>(new Test());
         */
        SoftReference<byte[]> softReference = new SoftReference<>(new byte[10 * 1024 * 1024]);
        // 第一次記憶體足夠,即使手動 GC,軟引用也不會被回收
        System.out.println(softReference.get());
        System.gc();
        System.out.println(softReference.get());

        // 第二次記憶體不夠,觸發 GC,軟引用被回收
        byte[] buffer = new byte[8 * 1024 * 1024];
        System.out.println(softReference.get());
        System.gc();
        System.out.println(softReference.get());

    }
}

 

 

 

(5)弱引用 -- 被發現就回收
  弱引用也是用來描述非必須物件,其強度弱於 軟引用,被弱引用關聯的物件只能生存到下一次垃圾收集前。當系統 GC 時,只要發現了弱引用,無論堆空間記憶體是否足夠,都會回收掉弱引用關聯的物件。
  弱引用同樣可以用於實現快取。

【舉例:(弱引用被發現就回收)】
import java.lang.ref.WeakReference;

/**
 * JVM 引數設定 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        byte[] buffer = new byte[10 * 1024 * 1024];
        WeakReference<byte[]> weakReference = new WeakReference<>(buffer);
        System.out.println(weakReference.get());
        System.gc();
        System.out.println(weakReference.get());

        buffer = null; // 去除強引用,此時弱引用可被回收
        System.out.println(weakReference.get());
        System.gc();
        System.out.println(weakReference.get());
    }
}

 

 

 

注:
  WeakHashMap、ThreadLocal 中都使用到了 弱引用。
  WeakHashMap 是基於弱引用實現的,其儲存的物件可能隨時被回收,即 使用 WeakHashMap 儲存元素時,即使你沒有進行刪除元素操作,其最後儲存的值也可能不一樣。可用於實現快取。

【舉例:】
import java.util.WeakHashMap;

/**
 * JVM 引數設定 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        for (int j = 0; j < 3; j++) {
            WeakHashMap<Integer, Object> weakHashMap = new WeakHashMap<>();
            for (int i = 0; i < 1000; i++) {
                weakHashMap.put(i, new Object());
            }
            System.out.println("未進行 GC 前:     " + weakHashMap.size());
            System.gc();
            System.out.println("進行 GC 後:       " + weakHashMap.size());
        }
    }
}

 

 

 

(6)虛引用 -- 用於物件回收跟蹤
  虛引用是引用型別中最弱的一個,一個物件是否有虛引用的存在,不會影響物件的生命週期,其不能單獨使用、也無法通過虛引用獲取到被引用的物件(呼叫 get 方法返回 null),其唯一作用就是跟蹤垃圾回收過程(物件被回收時收到一個系統通知)。
  虛引用需要與引用佇列一起使用(建立虛引用時提供一個引用佇列物件作為引數),當 GC 準備回收一個物件時,若發現其有虛引用,在回收物件後會將這個虛引用加入引用佇列,可以通過 引用佇列是否存在虛引用來了解 物件是否被垃圾回收 並作出相應處理。

【舉例:(引用物件加入 引用佇列,通過引用佇列是否存在虛引用 作出相應處理)】
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

/**
 * JVM 引數設定 -XX:+PrintGCDetails -Xmx15m -Xms15m
 */
public class JVMDemo {

    public static void main(String[] args) {
        byte[] buffer = new byte[10 * 1024 * 1024];
        ReferenceQueue referenceQueue = new ReferenceQueue<>();
        PhantomReference<byte[]> phantomReference = new PhantomReference<>(buffer, referenceQueue);
        System.out.println(phantomReference.get() + "==========" + referenceQueue.poll() + "=========" + phantomReference);

        // 消除強引用,GC 回收後,虛引用會進入 引用佇列,此時虛引用不會置 null
        buffer = null;
        System.gc();
        System.out.println(phantomReference.get() + "==========" + referenceQueue.poll() + "=========" + phantomReference);

        // 手動釋放 虛引用物件堆空間
        phantomReference.clear();
        System.gc();
        System.out.println(phantomReference.get() + "==========" + referenceQueue.poll() + "=========" + phantomReference);
    }
}

 

 

 

二、垃圾標記演算法

1、標記演算法有什麼用?

(1)標記演算法作用?
  堆中幾乎儲存了 Java 程式中的例項物件,如何確定哪些 物件 屬於無用物件 是 GC 的關鍵。
  標記演算法作用就是標記出哪些物件無用(當一個物件 不被 任何物件引用時,可以將其視為 死亡物件,即垃圾物件),從而方便 GC 進行記憶體回收。

(2)常用標記演算法分類:
  常用標記演算法有 引用計數演算法 和 可達性分析演算法。
  JVM 一般採用 可達性分析演算法。

2、標記演算法 -- 引用計數演算法(Reference Counting)

(1)引用計數演算法(Reference Counting):
  引用計數演算法為 每個物件維護了一個 引用計數器屬性,用於記錄物件被引用的情況。
  簡單的理解:為每個物件新增一個 引用計數器,當物件被引用時,引用計數器加 1,當引用失效時,引用計數器減 1。當引用計數器為 0 時,表示物件無用,可被回收。

(2)優缺點:
優點:
  實現簡單,垃圾物件標識清晰,回收效率高。

缺點:
  需要記憶體空間維護 引用計數器,增加了記憶體空間的開銷。
  每次賦值操作會觸發 引用計數器 的加減法,增加了時間的開銷。
  無法處理 迴圈引用 問題,導致一般 JVM 並沒有採用這個演算法進行垃圾回收。
注:
  雖然 Java 並未選擇 引用計數演算法,但是仍有其他語言選擇,比如 Python。Python 解決迴圈引用的方式:手動解除(在合適的場合,主動解除引用關係) 或者 使用弱引用 weakref(weakref 是 Python 提供的標準庫,用於解決迴圈引用)。

(3)引用計數演算法無法解決迴圈引用 舉例:
  如下,例項化兩個物件,此時引用計數器為 1,然後使兩個物件 相互引用,此時引用計數器為 2,然後將物件置 null,即引用計數器減 1,此時理論上說,這兩個物件都已失效,但是由於兩個物件相互引用導致 引用計數器不為 0,從而無法進行回收(造成記憶體洩露)。

【舉例:】
public class JVMDemo {

    public static void main(String[] args) {
        CircularReference a = new CircularReference();
        CircularReference b = new CircularReference();
        a.ref = b;
        b.ref = a;
        a = null;
        b = null;
        System.gc();
    }
}

class CircularReference {
    CircularReference ref = null;
    private byte[] size = new byte[3 * 1024 * 1024];
}

 

3、標記演算法 -- 可達性分析演算法(Reachability Analysis)

(1)可達性分析演算法(Reachability Analysis)
  可達性分析 又可稱為 根搜尋演算法 或者 追蹤性垃圾收集。
  其以 根物件集合(GC Roots)為起始點,根據引用關係 從上至下 搜尋,搜尋過程中走過的路徑稱為 引用鏈(Reference Chain),所有不在 引用鏈 上的物件 均為不可達物件(即無用物件,可標記為垃圾)。
注:
  在可達性演算法中,只有能夠被 GC Roots 直接或間接連線的物件才屬於 有用物件。
  GC Roots 是一系列符合條件的物件節點(一組活躍引用的集合),並非某一個節點。
  可達性分析演算法解決了 迴圈引用問題,一般 JVM 均採用此演算法標記可回收物件。

 

 

 

(2)GC Roots 分類
GC Roots 是一組活躍引用的集合。包括如下幾類:
  虛擬機器棧中 引用的物件。比如:各個執行緒被呼叫的方法中使用的 引數、區域性變數等。
  本地方法棧中 引用的物件。比如:native 方法引用的物件。
  方法區中 類靜態屬性 引用的物件。比如:Java 類的引用型別的靜態變數。
  方法區中 常量 引用的物件。比如:執行時常量池中的引用。
  所有被同步鎖(synchronized 關鍵字)持有的物件。
  JVM 內部的 引用物件。比如:基本型別對應的 Class 物件、系統類載入器、OOM 異常物件等。

除了以上分類,根據不同的垃圾收集器 以及 記憶體回收區域的不同,會產生一些 臨時物件 進入 GC Roots 中。比如:分代收集、區域性回收(Partial GC)。

一般來說,一個引用指向了堆記憶體中的物件,但是其本身並不存放在堆記憶體中,那麼其就屬於 Root。

注:
  使用可達性分析演算法標記物件時,必須保證分析工作 在能保障一致性的快照中進行。簡單的理解就是,分析物件是否可以回收時,不能操作物件,否則可能導致分析結果不正確。

4、物件的 finalization 機制、 finalize() 方法

(1)物件的 finalization 機制
  Java 提供的物件終止機制(finalization)可以允許 開發人員 在物件被銷燬前進行自定義邏輯處理。
  當垃圾回收器發現 無用物件(沒有引用指向物件)時,會先去呼叫該物件的 finalize() 方法,該方法屬於 Object 類,可以被子類重寫,用於物件被回收時進行資源的釋放(比如關閉檔案、關閉資料庫連線等)。

(2)finalize() 方法
  一般不建議主動呼叫物件的 finalize() 方法,應該交給垃圾回收機制 觸發(該方法只會被觸發一次)。
理由:
  使用 finalize() 方法可能導致物件復活(後面介紹,此處有個印象)。
  finalize() 方法執行時間沒有保障,極端情況下若不發生 GC,則不會去執行該方法。
  finalize() 方法執行可能會影響 GC 效能(比如程式碼裡發生了死迴圈)。

(3)物件的狀態
  執行可達性分析演算法後,從根節點無法訪問到的物件,一般都是需要被回收的,但事實上也許這些物件不一定 非死不可。比如 某個物件可能會在 某個條件下 復活自己,此時對該物件的回收就是不合理的。

一般物件狀態分類如下:
  可達物件,即 從根節點開始可以訪問的物件。
  可復活物件,即 物件引用已被釋放,GC 回收觸發 finalize() 方法時物件被複活。
  不可達物件,即 物件引用已被釋放,GC 回收觸發 finalize() 方法時沒有復活物件。
注:
  物件為 不可達狀態時 才會被回收。
  GC 只會觸發一次物件的 finalize() 方法,物件復活後,下一次 GC 並不會觸發該方法。

(4)兩次標記物件的流程:
Step1:如果物件 沒有被 GC Roots 引用鏈關聯,則進行第一次標記。
Step2:篩選物件 是否需要執行 finalize() 方法。
  Step2-1:如果物件 沒有重寫 finalize() 方法 或者 finalize() 方法已被呼叫過,則物件直接判定為 不可達物件,可以被回收。
  Step2-2:如果物件 重寫了 finalize() 方法且未被執行,則物件會被插入到 F-Queue 佇列中,並由 JVM 自動建立的低優先順序的 Finalizer 執行緒觸發其 finalize() 方法執行。
  Step2-3:GC 會對 F-Queue 佇列中的物件進行 二次標記,將物件放入 “即將回收” 的集合,如果 finalize() 方法執行過程中 物件與引用鏈上的物件建立關聯(可以將 this 關鍵字賦值給某個類變數 或者 物件的成員變數),即此時物件已復活,當該物件再次與 引用鏈無關聯時,會直接變為不可達物件。

【舉例:】
public class JVMDemo {
    // 定義一個類變數,作為 GC Roots
    public static JVMDemo jvmDemo = null;

    public static void main(String[] args) throws InterruptedException {
        // 例項化
        jvmDemo = new JVMDemo();

        // 第一次觸發 GC,會觸發 finalize() 方法,復活物件
        jvmDemo = null;
        System.gc();
        // Finalizer 執行緒優先順序較低,暫停一下讓它有時間響應
        Thread.sleep(500);
        if (jvmDemo != null) {
            System.out.println("我還活著");
        } else {
            System.out.println("我死了");
        }

        // 第二次觸發 GC,不會觸發 finalize() 方法,可直接回收物件
        jvmDemo = null;
        System.gc();
        // Finalizer 執行緒優先順序較低,暫停一下讓它有時間響應
        Thread.sleep(500);
        if (jvmDemo != null) {
            System.out.println("我還活著");
        } else {
            System.out.println("我死了");
        }

    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("執行 finalize() 方法");
        // this 賦值給 類變數,與 引用鏈建立聯絡,即復活物件
        JVMDemo.jvmDemo = this;
    }
}

 

 

 

三、垃圾清除演算法

1、什麼是垃圾清除?

(1)什麼是垃圾清除?
  通過前面的垃圾標記 演算法,可以在記憶體中區分 存活物件 以及 死亡物件(無用物件),接下來 GC 需要執行垃圾清除,釋放掉無用物件所佔用的記憶體空間,以便於有足夠可用的記憶體空間存放新物件。

(2)常見垃圾清除演算法:
  標記-清除演算法(Mark-Sweep)。
  標記-複製演算法(Mark-Copying)。
  標記-壓縮演算法(Mark-Compact)。

2、清除演算法 -- 標記-清除演算法(Mark-Sweep)

(1)什麼是標記-清除演算法?
  是最早出現、最基礎的垃圾收集演算法,其分為 “標記”、“清除” 兩個階段。
  當堆中有效記憶體空間被消耗完時,會暫停整個程式(Stop The World,簡稱 STW),並開始執行垃圾回收(標記 與 清除)。
    標記:從 GC Roots 開始遍歷,標記所有由 GC Roots 直接或間接關聯的物件,未標記的物件均為 垃圾物件,可被回收。
    清除:從堆記憶體中開始遍歷,如果發現未標記的物件(即無用物件),則將其回收。

如下圖所示(圖片來源於網路):

 

 

 

 

 

 

(2)缺點:
  進行 GC 時,會暫停整個應用程式,導致使用者體驗差。
  當堆中包含大量需要被回收物件時,有大量標記、清除動作,執行效率低。
  回收空間不連續,存在大量記憶體碎片,需要維護一個空閒列表來管理記憶體分配,當分配大物件且沒有足夠的連續空間時,可能會提前觸發一次 GC。

注:
  清除並非為置空,而是將需要清除的物件地址儲存在空閒列表中,下次分配新物件時,如果空間足夠就將其覆蓋。

3、清除演算法 -- 標記-複製演算法(Mark-Copying)

(1)什麼是標記-複製演算法?
  為了解決 標記-清除 演算法 面對大量可回收物件執行效率低的問題,引出了 半區複製 演算法。
  其將記憶體空間 分為兩塊,每次只使用其中一塊,當某一塊記憶體被使用完,就將此記憶體中仍然存活的物件 複製 到未使用 的記憶體塊中,並清除當前正在使用的記憶體塊的所有物件,交換兩個記憶體塊的角色,最終完成垃圾回收。

注:
  年輕代中, survivor 區就是採用這種形式進行垃圾回收。

 

 

 

 

 

 

(2)優缺點
優點:
  其保證了記憶體空間的連續性,不會產生記憶體碎片(移動指標,按順序分配記憶體空間)。

缺點:
  需要兩倍記憶體空間,且會浪費 一塊記憶體空間。
  如果記憶體中有大量物件存活,那麼物件複製 會產生時間開銷,且執行效率低。

4、清除演算法 -- 標記-壓縮(標記-整理) 演算法(Mark-Compact)

(1)什麼是標記 - 壓縮演算法?
  複製演算法高效性建立在 存活物件少、垃圾物件多 的情況下,比如年輕代。但是老年代,大部分物件都是存活物件,如果仍使用複製演算法,那麼其物件複製的時間開銷將會很高。
  為了解決上面的問題,在 標記-清除 的基礎上改進,產生了 標記-壓縮演算法。分為標記、壓縮 兩個階段。先標記出所有存活的物件,然後將這些物件按照順序壓縮記憶體的一端,最後清理掉邊界之外的記憶體空間。

注:
  標記-壓縮演算法最終效果 等同於 標記-清除-壓縮 的過程,即先執行標記、清除的過程,最後將記憶體碎片整理(壓縮)。
  標記-清除演算法 是一種 非移動式的回收演算法,標記-壓縮演算法 是一種 移動式的回收演算法,各有優缺點。移動物件時 記憶體回收會很複雜(物件移動會觸發 STW,若物件被其他物件引用,還需修改其引用地址,但是分配記憶體更容易),不移動物件時,記憶體分配會很複雜(記憶體碎片多,需要使用空閒列表維護記憶體碎片)。

 

 

 

 

 

 

(2)優缺點
優點:
  解決了 標記-清除 演算法中的記憶體碎片問題,分配記憶體給新物件時,只需要分配一個記憶體的起始地址即可。
  解決了 標記-複製 演算法中的兩倍記憶體問題。

缺點:
  效率上 標記-壓縮 演算法 比 複製演算法 低。
  移動物件時,需要暫停程式,即 STW。
  移動物件時,若物件被其他物件引用,還需要調整引用地址。

5、清除演算法 -- 分代收集演算法(Generational Collection)

(1)常見清除演算法使用場景
  標記-清除演算法 雖然會產生記憶體碎片,但其不需要移動物件,適合存活物件較多的場景。
  標記-複製演算法 雖然會消耗兩倍記憶體空間 且 需要移動物件,但其不會產生記憶體碎片,執行效率也較高,適合存活物件較少的場景。
  標記-壓縮演算法 屬於 標記-清除演算法的優化版,其需要移動物件 但不會產生記憶體碎片,可用於存活物件較多的場景。

(2)什麼是分代收集演算法?
  沒有更優的演算法,只有更合適的演算法。上面介紹的幾種演算法,各有優缺點,可用於不同場景下。為了綜合這些演算法的優點,分代收集演算法出現了。

分代收集演算法:
  不同物件的生命週期是不同的,針對不同生命週期的物件採用不同的收集方式,可以提高垃圾回收效率。比如堆可以分為 年輕代、老年代,針對各年代的特點使用不同的回收演算法,從而提高垃圾回收的效率。

(3)HotSpot 分代演算法
年輕代:
  年輕代佔用記憶體區域較小,物件生命週期較短、存活率低、回收頻繁。
  此區域可以採用複製演算法進行回收,執行速度快,比如 survivor 區的實現。

老年代:
  老年代佔用記憶體區域較大,物件生命週期較長、存活率高、回收不頻繁。
  此區域可以採用 標記-清除 或者 標記-清除、標記-整理 混合回收。比如 CMS 回收器,基於 標記-清除 演算法實現,當記憶體碎片過多 並影響到 物件分配時,採用 標記-壓縮 演算法進行記憶體碎片的整理。

6、清除演算法 -- 增量收集演算法(Incremental Collection)

(1)什麼是增量收集演算法?
  垃圾回收過程中,程式會處於 STW 的狀態,程式執行緒會被掛起直至垃圾回收結束。如果垃圾回收時間較長,程式將被掛起很久,影響使用者體驗 以及 系統穩定性。為了解決這個問題,增量收集演算法出現了。

增量收集演算法:
  如果一次性對所有垃圾進行回收,可能造成系統長時間停頓。可以採用垃圾回收執行緒 與 應用程式執行緒 交替執行的形式,每次垃圾收集執行緒收集一小片區域後,切換到應用程式執行緒執行,然後又切回垃圾收集執行緒進行垃圾收集,如此反覆直至垃圾回收完畢。

注:
  增量收集演算法建立在 標記-清除 以及 複製演算法基礎上,處理執行緒間衝突、允許垃圾收集執行緒 採用分階段標記 形式完成標記、清理、複製等工作。


(2)優缺點:
優點:
  在垃圾回收過程中,間斷性的執行應用程式程式碼,可以減少系統停頓時間。

缺點:
  執行緒切換會消耗資源。

7、清除演算法 -- 分割槽演算法

(1)什麼是分割槽演算法?
  一般情況下,堆空間越大,GC 耗時越長,而 STW 時間也越長。為了更好控制 GC 產生的停頓時間,可以將一個大的記憶體區域 分隔成 多個小記憶體區域。每次回收若干個小區間而非整個堆空間,從而減少停頓時間。

(2)分代演算法、分割槽演算法區別?
  分代演算法 按照物件的生命週期長短將堆劃分為 年輕代、老年代 進行垃圾回收。
  分割槽演算法 將堆劃分為若干個小區間,每次對部分小區間進行垃圾回收。

四、垃圾收集器

1、垃圾收集器

(1)概述
  JVM 規範中並未對垃圾收集器如何實現做出過多的規定,不同的廠商、不同版本的 JVM 內部使用的垃圾收集器也可能由很大差別。

(2)垃圾收集器分類
按執行緒數劃分:
  序列垃圾回收器。指同一時間段內只允許一個執行緒執行垃圾回收操作,應用程式執行緒將被暫停直至垃圾收集結束。適用於併發能力較弱的機器。
  並行垃圾回收器。指允許多執行緒執行垃圾回收操作,能減低應用程式執行緒暫停時間。適用於並行能力較強的機器。

按工作模式劃分:
  併發式垃圾回收器。指 垃圾回收執行緒 與 應用程式執行緒交替工作,降低應用程式暫停時間。
  獨佔式垃圾回收器。指 垃圾回收執行緒執行時,應用程式執行緒將暫停直至垃圾收集結束。

按記憶體空間劃分:
  年輕代垃圾回收器。收集 年輕代 記憶體空間。
  老年代垃圾回收器。收集 老年代 記憶體空間。

按記憶體碎片處理劃分:
  壓縮式垃圾回收器。指 垃圾回收完成後,對存活物件進行壓縮整理,清除記憶體碎片,再次分配物件記憶體空間時可以採用 指標碰撞的方式。
  非壓縮式垃圾回收器。指 不清理記憶體碎片,再次分配物件記憶體空間時可以採用 空閒列表的方式。

2、垃圾回收 效能指標(吞吐量、暫停時間、記憶體佔用)

(1)常見效能指標:

【吞吐量:】
    吞吐量指 程式碼執行時間 佔 總執行時間的比例。
注:
    總執行時間 = 程式碼執行時間 + 記憶體(垃圾)回收時間
    比如: JVM 執行總時間為 100 分鐘,垃圾回收時間為 1 分鐘,那麼吞吐量就為 99%。
    
【垃圾收集開銷:】
    垃圾收集開銷指 記憶體(垃圾)回收時間 佔 總執行時間的比例。

【暫停時間(停頓時間):】
    暫停時間指 每次執行垃圾回收時,應用程式執行緒被暫停的時間(STW 時間)。
    
【收集頻率:】
    收集頻率指 相對於應用程式執行,發生垃圾回收的次數(頻率)。
    
【記憶體佔用:】
    Java 堆記憶體佔用大小。

(2)重要指標
  一般來說,吞吐量、記憶體佔用、暫停時間 是衡量 GC 的三大標準,但是一般不能同時滿足三個條件。但隨著硬體的提升,記憶體佔用的影響相對較小。所以一般還是抉擇 吞吐量 與 暫停時間。
  若以 高吞吐量 優先,則必須降低 記憶體回收 的頻率(減少執行緒切換導致的 時間消耗),但是這樣會導致每次 記憶體回收 導致的暫停時間 變長。
  若以 低暫停時間 優先,則只能頻繁進行 記憶體回收(多次少量),但是這樣會導致 頻繁切換執行緒 增加時間消耗。
  高吞吐量、低暫停時間 處於一種矛盾狀態,現在一般標準為:在保證最大吞吐量的情況下降低暫停時間。
比如:10 秒進行一次垃圾回收且每次停頓 100 毫秒,現在改為 5 秒進行一次回收且每次停頓 70 毫秒,雖然暫停時間 從 100 毫秒 下降到 70 毫秒,但是垃圾收集頻率增加了,若簡單的按 10 秒計算,5 秒回收一次導致 總垃圾回收時間為 140 毫秒,從而導致 吞吐量降低。

3、七款經典的垃圾收集器

(1)垃圾收集器分類
  序列垃圾收集器:Serial、Serial Old。
  並行垃圾收集器:ParNew、Parallel Scavenge、Parallel Old。
  併發垃圾收集器:CMS、G1。

(2)垃圾收集器 與 垃圾分代 的關係
  新生代垃圾收集器:Serial、ParNew、Parallel Scavenge。
  老年代垃圾收集器:Serial Old、Parallel Old、CMS。
  整堆收集器:G1。

(3)垃圾收集器組合
  不同垃圾收集器有不同的優缺點,沒有更完美的垃圾收集器,只有更合適的垃圾收集器。
  由於 Java 使用場景很多(比如:移動端、服務端等),針對不同的場景,使用不同的垃圾收集器,可以提高垃圾回收的效能。

如下圖(圖片來源於網路)為各個垃圾收集器的組合關係:

 

 

 

注:
  各收集器之間的連線表示 可以組合使用。比如:Serial 與 Serial Old,ParNew 與 CMS 等。
  紅色虛線表示移除組合。由於 JDK 版本更新,會廢棄、取消一些 垃圾回收器組合。在 JDK8 中 Serial + CMS、ParNew + Serial Old 方式被宣告為 廢棄,在 JDK 9 中被棄用。
  綠色虛線表示棄用組合。在 JDK14 中廢棄 Parallel Scavenge + Serial Old 方式。
  黃色框表示刪除。在 JDK14 中刪除 CMS 垃圾回收器。

(4)檢視預設的垃圾收集器

【引數:】
    -XX:+PrintCommandLineFlags      用於檢視命令列引數以及使用的 垃圾回收器

 

 

 

【工具:】
    jps                         用於檢視 JVM 程序ID
    jinfo -flags 程序ID         用於檢視 JVM 的狀態(所有引數)
    jinfo -flag [引數] 程序ID    用於檢視是否存在某引數

 

 

 

4、序列垃圾收集器 -- Serial、Serial Old 收集器

(1)Serial 收集器
  Serial 收集器是最基本的垃圾收集器,JDK1.3 之前用於回收 新生代的唯一選擇。其採用 複製演算法、序列回收、以及 STW 機制 實現記憶體回收。
  Serial Old 收集器用於執行 老年代回收,其採用 標記-整理演算法、序列回收、以及 STW 機制。

(2)工作流程圖(圖片來源於網路)
  此收集器屬於 單執行緒收集器,執行垃圾回收時,會暫停使用者執行緒(STW)。
注:
  STW 是由 JVM 後臺自動發起、完成的,即在使用者不可知、不可控的情況下 將使用者執行緒暫停、啟動(若 STW 時間過長,會導致程式卡頓、使使用者體驗差)。

 

 

 

(3)優缺點:
  簡單而高效。在單核 CPU 下,由於沒有執行緒互動的開銷,專門進行垃圾回收操作從而執行效率高。
  適用於 Client 模式下的虛擬機器。

(4)常見引數

【引數:】
    -XX:+UseSerialGC       
注:
    該引數指定 年輕代、老年代 均使用 序列收集器。
    即 年輕代使用 Serial GC,老年代使用 Serial Old GC。

 

 

 

5、並行垃圾收集器 -- ParNew 收集器

(1)ParNew 收集器
  ParNew GC 本質上屬於 Serial GC 的多執行緒版本,除了採用 並行回收 的方式進行記憶體回收外,兩款收集器差別不大。
注:
  Par 是 Parallel 的縮寫,New 指的是處理 年輕代。

(2)工作流程圖(圖片來源於網路)
  此收集器屬於 多執行緒收集器,執行垃圾回收時,會暫停使用者執行緒(STW)。
  由於 年輕代 回收次數頻繁,採用並行方式回收 效率高。

 

 

 

(3)優缺點
  PraNew 收集器 若執行在 多 CPU 環境下,可以充分利用 多 CPU 等硬體優勢,完成垃圾回收,此時效率比 Serial 收集器高。
  但若執行在 單核 CPU 環境下,由於並行導致 CPU 頻繁執行緒切換產生額外開銷,從而效率反而比不上 Serial 收集器。

(4)常見引數

【引數:】
    -XX:+UseParNewGC 
注:
    手動指定使用 ParNew 收集器作為年輕代收集器(ParNew + SerialOld)。
    
    -XX:+UseConcMarkSweepGC 
注:
    可以與 CMS 一起使用,新增引數後,指定 ParNew 收集器 為年輕代收集器。

    -XX:ParallelGCThreads
注:
    設定執行緒數量,預設與 CPU 核心數相同,比如:-XX:ParallelGCThreads=4。

 

6、並行垃圾收集器 -- Parallel Scavenge 、Parallel Old 收集器

(1)Parallel Scavenge 收集器
  Parallel Scavenge 是 年輕代收集器,JDK 1.4 時出現,其採用 複製演算法、並行回收、以及 STW 機制。
  Parallel Old 是 老年代收集器,JDK 1.6 後出現並用來替代 Serial Old 收集器,其採用 標記-整理演算法、並行回收、以及 STW 機制。

(2)Parallel Scavenge 與 ParNew 收集器不同之處:
  Parallel Scavenge 可以通過引數控制 吞吐量。所以有時也稱其為 吞吐量優先的 垃圾收集器。
  Parallel Scavenge 可以通過引數設定 自適應調節策略,動態提供合適的 暫停時間以及吞吐量。

(3)工作流程圖
  年輕代、老年代 均採用並行回收。
  JDK 8 預設使用此組合進行垃圾回收。

 

 

 

(4)優缺點
  高吞吐量可以高效利用 CPU 執行程式任務,適用於 後臺計算且不用太多互動 的任務(比如:訂單處理、批量處理等)。

(5)常見引數

【引數:】
     -XX:+UseParallelGC  / -XX:+UseParallelOldGC
注:
    JDK8 預設使用,上面兩個引數任選其一即可。
    手動指定年輕代 使用 Parallel Scavenge 進行垃圾回收。
    老年代使用 Parallel Old 進行垃圾回收。
    
     -XX:ParallelGCThreads
注:
    設定垃圾收集執行緒數,一般與 CPU 數量相同,以避免執行緒過多引起 CPU 阻塞從而影響垃圾回收。
    
    -XX:MaxGCPauseMillis
注:
    設定垃圾收集器 最大停頓時間(STW 時間),單位為 毫秒。
    謹慎使用該值,為了控制停頓時間,收集器執行時可能 會調整 Java 堆大小或者 其他引數。
    
    -XX:GCTimeRatio
注:
    設定 垃圾收集時間 佔總時間的比例,可以用來修改 吞吐量。
    取值範圍為 0~100,預設為 99,即 垃圾回收最大時間為 1%。    
    
    -XX:+UseAdaptiveSizePolicy
注:
    開啟自適應調節策略(預設開啟)。
    此模式下,年輕代大小(-Xmn)、Eden 區 與 Survivor 區比例(-XX:SurvivorRatio) 等引數會自動調整,
    從而達到 堆大小、吞吐量、停頓時間 三者平衡。

 

7、併發垃圾收集器 -- Concurrent Mark Sweep(CMS) 收集器

(1)CMS 收集器
  JDK 1.5,HotSpot 推出真正意義上的併發收集器 -- CMS 收集器,第一次實現 垃圾收集執行緒 與 使用者執行緒 同時工作。
  CMS 關注點為 儘可能縮短 垃圾收集的暫停時間(STW 時間),暫停時間越短,響應速度越高,從而提高使用者體驗(適用於 互動性強的程式)。
  CMS 採用 標記-清除演算法、併發回收、以及 STW 機制。
注:
  CMS 不能與 Parallel Scavenge 一起工作,使用 CMS 作為老年代收集器時,年輕代收集器只能從 ParNew 或者 Serial 中選擇一個。
  JDK9 將 CMS 標記為 Deprecate,JDK 14 已經移除 CMS。

(2)工作流程圖
CMS 工作分為 四個部分:
  初始標記(Initial Mark)階段。
  併發標記(Concurrent Mark)階段。
  重新標記(Remark)階段。
  併發清除(Concurrent Sweep)階段。
其中:
  初始標記階段、重新標記階段 仍然需要 STW(垃圾回收器一般只能儘可能縮短 STW 時間,無法完全消除 STW)。
  併發標記階段、併發清除階段 雖然耗時但不需要 STW,從整體上看,屬於低暫停時間的。
注:
  由於使用者執行緒不中斷,所以 CMS 進行回收時應該保證有足夠記憶體可用,即當老年代記憶體使用達到閾值時,便開始回收,當記憶體不足時,會出現併發失敗(Concurrent Mode Failure),此時虛擬機器將會臨時啟用 Serial Old 收集器作為預備方案 重新進行 老年代的垃圾回收,導致停頓時間長。
  JDK5 預設閾值為 68%,JDK6 之後為 92%,可以通過 -XX:CMSInitiatingOccupancyFraction 引數進行設定。

Step1:初始標記階段:
  此階段 所有工作執行緒均會出現 STW,
  主要用於 標記出 GC Roots 可以直接關聯到的物件,速度很快。

Step2:併發標記階段。
  此階段主要為 GC Roots 的直接關聯物件開始遍歷整個物件鏈的過程,雖然耗時較長,但是可以與垃圾收集執行緒一起併發執行。

Step3:重新標記階段。
  此階段 所有工作執行緒均會出現 STW,
  由於併發標記階段,工作執行緒並未暫停 可能產生一些 標記變動的物件,此階段主要任務就是標記出這部分物件,一般耗時稍長,但遠小於 併發標記階段 時間。

Step4:併發清除階段。
  此階段主要用於 清理物件、釋放空間記憶體。採用標記-清除演算法,不需要移動存活物件,但是會產生記憶體碎片。

 

 

 

(3)優缺點:
優點:
  支援併發收集、低暫停時間(STW)。

缺點:
  會產生記憶體碎片。記憶體空間無法分配大物件時,會提前觸發 Full GC。
  程式執行速度可能下降。併發時不停頓使用者執行緒,但會佔用一些執行緒進行垃圾回收((預設垃圾回收執行緒計算為:(處理器核心數 + 3)/ 4)),從而導致程式變慢,CPU 核心數不夠時,程式執行速度將會極大程度降低。
  無法處理浮動垃圾。浮動垃圾指的是 併發標記、併發清除階段 使用者執行緒執行 產生的垃圾物件,這些垃圾物件出現在 垃圾標記過程結束 後,此次垃圾回收無法被再次標記,即只能在下一次 GC 時被回收。

(4)常見引數

【引數:】
    -XX:+UseConcMarkSweepGC 
注:
    手動指定老年代使用 CMS 收集器,年輕代使用 ParNew 收集器。
    即 ParNew(年輕代回收) + CMS(老年代回收)+ Serial Old(老年代預備回收方案)
    
    -XX:CMSInitiatingOccupancyFraction
注:
    設定堆記憶體使用率閾值,達到閾值開始回收。
    JDK 5 及以前預設為 68%。JDK 6 及以後預設為 92%。
    若記憶體增長緩慢,可以設定較高閾值,降低 CMS 執行頻率。
    若記憶體增長迅速,可以設定較低閾值,增加 CMS 執行頻率,以避免頻繁觸發 Serial Old GC。   
    
     -XX:+UseCMSCompactAtFullCollection
注:      
    用於指執行完 Full GC 後進行 記憶體壓縮整理,避免記憶體碎片產生,但記憶體壓縮過程無法併發執行,所以可能導致停頓時間變長。
    
    -XX:CMSFullGCsBeforeCompaction
注:
    設定在執行多少次 Full GC 後進行記憶體空間壓縮整理。

 

8、併發垃圾回收器 -- Garbage First(G1)收集器

(1)Garbage First 收集器
  隨著業務龐大、複雜,不斷的對 GC 優化,為了適應不斷擴大的記憶體 和 不斷增加的處理器數量、進一步降低暫停時間、兼顧良好的吞吐量,在 JDK7 時引入了 G1 收集器。
  G1 是一款面向服務端應用、低暫停時間的垃圾收集器,只要針對 多核 CPU 以及 大容量記憶體的機器,在 JDK 7 中正式啟用,並在 JDK 9 中作為預設垃圾回收器。在減少暫停時間的基礎上提高吞吐量(用來替代 CMS)。
  G1 將堆記憶體空間 劃分為若干大小相同的獨立的 Region(預設劃分為 2048 個記憶體大小相同的區域),通過引數 -XX:G1HeapRegionSize 可以設定記憶體大小,範圍為 1MB ~ 32MB 且為 2 的 N 次冪。一個 region 可能屬於 Eden、Survivor、Old 記憶體區域,但是每一個 region 一次只能代表一個記憶體區域。新增一個 Humongous 記憶體區域,用於儲存大物件(超過 1.5 region 大小即為大物件)。

 

 

 

(2)為什麼叫 G1?
  G1 將堆記憶體分割成很多不相關的區域(Region),使用不同的 Region 表示 Eden、Survivor0、Survivor1、老年代等。其跟蹤各個 Region 裡面垃圾堆積的價值(回收所獲得的空間大小以及回收所需時間),並在後臺對這些值維護一個優先列表,每次回收優先順序大的 Region,也即優先收集垃圾價值最大的區域,所以叫垃圾優先(Garbage First)。

(3)工作流程圖
詳見參考:
  https://www.jianshu.com/p/7dd309cc3442
  https://blog.csdn.net/coderlius/article/details/79272773

GC 回收流程主要為三步:
  年輕代 GC(Young GC)。
  混合 GC(Mixed GC)。
  記憶體分配不足時會觸發 Full GC。

Step1:年輕代 GC
  Young GC 是 STW、並行執行的。
  Eden 區滿後觸發 Young GC,Eden 區物件移動到 Survivor 區,大物件直接進入 Old 區,Survivor 區滿足年齡條件後,同樣可以進入 Old 區。

Step2:混合 GC。
  Mixed GC 分為兩個階段:
    併發標記階段。
    拷貝存活物件階段。

  併發標記過程與 CMS 併發收集過程類似,稍有區別。同樣超過記憶體佔用閾值時將會觸發(使用引數可以設定,預設為 -XX:InitiatingHeapOccupancyPercent=45),閾值的區別在於 G1 指的是 整堆的記憶體佔用率,CMS 指的是 老年代的佔用率。

併發標記流程:
  初始標記。
  併發標記。
  重新標記。
  清理階段。

【初始標記:】
    此階段 所有工作執行緒均會出現 STW,
    主要用於 標記出 GC Roots 可以直接關聯到的物件,借用了 Young GC 的暫停時間進行標記。

【併發標記:】       
    此階段主要為 GC Roots 的直接關聯物件開始遍歷整個物件鏈的過程,雖然耗時較長,但是可以與垃圾收集執行緒一起併發執行。
    
【重新標記:】
    此階段 所有工作執行緒均會出現 STW,
   標記出併發階段發生變化的物件。

【清理階段:】
    此階段 所有工作執行緒均會出現 STW,
    找到空閒的 Region 並回收到 可分配的 Region 中。
注:
    此階段只回收 完全空閒的 Region,若有存活物件的 Region,將會在 Mixed GC 中回收。    

 

拷貝存活物件階段(Evacuation):
  此階段 所有工作執行緒均會出現 STW,
  更新 Region 資料,對 Region 回收價值、回收成本 排序,根據 使用者設定的停頓時間 選擇 多個 Region (所有年輕代、部分老年代)構成回收集(Collection Set、CSet),將存活物件複製到空的 Region 中,並清理舊的 Region。

 

 

 

(4)常見引數

【引數:】
    -XX:+UseG1GC
注:
    手動指定使用 G1 收集器執行垃圾回收。
    
    -XX:G1HeapRegionSize
注:
    設定每個 Region 大小,值為 2 的冪,範圍為 1MB ~ 32MB。
    
    -XX:MaxGCPauseMillis
注:
    設定最大 GC 停頓時間,預設 200 ms。    
    
    -XX:InitiatingHeapOccupancyPercent
注:
    設定觸發 GC 的堆佔用率閾值,預設 45%。   

 

五、GC 日誌分析

  通過閱讀 GC 日誌資訊,可以快速瞭解當前 JVM 記憶體分配與回收策略。

1、常用引數

【引數:】
    -XX:+PrintGC            輸出 GC 日誌資訊。
    -XX:+PrintGCDetails     輸出 GC 詳細資訊(最後輸出堆記憶體分配、使用情況)。
    -XX:+PrintGCTimeStamps  輸出 GC 時間戳。
    -XX:+PrintGCDateStamps  輸出 GC 時間戳(日期格式)。
    -XX:+PrintHeapAtGC      在 GC 執行前後打印出 堆的資訊。
    -Xloggc:logs/gc.log     指定日誌輸出路徑。

 

2、日誌分析

【舉例:】
/**
 * JVM 引數設定 -XX:+PrintGCDetails -Xloggc:logs/gc.log -Xms5m -Xmx5m
 *
 */
public class JVMDemo {

    public static void main(String[] args) {
        byte[] buffer = new byte[1 * 1024 * 1024];
        byte[] buffer2 = new byte[1 * 1024 * 1024];
        System.gc();
    }
}

【輸出:】
Java HotSpot(TM) 64-Bit Server VM (25.92-b14) for windows-amd64 JRE (1.8.0_92-b14), built on Mar 31 2016 21:03:04 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16646060k(8616984k free), swap 19136428k(6872820k free)
CommandLine flags: -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
0.123: [GC (Allocation Failure) [PSYoungGen: 1018K->485K(1536K)] 1018K->557K(5632K), 0.0009012 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.145: [GC (System.gc()) [PSYoungGen: 983K->485K(1536K)] 3103K->2661K(5632K), 0.0005754 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.146: [Full GC (System.gc()) [PSYoungGen: 485K->0K(1536K)] [ParOldGen: 2176K->2589K(4096K)] 2661K->2589K(5632K), [Metaspace: 3088K->3088K(1056768K)], 0.0040756 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 1536K, used 51K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 5% used [0x00000000ffe00000,0x00000000ffe0ce68,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 4096K, used 2589K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
  object space 4096K, 63% used [0x00000000ffa00000,0x00000000ffc875f0,0x00000000ffe00000)
 Metaspace       used 3096K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 340K, capacity 388K, committed 512K, reserved 1048576K
  
【分析:】
重點關注一下三行:
    0.123: [GC (Allocation Failure) [PSYoungGen: 1018K->485K(1536K)] 1018K->557K(5632K), 0.0009012 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    0.145: [GC (System.gc()) [PSYoungGen: 983K->485K(1536K)] 3103K->2661K(5632K), 0.0005754 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
    0.146: [Full GC (System.gc()) [PSYoungGen: 485K->0K(1536K)] [ParOldGen: 2176K->2589K(4096K)] 2661K->2589K(5632K), [Metaspace: 3088K->3088K(1056768K)], 0.0040756 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
其中:
    GC、Full GC 表示停頓型別,GC 為年輕代收集, Full GC 為整堆收集(發生 STW,收集堆 與 方法區)。
    (Allocation Failure) 表示引起 GC 原因為年輕代中沒有足夠記憶體儲存新資料。
    (System.gc()) 表示引起 GC 原因為觸發了 System.gc()。
    [PSYoungGen: 1018K->485K(1536K)] 表示回收年輕代,回收前記憶體 1018K,回收後 485K,年輕代總大小 1536K。
    [ParOldGen: 2176K->2589K(4096K)] 表示回收老年代,回收前記憶體 2176K,回收後記憶體 2589K,老年代總大小 4096K。
    3103K->2661K(5632K), 0.0005754 secs 表示回收年輕代、老年代,回收前 3103K,回收後 2661K,年輕代、老年代總大小 5632K。GC 時間為 0.0005754 秒。
    [Times: user=0.00 sys=0.00, real=0.00 secs] 表示使用者態回收耗時,系統核心態回收耗時,實際耗時。
    
注:
    不同收集器,年輕代、老年代 名字不同。
    收集器             年輕代             老年代
    Serial GC          DefNew            Tenured
    ParNew GC          ParNew            Tenured
    Parallel GC        PSYoungGen        ParOldGen

&n