JVM解析(二) JVM 垃圾回收器

一. 概覽

    定義

        JVM的垃圾回收器是Java內存管理中的一個重要組成部分。其主要任務是自動回收不再被程序使用的對象所佔用的內存空間,以避免內存泄漏和內存溢出等問題。

    作用區域

        垃圾回收器主要作用於堆和方法區,對於線程私有的區域(程序計數器、虛擬機棧、本地方法棧)垃圾回收器不會進行回收。因爲這些區域的內存,會隨着線程的死亡而釋放。

    判斷對象是否可以回收

        引用計數器算法:每個對象中有個被其他對象引用的計數器,如果被一個對象引用了,那麼計數器加一。如果在垃圾回收器回收過程中,發現對象不存在被引用的情況,那麼該對象會被回收。但是有一個缺點,如果存在循環引用的情況(如以下代碼),那麼對象將無法被回收器回收。所以目前不推薦使用引用計數器方法來判斷對象是否可以回收。

JvmGcDemoClass a = new JvmGcDemoClass();
JvmGcDemoClass b = new JvmGcDemoClass();
a.setReference(b);
b.setReference(a);

第一步:創建A對象,存儲在堆空間中,但是a變量是存儲在棧幀裏面的局部變量表中,所以a的引用地址就是堆空間引用地址 
第二步:創建B對象,存儲在堆空間中,但是b變量也是存儲在棧幀裏面的局部變量表中,所以b的引用地址就是堆空間引用地址 
第三步:A對象的屬性object的引用地址指向了B對象的引用地址 
第四步:B對象的屬性object的引用地址也執行了A對象的引用地址
第五步:局部變量表中的a變量引用地址置爲null,直接將下圖中的第一步去掉了
第六步:局部變量表中的b變量引用地址置爲null,直接將下圖中的第二步去掉了
這樣就導致了堆空間中的循環相互引用的問題

                                

        可達性分析算法:從根對象(GC Root)開始,沿着引用鏈下去進行追溯。如果一個對象沒有任何一個引用鏈,那麼這個對象可以被回收。目前流行的解決方案。

            1. 可被當作GC Root的對象                  

(1)首先第一種是虛擬機棧中的引用的對象,我們在程序中正常創建一個對象,對象會在堆上開闢一塊空間,同時會將這塊空間的地址作爲引用保存到虛擬機棧中,如果對象生命週期結束了,那麼引用就會從虛擬機棧中出棧,因此如果在虛擬機棧中有引用,就說明這個對象還是有用的,這種情況是最常見的。 
(2)第二種是我們在類中定義了全局的靜態的對象,也就是使用了static關鍵字,由於虛擬機棧是線程私有的,所以這種對象的引用會保存在共有的方法區中,顯然將方法區中的靜態引用作爲GC Roots是必須的。 
(3)第三種便是常量引用,就是使用了static final關鍵字,由於這種引用初始化之後不會修改,所以方法區常量池裏的引用的對象也應該作爲GC Roots。 
(4)最後一種是在使用JNI技術時,有時候單純的Java代碼並不能滿足我們的需求,我們可能需要在Java中調用C或C++的代碼,因此會使用native方法,JVM內存中專門有一塊本地方法棧,用來保存這些對象的引用,所以本地方法棧中引用的對象也會被作爲GC Roots。 

    · 虛擬機棧(棧幀中的本地變量表)中引用的對象。(可以理解爲:引用棧幀中的本地變量表的所有對象)
    · 方法區中靜態屬性引用的對象(可以理解爲:引用方法區該靜態屬性的所有對象)
    · 方法區中常量引用的對象(可以理解爲:引用方法區中常量的所有對象)
    · 本地方法棧中(Native方法)引用的對象(可以理解爲:引用Native方法的所有對象)

            2. 對象最終是否會被回收?(摘自深入理解 JVM )

                即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots 相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize() 方法。當對象沒有覆蓋finalize() 方法,或者finalize() 方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。

                如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queuc的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是如果一個對象在finalizeO 方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。finalize() 方法是對象逃脫死亡命運的最後一次機公稍後GC將對F-QUCUC中的對象進行第二次小規模的標記,如果對象要在finalize()中成功揚救自己一隻要重新與引用鏈上的任何- 一個對象建立關聯即可,譬如把自己(this 關鍵字) 賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合: 如果對象這時候還沒有逃脫,那基本上它就真的被回收了。從代碼清單3-2 中我們可以看到一個對象的finalize()被執行,但是它仍然可以存活。代碼清單3-2一次對象自我拯救的演示。

