JVM效能調優與實戰基礎理論篇-下

itxiaoshen發表於2022-02-15

JVM記憶體管理

JVM記憶體分配與回收策略

  • 物件優先在Eden分配,如果Eden記憶體空間不足,就會發生Minor GC。虛擬機器提供了-XX:+PrintGCDetails這個收集器日誌引數,告訴虛擬機器在發生垃圾收集行為時列印記憶體回收日誌,並且在程式退出的時候輸出當前的記憶體各區域分配情況。

    • 新生代GC(Minor GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。
    • 老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC,(但非絕對的,在Parallel Scavenge收集器的手機策略裡就有直接進行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

    VM option 增加-XX:+PrintGCDetails,觸發GC,列印出年輕代和年老代的記憶體資訊

    image-20220213235445632

  • 大物件直接進入老年代。虛擬機器提供了一個 -XX:PretenureSizeThreshold 引數 ,大於這個數量直接在老年代分配,預設為0 ,表示絕不會直接分配在老年代。

    • 大物件:需要大量連續記憶體空間的Java物件,比如很長的字串和大型陣列。大物件容易導致記憶體還有不少空間時,就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們
    • 比遇到一個大物件更加壞的訊息就是遇到一群“朝生夕滅”的“短命大物件”會進行大量的記憶體複製。VM option 增加-XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000000 -XX:+UseSerialGC,觸發大於1M的大物件直接進入老年代,設定這個引數可以避免為大物件分配記憶體時的複製操作而降低效率。

    image-20220214000303690

  • 長期存活的物件將進入老年代。預設15歲,-XX:MaxTenuringThreshold 引數可調整。

  • 動態物件年齡判定。為了能更好地適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

  • 空間分配擔保。新生代中有大量的物件存活,survivor空間不夠,當出現大量物件在MinorGC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有物件都存活),就需要老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代.只要老年代的連續空間大於新生代物件的總大小或者歷次晉升的平均大小,就進行Minor GC,否則FullGC。所以,新生代一般不會記憶體溢位,因為有老年代做擔保。

image-20220214001909047

判斷物件是否可以回收

引用計數法

引用計數法 即給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1。當計數器為0時,就認為該物件就是不可能再被使用的。

  • 優點:快、方便、實現簡單。
  • 缺點:物件互相引用時很難判斷物件是否該回收

可達性分析

可達性分析演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。而作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變數表)中的物件。(方法中的引數,方法體中的區域性變數)
  • 方法區中 類靜態屬性的物件。 (static)
  • 方法區中 常量的物件。 (final static)
  • 本地方法棧中 JNI(即一般說的Native方法)的物件。

image-20220214002136510

image-20220214003252607

image-20220214003312658

引用型別

無論是通過引用計數演算法判斷物件的引用數量,還是通過可達性分析演算法判斷物件的引用鏈是否可達,判斷物件是否存活都與引用有關,那麼就讓我們再次來談一談引用。

  • 強引用:就是指在程式程式碼中普遍存在的,類似於“Object obj = new Object() ”這類的就是強引用。只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。
  • 軟引用:是用來描述 一些有用但是並非必需 的物件。用軟引用關聯的物件,系統將要發生OOM之前,這些物件就會被回收。
package cn.itxs.entity;

public class User {
    public int id = 0;
    public String name = "";
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + "]";
    }
}

測試類,vm option 新增-Xms100m -Xmx100m -XX:+PrintGC,執行

package cn.itxs.garbage;

import cn.itxs.entity.User;

import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.List;

public class SoftReferenceMain {
    public static void main(String[] args) {
        User u = new User(100,"IT小神"); //new是強引用
        //軟引用的使用示例:
        SoftReference<User> userSoft = new SoftReference<User>(u);
        u = null;//幹掉強引用,確保這個例項只有userSoft的軟引用
        //--- 如果是 SoftReference<User> userSoft = new SoftReference<User>(new User()); 就沒法幹掉強引用
        System.out.println(userSoft.get());
        System.gc();//進行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userSoft.get());

        //往堆中填充資料,導致OOM
        List<byte[]> list = new LinkedList<>();
        try {
            for(int i=0;i<100;i++) {
                System.out.println("*************"+userSoft.get());
                list.add(new byte[1024*1024*50]); //1M的物件
            }
        } catch (Throwable e) {
            //丟擲了OOM異常時列印軟引用物件
            System.out.println("Exception*************"+userSoft.get());
        }
    }
}

