主頁 > 後端開發 > 學習一下 JVM (三) -- 了解一下 垃圾回收

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

2020-09-13 12:36:09 後端開發

一、簡單了解幾個概念

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 = https://www.cnblogs.com/l-y-h/p/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

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/24420.html

標籤:Java

上一篇:MySQL的事務隔離級別是什么?

下一篇:【國家地址庫】省市區三級資料生成方案

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more