/*此代碼演示了兩點
 * 對象可以在GC時自我拯救
 * 這種自救只會有一次,因爲一個對象的finalize方法只會被自動調用一次
 * */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK=null;
    public void isAlive(String prefix){
        System.out.println(prefix + "yes我還活着");
    }
    public void finalize() throws Throwable{
        super.finalize();
        System.out.println("執行finalize方法");
        FinalizeEscapeGC.SAVE_HOOK=this;//自救
    }
    public static void main(String[] args) throws InterruptedException{
        SAVE_HOOK=new FinalizeEscapeGC();
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("1 ");
        }else{
            System.out.println("1 no我死了");
        }
        //對象的第一次回收
        SAVE_HOOK=null;
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("2 ");
        }else{
            System.out.println("2 no我死了");
        }
        System.gc();
        //因爲finalize方法的優先級很低所以暫停0.5秒等它
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("3 ");
        }else{
            System.out.println("3 no我死了");
        }
        //下面的代碼和上面的一樣,但是這次自救卻失敗了
        //對象的第一次回收
        SAVE_HOOK=null;
        System.gc();
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive("4 ");
        }else{
            System.out.println("4 no我死了");
        }
    }
}

二. 市面上的GC回收器

垃圾回收器 作用區域 回收算法 是否會中斷其他線程
Serial 新生代 複製
ParNew 新生代 標記複製
Parallel Scavenge 新生代 標記複製
Serial Old 年老代 標記複製
Parallel Old 年老代 標記整理
CMS(Concurrent Mark Sweep) 年老代 標記清除
G1 新生代,年老代 標記整理 看情況

        G1垃圾回收算法,以標記整理作爲主要算法,但是和其他的標記清除不太一樣。G1並不是針對內存塊,而是針對內存區域(Region)。G1不再將年老代、新生代進行分類存放,而是以標記的形式,給一塊區域定義爲年老代或者新生代。

        G1垃圾回收算法是否會中斷其他線程,看情況,內部執行流程中一些會中斷用戶線程,一些不會。

            ​​​​​​​    ​​​​​​​    ​​​​​​​    ​​​​​​​    

三. 不同的GC對比

        1. Serial 回收器

            1. 執行流程

                

            2. 特點

                1. 單線程處理

                2. HotSpot中client模式下的默認新生代垃圾收集器

                3. 執行GC時會暫停其他所有線程 ParNew 回收器

    2. ParNew 垃圾回收器

        1. 執行流程

                

        2. 特點

                1. 多線程實現垃圾回收,默認線程數和CPU核心數一致

                2. 是Server模式下首選的垃圾回收器

                3. 是除了Serial回收器之外,能和CMS回收器搭配的回收器 Parallel Scavenge回收器

    3. Parallel Scavenge回收器

            1. 執行流程

                與ParNew 回收器類似,也是多線程垃圾回收器                

            2. 特點

                1. 吞吐量有限的處理器,以達到一個可控制的吞吐量爲標準進行垃圾回收的處理器

                2. 自適應的調節策略(+UseAdaptiveSizePolicy), 虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。只需要把基本的內存數據設置好(如-Xmx設置最大堆),然後使用-XX: MaxGCPauseMillis參數(更關注最大停頓時間)或-XX: GCTimeRatio(更關注吞吐量) 參數給虛擬機設立一個優化目標, 那具體細節參數的調節工作就由虛擬機完成了。 Serial Old 回收器

        4. Serial 垃圾回收器

            1. 執行流程

                    

            2. 特點

                1. 是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機使用

                2. 在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用 (Parallel Old 誕生以前)

                3. 作爲 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。 Parallel Old回收器

        5. Parallel Old 垃圾回收器

            1. 執行流程

                

            2. 特點

                1. Parallel Scavenge收集器的老年代版本

                2. 在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。 CMS 回收器

        6. CMS 垃圾回收器

            1. 執行流程

                    

            2. 特點

                1. 併發清除,可以和用戶線程同步執行

                2. 吞吐量低:低停頓時間是以犧牲吞吐量爲代價的,導致 CPU 利用率不夠高

                3. 無法處理浮動垃圾,可能出現 Concurrent Mode Failure。浮動垃圾是指併發清除階段由於用戶線程繼續運行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收。由於浮動垃圾的存在,因此需要預留出一部分內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啓用 Serial Old 來替代 CMS。

                4. 標記 - 清除算法導致的空間碎片,往往出現老年代空間剩餘,但無法找到足夠大連續空間來分配當前對象,不得不提前觸發一次 Full GC。 G1 垃圾回收器

        7.  G1垃圾回收器

            1. 執行流程

                

            2. 特點

                1. 空間整合: 整體來看是基於“標記 - 整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的,這意味着運行期間不會產生內存空間碎片。

                2. 可預測的停頓: 能讓使用者明確指定在一個長度爲 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒。

四. 內存分配與回收策略(留個坑,這兩塊內容比較多)

        · G1回收器與ParNew回收器的性能對比

        · JDK17默認的ZGC

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章