image-20220214004930814

  • 弱引用:一些有用(程度比軟引用更低)但是並非必需,用弱引用關聯的物件,只能生存到下一次垃圾回收之前,GC發生時,不管記憶體夠不夠,都會被回收。
package cn.itxs.garbage;

import cn.itxs.entity.User;

import java.lang.ref.WeakReference;

public class WeakReferenceMain {
    public static void main(String[] args) {
        User u = new User(1,"小爽");
        WeakReference<User> userWeak = new WeakReference<User>(u);
        u = null;//幹掉強引用,確保這個例項只有userWeak的弱引用
        System.out.println(userWeak.get());
        System.gc();//進行一次GC垃圾回收
        System.out.println("After gc");
        System.out.println(userWeak.get());
    }
}

image-20220214005347085

  • 虛引用:幽靈引用,最弱,被垃圾回收的時候收到一個通知。

軟引用 SoftReference和弱引用 WeakReference,可以用在記憶體資源緊張的情況下以及建立不是很重要的資料快取。當系統記憶體不足的時候,快取中的內容是可以被釋放的。實際運用如WeakHashMap、ThreadLocal

finalize()方法最終判定物件是否存活

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:

  • 第一次標記:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”和直接回收。

  • 第二次標記:如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列之中,由一低優先順序執行緒執行該佇列物件中的finalize方法. 執行完畢後, GC會再次判斷可達性(即只有一次自救的機會), 若不可達, 則直接進行回收, 否則物件“復活”

  • finalize()是Object的protected方法,子類可以覆蓋該方法以實現資源清理工作,GC在回收物件之前呼叫該方法。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件這個時候,未被重新引用,那它基本上就真的被回收了。

package cn.itxs.entity;

import cn.itxs.garbage.FinalizeMain;

public class User {
    public int id = 0;
    public String name = "";
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User [id=" + id + ", name=" + name + "]";
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("觸發finalize方法...");
        // 進行拯救
        FinalizeMain.user = this;
    }

    public void isAlive() {
        System.out.println("成功復活");
    }
}

測試類

package cn.itxs.garbage;

import cn.itxs.entity.User;

public class FinalizeMain {
    public static User user = null;
    public static void main(String[] args) throws InterruptedException {
        user = new User(100,"IT小神");

        // 物件被GC回收前執行finalize方法, 可以有一次自我拯救的機會
        user = null;
        System.gc();

        // finalize方法優先順序低(JVM會呼叫一個優先順序低的執行緒執行Queue-F佇列中的finalize方法),sleep保證finalize方法已經執行完畢
        Thread.sleep(1000);

        if (null != user) {
            user.isAlive();
        } else {
            System.out.println("被回收了");
        }

        // 嘗試再次自救
        user = null;
        System.gc();

        // 因為finalize()方法優先順序很低, 保證執行
        Thread.sleep(1000);

        if (null != user) {
            user.isAlive();
        } else {
            System.out.println("被回收了");
        }
    }
}

image-20220214112042526

方法區回收

方法區主要回收的是類和常量

  • 如何判斷一個類是無用的類:同時滿足一下3個條件,才能說一個類是無用的類。滿足這3個條件便可以對無用類進行回收,但並非一定會回收。

    • java堆中已沒有該類的例項。
    • 該類的類載入器已經被回收。
    • 該類對應的java.lang.Class物件沒有在任何地方被引用,無法通過反射訪問該類的任何方法。
  • 如何判斷一個常量是廢棄常量:執行時常量池主要回收的是廢棄的常量。

    • 假如在常量池中存在字串"abc" ,若是當前沒有任何String物件引用該字串常量的話,就說明常量"abc"就是廢棄常量,若是這時發生記憶體回收的話並且有必要的話," abc"就會被系統清理出常量池。

垃圾收集演算法

  • 標記-清除演算法(Mark-Sweep):分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

    • 特點是利用率百分之百、不需要記憶體複製。而它的主要不足是空間碎片問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

    image-20220214175254963

  • 複製演算法:將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要按順序分配記憶體即可。新生代使用,有老年代空間擔保。

    • 特點是實現簡單,執行高效;記憶體複製、沒有記憶體碎片;但這種演算法的代價是利用率減半也即是將記憶體縮小為原來的一半。

image-20220214180013314

  • 標記-整理演算法(Mark-Compact):首先標記出所有需要回收的物件,在標記完成後,後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
    • 特點是利用率百分之百、 沒有記憶體碎片;但需要進行記憶體複製、效率也一般。

image-20220214180900632

  • 分代收集演算法:根據物件存活的不同生命週期將記憶體劃分為不同的域,可以根據不同區域選擇不同的演算法。新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,一般使用複製演算法。老年代的特點是每次垃圾回收時只有少量物件需要被回收,一般使用標記-整理演算法或標記-清除演算法;

image-20220214181401894

垃圾收集器

概述

垃圾收集演算法是記憶體回收的方法論,那垃圾收集器就是記憶體回收的實現。HotSpot虛擬機器常見的垃圾收集器所處新生代和年老代、搭配使用關係如下圖

image-20220215112226421

  • HotSpot虛擬機器常見其中垃圾收集器為Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。而作為未來趨勢的ZGC收集器在JAVA11中開始引入,JAVA16優化推薦生產使用,目前越來越多大廠開始使用ZGC我們在後續單獨研究ZGC。
  • 所處區域上半部分為新生代收集器下半部分為老年代收集器
    • 新生代收集器:Serial、ParNew、Parallel Scavenge。
    • 老年代收集器:Serial Old、Parallel Old、CMS。
    • 整堆收集器:G1。
  • 兩個收集器間有連線,表明它們可以搭配使用。組合關係:Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1。其中
    • ParNew/Serial Old:與Serial/Serial Old相比,只是比年輕代多了多執行緒垃圾回收而已
    • ParNew/CMS:目前使用較多,比較高效的組合
    • Parallel Scavenge/Parallel Old:自動管理的組合
    • G1:屬於上面7種最先進整堆垃圾收集器
  • 其中Serial Old作為CMS出現"Concurrent Mode Failure"失敗的後備預案使用。

Serial收集器

Serial(序列)收集器是最基本、發展歷史最悠久的收集器。用於Client模式;它是一個單執行緒收集器,只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是它在進行垃圾收集時,必須暫停其他所有的工作執行緒,直至Serial收集器收集結束為止(“Stop The World”)。年輕代Serial收集器採用單個GC執行緒實現"複製"演算法(包括掃描、複製),年老代Serial Old收集器採用單個GC執行緒實現"標記-整理"演算法。Serial與Serial Old都會暫停所有使用者執行緒(即STW),設定引數為

  • -XX:+UseSerialGC。
  • -XX:+UseSerialOldGC。

image-20220215115547084

ParNew 收集器

ParNew 收集器除了多執行緒外,其餘的行為、特點和Serial收集器一樣;但是此組合中的Serial Old又是一個單GC執行緒,所以該組合是一個比較尷尬的組合。

設定引數:

  • "-XX:+UseParNewGC":強制指定使用ParNew;
  • "-XX:ParallelGCThreads":指定垃圾收集的執行緒數量,ParNew預設開啟的收集執行緒與CPU的數量相同;

image-20220215120421260

Parallel Scavenge收集器

Parallel Scavenge收集器也是一個並行的多執行緒新生代收集器,它也使用複製演算法。用於Server模式;Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。主要注重吞吐量(吞吐量越大,說明CPU利用率越高,所以主要用於處理很多的CPU計算任務而使用者互動任務較少的情況),目標是達到一個可控制的吞吐量(Throughput)。 設定引數為

  • "-XX:+UseParallelScavengeGC":指定使用Parallel Scavenge收集器;
  • "-XX:+UseParallelOldGC":指定使用Parallel Old收集器;

image-20220215121130968

CMS收集器

併發標記清理(Concurrent Mark Sweep,CMS)收集器也稱為併發低停頓收集器(Concurrent Low Pause Collector)或低延遲(low-latency)垃圾收集器。

  • 特點

    • 以獲取最短回收停頓時間為目標。
    • 併發收集、低停頓;是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器;第一次實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作。
  • 缺點

    • 對CPU資源非常敏感。
    • 無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗。
    • 基於"標記-清除"演算法(不進行壓縮操作,產生記憶體碎片) 。
      • "-XX:+UseCMSCompactAtFullCollection",使得CMS出現上面這種情況時不進行Full GC,而開啟記憶體碎片的合併整理過程;但合併整理過程無法併發,停頓時間會變長;預設開啟(但不會進行,結合下面的CMSFullGCsBeforeCompaction)。
      • "-XX:+CMSFullGCsBeforeCompaction": 設定執行多少次不壓縮的Full GC後,來一次壓縮整理;為減少合併整理過程的停頓時間; 預設為0,也就是說每次都執行Full GC,不會進行壓縮整理。
  • 應用場景

    • 與使用者互動較多的場景。
    • 希望系統停頓時間最短,注重服務的響應速度。
    • 以給使用者帶來較好的體驗。
    • 如常見WEB、B/S系統的伺服器上的應用。
  • 運作過程:總體上說CMS收集器的記憶體回收過程與使用者執行緒一起併發執行;分為一下四步:

    • 初始標記(CMS initial mark):僅標記一下GC Roots能直接關聯到的物件;速度很快;但需要"Stop The World"。
    • 併發標記(CMS concurrent mark): 進行GC Roots Tracing的過程;剛才產生的集合中標記出存活物件;應用程式也在執行; 並不能保證可以標記出所有的存活物件。
    • 重新標記(CMS remark):為了修正併發標記期間因使用者程式繼續運作而導致標記變動的那一部分物件的標記記錄;需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;採用多執行緒並行執行來提升效率。
    • 併發清除(CMS concurrent sweep):回收所有的垃圾物件;整個過程中耗時最長的併發標記和併發清除都可以與使用者執行緒一起工作;
  • 設定引數

    • "-XX:+UseConcMarkSweepGC":指定使用CMS收集器;會預設使用ParNew作為新生代收集器。
  • 總體來看,與Parallel Old垃圾收集器相比,CMS減少了執行老年代垃圾收集時應用暫停的時間; 但卻增加了新生代垃圾收集時應用暫停的時間、降低了吞吐量而且需要佔用更大的堆空間;

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-0Ye0vKYK-1644940079235)(http://www.itxiaoshen.com:3001/assets/1644899687176xbFaCsiJ.png)]

G1收集器

從G1與上面的CMS運作過程相比,僅在最後的"篩選回收"部分不同(CMS是併發清除)。

  • 特點

    • 能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;能夠採用不同方式處理不同時期的物件;雖然保留分代概念,但Java堆的記憶體佈局有很大差別;它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,而都是一部分Region(不需要連續)的集合。

    image-20220215123701731

    • 可預測的停頓:低停頓的同時實現高吞吐量;G1除了追求低停頓處,還能建立可預測的停頓時間模型; 可以明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒。
      • G1可以建立可預測的停頓時間模型,有計劃地避免在Java堆的進行全區域的垃圾收集是因為G1跟蹤各個region裡面的垃圾堆積的價值(回收後所獲得的空間大小以及回收所需時間長短的經驗值),在後臺維護一個優先列表;每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);在指定的時間內,掃描部分最有價值的region(而不是掃描整個堆記憶體),並回收,做到儘可能的在有限的時間內獲取儘可能高的收集效率。
    • 結合多種垃圾收集演算法,空間整合,不產生碎片;從整體看,是基於標記-整理演算法;從區域性(兩個Region間)看,是基於複製演算法。
  • 應用場景

    • 面向服務端應用,針對具有大記憶體、多處理器的機器。
    • 需要低GC延遲,並具有大堆的應用程式提供解決方案。
  • 運作過程

    • 初始標記(Initial Marking) 僅僅只是標記一下GC Roots 能直接關聯到的物件,並且修改TAMS(Nest Top Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可以的Region中建立物件,此階段需要停頓執行緒,但耗時很短。
    • 併發標記(Concurrent Marking) 從GC Root 開始對堆中物件進行可達性分析,找到存活物件,此階段耗時較長,但可與使用者程式併發執行。
    • 最終標記(Final Marking) 為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程的Remembered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這階段需要停頓執行緒,但是可並行執行。
    • 篩選回收(Live Data Counting and Evacuation) 首先對各個Region中的回收價值和成本進行排序,根據使用者所期望的GC 停頓是時間來制定回收計劃。此階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅度提高收集效率。
  • image-20220215125336090

  • 設定引數

    • "-XX:+UseG1GC":指定使用G1收集器。
    • "-XX:InitiatingHeapOccupancyPercent":當整個Java堆的佔用率達到引數值時,開始併發標記階段;預設為45。
    • "-XX:MaxGCPauseMillis":為G1設定暫停時間目標,預設值為200毫秒。
    • "-XX:G1HeapRegionSize":設定每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region。

**本人部落格網站 **IT小神 www.itxiaoshen.com

相關文章