JVM學習之垃圾回收和垃圾回收器

目錄

背景

概述

垃圾定義

爲何需要GC

早期垃圾回收

Java的垃圾回收機制

相關算法

標記階段:引用計數算法

標記階段:可達性分析算法

對象的finalization機制

使用MAT進行GC Roots溯源

清除階段:標記-清除算法

清除階段:複製算法

清除階段:標記-壓縮算法

小結

分代收集算法

增量收集算法、分區算法

增量收集算法

分區算法

相關概念

System.gc()的理解

內存溢出與內存泄漏

內存溢出(OOM)

內存泄漏

Stop The World

垃圾回收的並行與併發

安全點與安全區域

安全點

安全區域

實際執行

強引用

軟引用

弱引用

虛引用

終結器引用

垃圾回收器

GC分類與性能指標

分類

性能指標

吞吐量

暫停時間

不同的垃圾回收器概述

Serial回收器:串行回收

ParNew回收器:並行回收

Parallel回收器:吞吐量優先

CMS回收器:低延遲

G1回收器:區域化分代式

分區算法的特點

參數設置

區域Region:化整爲零

簡要回收過程

記憶集與寫屏障

詳細回收過程

優化建議

垃圾回收器總結

GC日誌分析

垃圾回收器的新發展

EpsilonGC

Shenadoah GC

ZGC

其他GC

結語


背景

遲遲不開學,賦閒多日,學習新東西以打發時間,現在整理下JVM的最後一部分筆記——垃圾回收和垃圾回收器

概述

垃圾定義

進程中沒有任何指針指向的對象,是爲垃圾

爲何需要GC

爲了方便JVM整理出內存分配給新的對象,不進行GC的話,內存遲早要被消耗完。

早期垃圾回收

C階段,使用malloc、realloc、calloc函數申請內存,使用free函數釋放內存

C++階段,使用new關鍵字申請內存,使用delete關鍵字釋放內存

Java的垃圾回收機制

Java的自動內存管理,可以降低內存泄漏和溢出的風險,讓程序員更專注業務開發

GC作用區域是堆和方法區,頻繁回收新生代,較少收集老年代,基本不動方法區

相關算法

垃圾回收分爲標記-清除兩階段,第一階段標記需要回收的對象,第二階段對這些對象進行回收

標記階段:區分出哪些對象是存活對象,哪些是死亡的對象,死亡的定義是不被任何對象繼續引用

標記階段:引用計數算法

對每個對象都保存一個整型的引用計數器屬性,記錄對象被引用的情況

如果任何一個對象引用了對象A,則A的引用計數器+1,如果引用失效了,計數器就-1

如果A的引用計數器值爲0,則A不可能再被使用,可進行回收

 

優點:

1)、實現簡單,垃圾對象便於標識

2)、判斷效率高,回收沒有延遲

 

缺點:

1)、需要單獨的字段存儲計數器增加了存儲空間開銷;

2)、每次賦值都要更新計數器,伴隨着加法減法操作,增加了時間開銷;

3)、無法處理循環引用的情況,這一點直接導致java的垃圾回收器裏沒有使用這種算法

 

循環引用:

對於以下循環鏈表,p指針指向頭結點,尾結點的next對象指向頭結點,這種情況就是循環引用

將p置爲null,頭結點的計數器-1,值爲1,導致不能被回收,從而發生內存泄漏

 

 

雖然java沒有使用引用計數,但python用了,但也對其進行了優化:

1)、手動解除:在合適的時機,解除引用關係

2)、使用弱引用weakref,這是python提供的標準庫,旨在解決循環引用

標記階段:可達性分析算法

基本思路:

1)、可達性分析是以根對象集合(GC Roots)爲起始點,按照從上至下的方式搜索被根對象集合所連接的目標是否可達

2)、使用可達性分析算法後,內存中的存活對象都會被根對象集合直接或間接連接着,搜索走過的路徑稱之爲引用鏈

3)、如果目標對象沒有任何引用鏈相連,則是不可達的,意味着此對象已經死亡,可以被標記爲垃圾對象

4)、在可達性分析算法中,只有能被根對象集合直接或間接連接的對象纔是存活對象

GC Roots包含以下幾類元素:

1)、棧中引用的對象(局部變量表),虛擬機棧或本地方法棧

2)、方法區中類的靜態屬性引用的對象

3)、方法區中常量引用的對象

4)、同步鎖syncronized持有的對象

5)、JVM內部的引用:Class對象、常駐異常對象、系統類加載器

6)、反應JVM內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等

 

如果一個指針指向了堆內存(元空間外)的對象,而自己又不在堆內存(元空間外)裏面,那他就是一個GC Root

 

除了以上常規的GC Roots集合外,根據用戶所選的垃圾回收器以及當前回收的內存區域的不同(方法區),還可以將其他對象臨時性加入,共同構成完整的根對象集合。比如分代收集和局部回收。

如果只針對堆中某一塊區域進行垃圾回收(比如只針對新生代),就必須考慮內存區域的實現細節,因爲這個區域的對象完全有可能被其他區域的對象所引用,這是就需要一併將關聯區域的對象也加入根結點集合,才能保證可達性分析的準確性。

 

注意,如果使用可達性分析來判斷內存是否可回收,就必須在一個能保證一致性的快照中進行分析工作,這樣才能保證分析結果的準確性,所以GC時必須Stop The World暫停用戶線程

對象的finalization機制

在回收某對象之前,總會先調用這個對象的finalize()方法,我們可以覆寫此方法,用於在對象被回收時進行資源釋放等收尾工作

 

永遠不要主動調用某對象的finalize()方法,原因有三:

1)、在finalize()時,可能會導致對象復活

2)、finalize()方法執行時間是沒有保障的,完全由GC線程決定。一個極端條件下,則此方法沒有執行機會

3)、如果finalize()方法寫的不好,會嚴重影響GC性能

 

由於finalize()方法的存在,虛擬機中的對象一般處於三種可能的狀態:

1)、可觸及的:從根結點開始,可以到達這個對象

2)、可復活的:對象的所有引用都被釋放,但是對象有可能在finalize()方法中復活

3)、不可觸及的:對象的finalize()方法被調用,且沒有復活,那麼就會進入不可觸及狀態。不可觸及的對象不可能被複活,因爲finalize()方法只會被調用一次

 

判斷一個對象objA是否可回收,至少要經歷兩次標記過程:

1)、如果objA到根結點集合沒有引用鏈,則進行第一次標記

2)、進行篩選,看此objA是否有必要執行finalize()方法

    a.如果此對象沒有覆寫finalize()方法,或者finalize()方法已經被虛擬機調用過,則虛擬機視爲沒有必要執行,objA被判定爲不可觸及的

    b.如果對象覆寫了finalize()方法,且沒有被執行過,那麼objA會被插入到F-Queue隊列中。這是一個由虛擬機自動創建的、低優先級的Finalizer線程,此線程會觸發objA的finalize()方法去執行

    c.finalize()方法是對象逃脫死亡的最後機會,稍後GC會對F-Queue中的對象進行第二次標記。如果objA在finalize()方法中與引用鏈上的任何一個對象建立了聯繫,那麼第二次標記時,objA會被移出“即將回收”集合。之後,如若對象再次出現沒有引用存在的情況,此時,finalize()方法不會再被調用,對象會直接變成不可觸及的對象,換言之,finalize()方法只會被調用一次

 

所以只有在對象不可觸及時纔會被回收

 

對於對象在finalize()方法中復活的代碼演示

package jvm.gcDemos;

public class RebornObj {
    public static RebornObj obj;


    public static void main(String[] args) throws InterruptedException {
        obj = new RebornObj(); // 在堆空間中new一個RebornObj對象,並讓元空間的obj指針指向它
        obj = null;
        System.gc(); // 第一次gc,堆中對象的finalize()方法被調用
        System.out.println("第一次gc");


        Thread.sleep(2000);


        if (obj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }


        obj = null;
        System.gc(); // 第二次gc,堆中對象的finalize()方法不會被調用
        System.out.println("第二次gc"); 


        if (obj == null) {
            System.out.println("obj is dead");
        } else {
            System.out.println("obj is still alive");
        }
    }


    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("待回收對象的finalize方法被調用");
        obj = this;
    }
}

控制檯輸出

待回收對象的finalize方法被調用
第一次gc
obj is still alive
第二次gc
obj is dead


Process finished with exit code 0

使用MAT進行GC Roots溯源

1)、首先下載MAT,可訪問官網https://www.eclipse.org/mat/

下載後直接解壓,打開裏面的MemoryAnalyzer.exe就能用

2)、編寫樣例代碼

package jvm.gcDemos;


import java.util.ArrayList;
import java.util.Date;
import java.util.Scanner;


public class GCRoots {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<String>();
        Date date = new Date();


        for (int i = 0; i < 100; i++) {
            strings.add(String.valueOf(i));
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


        System.out.println("添加數據結束,請指示:");
        new Scanner(System.in).next();


        strings = null;
        date = null;


        System.out.println("數據已置空,請結束:");
        new Scanner(System.in).next();
        System.out.println("結束");


    }
}

3)、運行以上代碼,會阻塞在第一次控制檯輸入,此時打開jvisualvm,對此進程的堆進行快照。關於jvisualvm的簡單使用,可以參見文章JVM學習筆記之堆的核心概述部分

會在左側出現新建好的快照

右擊它,選擇保存的目錄,點擊確定即可

 

然後在控制檯輸入指令,讓程序釋放strings和date引用,此時會阻塞在第二次控制檯輸入,然後重複上面操作,生成第二個快照文件並保存,最後在控制檯隨便輸入文字,讓程序結束即可

關閉jvisualvm後,在我們指定的目錄下,會有生成的兩個快照文件

其他文件都是我之前用mat分析快照文件時生成的

4)、用mat打開比較老的hprof文件,點擊中間窗口中類似數據庫的圖標,點擊Java Basics->GC Roots

在新出來的GC Roots界面,點擊線程選項,可以看到java.lang.Thread,也可以看到傳說中的FinalizerThread。我們點擊java.lang.Thread,找到主線程,裏面可以看到我們的ArrayList和Date引用,是一個根結點

同樣的方法打開新的hprof文件,這是把strings和date對象都置空後的快照,用上面的方法查看主線程的根結點集合,會發現已經沒有了ArrayList和Date

根據線程下面的統計(21->19),也可以證實對strings和date進行了GC

 

至此,使用mat溯源根結點就完成了

清除階段:標記-清除算法

當堆中有效內存耗盡時,就會停止所有用戶線程(Stop The World),然後進行標記-清除

 

標記:收集器從根結點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄爲可達對象。關於對象的頭部信息等內存佈局,可以參見文章JVM學習之對象的實例化、內存佈局與訪問定位中對象的內存佈局部分

清除:收集器對堆內存從頭到尾進行線性遍歷,如果發現某個對象在其Header中沒有標記爲可達對象,則將其清除。此處的清除不是真的置空,而是把需要清除的對象地址保存在空閒的地址列表裏。下次要加載新的對象時,判斷垃圾的位置空間是否夠,如果夠,就直接用新的對象覆蓋原有的垃圾對象。

 

標記-清除算法過程舉例如下圖所示,綠色對象都是可達對象,在其Header中進行標記;在清除階段,Header中沒有記錄可達標記的黑色對象,就都被回收了

 

 

缺點:

1)、效率不高,要遍歷整個堆空間

2)、進行GC時,需要暫停整個應用程序,導致用戶體驗差

3)、會產生內存碎片,要維護一個空閒鏈表,以便實例化新對象時分配內存

關於空閒鏈表法分配內存,請參見文章JVM學習之對象的實例化、內存佈局與訪問定位中對象實例化的步驟部分

清除階段:複製算法

將活着的內存空間分爲兩塊,每次只使用其中的一塊,在垃圾回收時將正在使用的內存中存活的對象複製到未被使用的內存塊中,之後清除正在使用的內存塊中的所有對象,交換兩個內存塊的角色,最後完成垃圾回收。

 

複製算法舉例如下圖所示,把內存分爲A、B兩塊,兩塊內存大小相同。對A進行GC時,遍歷根結點集合,把所有可達對象複製到未被使用的B區域中,不對對象添加任何標記,然後把A清空,完成GC。對B進行GC時,交換AB角色,如法炮製。

 

在現有JVM中,AB對應的是新生代裏的倖存者0區和倖存者1區,所以倖存者區中的垃圾回收用的就是複製算法

 

優點:

1)、沒有標記和清除過程,實現簡單,運行高效

2)、複製過去後保證空間的連續性,爲新對象分配內存時可採用指針碰撞的方式分配

關於指針碰撞法分配內存,請參見文章JVM學習之對象的實例化、內存佈局與訪問定位中對象實例化的步驟部分

 

缺點:

1)、需要兩倍的內存空間

2)、對於G1這種分拆成大量region的GC,複製而不是移動,意味着GC要維護region之間對象的引用關係,不管是內存佔用還是時間開銷,都不算小

3)、如果系統中垃圾對象很少,則複製算法需要複製的存活對象數量很大,效果就不會很理想

 

不過對於新生代中的對象絕大多數都是朝生夕死,那麼倖存者區使用複製算法就很合適。但老年代就不適合用這個算法了。

清除階段:標記-壓縮算法

標記:此階段和標記-清除算法一樣,從根結點開始標記所有被引用的對象

壓縮:把所有存活對象壓縮到內存的一端,按序排放,並清理邊界外的所有空間

 

標記-壓縮算法舉例如下圖所示

 

標記-壓縮算法和標記-清除算法的區別是:前者是移動性算法,後者是非移動性算法。移動存活對象是一個優缺點並存的風險決策。

 

優點:

1)、消除了內存碎片,可以使用指針碰撞爲新對象分配內存

2)、充分利用內存空間

 

缺點:

1)、效率不如複製算法

2)、移動對象時,如果對象被其他對象引用,則還需調整引用地址

3)、STW時間長

 

小結

三種經典GC算法對比如下表所示

經典GC算法的對比
  標記-清除 標記-壓縮 複製
速率 最慢 最快
空間開銷 少,但會堆積碎片 少,且不會堆積碎片 需要存活對象2倍大小的空間,但不堆積碎片
移動對象

 

複製算法的效率最高,但浪費了太多內存,用空間換時間

綜合三個指標,標記-壓縮算法相對更平滑,但效率很低,比複製算法多了一個標記階段,比標記-清除算法多了一個整理內存階段

 

沒有最優的算法,只有最合適的算法,物盡其值

分代收集算法

分代收集算法是融合了上面三種算法的新算法,它基於對象的生命週期的不同採用不同的收集方式,以便提高回收效率。關於堆中的分代,可以參見文章JVM學習筆記之堆中年輕代與老年代部分

 

目前幾乎所有GC都採用的是分代收集來進行GC的

 

對於年輕代,對象生命週期短,存活率低,回收頻繁,而且空間比老年代小,所以這種情況採用複製算法最合適。HotSpot中的兩個倖存者區可以緩解複製算法對空間的浪費

 

對於老年代,對象生命週期長,存活率高,回收頻率比年輕代低,空間比較大,一般是由標記-清除和標記-壓縮算法混合實現。標記階段的時間開銷和存活對象數量成正比,清除階段的時間開銷和所管理內存大小成正比,壓縮階段時間開銷和存活對象大小成正比

 

以HotSpot中的CMS回收器爲例,CMS是基於標記-清除算法實現的,回收效率高。對於碎片問題,CMS使用基於標記-整理的Serial Old回收器進行補償;當內存回收效果不佳(內存碎片導致的Concurrent Mode Failure)時,將採用Serial Old執行Full GC達到對老年代內存的整理

 

增量收集算法、分區算法

這兩種算法都是爲了減少STW時間,提高用戶體驗

增量收集算法

讓GC線程和用戶線程交替執行,GC每次執行時,只收集一小片區域的內存空間。

增量收集算法通過對線程間衝突的妥善處理,允許GC線程以分階段的方式完成標記、清理或複製工作

 

缺點:因爲線程的切換和上下文轉換的消耗,會造成GC的總體成本上升,造成系統吞吐量的下降

分區算法

將整個堆空間劃分成連續的不同小區間(分代算法是按照對象生命週期長短,把堆區分成兩個部分),每個小區間獨立使用,獨立回收,可以存放伊甸園區、倖存區、老年代等區域的對象或者大對象

這種算法的好處是可以控制一次性回收多少個塊

相關概念

System.gc()的理解

默認情況下,通過System.gc()或Runtime.getRuntime().gc()的調用可以顯示觸發Full GC

然而,System.gc()的調用附帶一個免責聲明,無法保證對垃圾收集器的立刻調用

所以,一般情況下,無需手動觸發GC,否則就太麻煩了

 

如果一定要手動gc,可以同時調用System.gc()和System.runFinalization()兩個方法

 

針對System.gc()方法和局部變量表的關係,請看以下代碼,先使能輸出詳細的GC信息-XX:+PrintGCDetails

package jvm.gcDemos;


public class HandGC {


    public void localVarGc() {
        {
            byte[] bytes = new byte[10 * 1024 * 1024];
        }
        System.gc();
        // 此時bytes還佔用着索引爲1的局部變量表槽,所以不會被回收
    }


    public void localVarGc1() {
        {
            byte[] bytes = new byte[10 * 1024 * 1024];
        }
        int i = 0;
        // 此時bytes所佔的索引爲1的局部變量表槽,已經被i佔據,所以bytes會被回收
        System.gc();
    }


    public static void main(String[] args) {
        HandGC test = new HandGC();
        test.localVarGc();
    }
}

調用方法localVarGc()時,輸出的GC日誌如下

[GC (System.gc()) [PSYoungGen: 15442K->10720K(75776K)] 15442K->11190K(249344K), 0.0201163 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]
[Full GC (System.gc()) [PSYoungGen: 10720K->0K(75776K)] [ParOldGen: 470K->11095K(173568K)] 11190K->11095K(249344K), [Metaspace: 3233K->3233K(1056768K)], 0.0064126 secs] [Times: user=0.09 sys=0.00, real=0.01 secs]
...

System.gc()觸發Full GC,Full GC會先觸發新生代GC,所以第一行是PSYoungGen,PS表示Parallel System,用的是併發回收器

這次Full GC後,年輕代佔用的空間直接清零,但老年代佔用的空間卻漲到了11095K,說明我們代碼塊裏的10M的字節數組對象沒有被回收掉,而是轉到了老年代。

對於GC日誌的解讀,也可以參見文章JVM學習筆記之堆;關於GC日誌參數的設置,可以參見本文的GC日誌分析部分

查看localVarGC的局部變量表

會發現只有一個this指針,但局部變量表最大長度卻是2

說明方法結束時,bytes佔用的槽被清空,但調用System.gc()時方法沒有結束,因此bytes還佔着一個局部變量槽,因此它引用的對象不會被回收

關於局部變量槽與局部變量表,也可以參見文章JVM學習筆記上(概述-本地方法棧)

但調用localVarGc1()時,日誌輸出如下

[GC (System.gc()) [PSYoungGen: 15442K->968K(75776K)] 15442K->976K(249344K), 0.0007605 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [PSYoungGen: 968K->0K(75776K)] [ParOldGen: 8K->829K(173568K)] 976K->829K(249344K), [Metaspace: 3200K->3200K(1056768K)], 0.0046999 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
...

年輕代照樣被清空,老年代也只有829K,盛不下10M的bytes,因此可見bytes被回收了。原因是代碼塊執行完後,System.gc()執行前,我又聲明瞭一個局部變量i,此時方法的局部變量表如下所示

並且局部變量表長度還是2

因此此時,調用System.gc()時,bytes佔用的局部變量槽被i佔用,因此引用消失,字節數組對象得以被回收

內存溢出與內存泄漏

內存溢出(OOM)

沒有空閒內存,並且垃圾回收器回收後也不能提供更多內存

原因有二:

1)、JVM的堆內存設置不夠,可通過-Xms、-Xmx來設置

2)、代碼中創建了大量大對象,並且長時間不能被垃圾回收器回收

內存泄漏

對象不會再被程序使用,但GC又不能回收它們的情況

如上圖所示,如果紅色箭頭表示的引用不消失,藍色框內的對象就都不能被回收,而這些對象如果在程序中都不再使用的話,就發生了內存泄漏

 

舉例:

1)、單例模式:單例的生命週期和應用程序一樣長,所以在單例程序中,如果持有對外部對象的引用的話,這個外部對象是不能被回收的,就會導致內存泄漏

2)、一些提供close()的外部資源沒有關閉

Stop The World

Stop The World(STW)指的是GC事件發生時,會造成用戶線程的停頓,整個應用程序會被暫停,沒有任何響應

這麼做的原因,是因爲GC時要首先找到所有根結點,如果分析過程時對象引用關係還在變化,那麼分析結果的準確性將無法保證

 

GC完成後Stop The World就會結束,所有的GC都有這個事件,不可能被完全消除

垃圾回收的並行與併發

並行垃圾回收:多條收集線程並行工作,用戶線程等待

併發垃圾回收:用戶線程與收集線程同時進行,但依舊有STW

安全點與安全區域

安全點

能夠停下來進行GC的程序位置,稱之爲安全點。

安全點太少,可能導致GC等待時間過長;安全點太多,導致GC頻繁會影響程序性能。安全點的選擇標準爲能讓程序長時間執行

常見的安全點有方法調用、循環跳轉、異常跳轉等

 

發生GC時,在安全點設置一箇中斷標誌,各個線程運行到安全點時輪詢這個標誌,如果這個中斷標誌爲真,則將自己進行中斷掛起。這種方式稱爲主動中斷方式。

安全區域

指的是在一段代碼片段中,對象的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的

實際執行

1)、當線程運行到安全區域的代碼時,首先標識已經進入安全區域。如果這段時間內發生了GC,JVM會忽略標識爲進入安全區域的線程

2)、當線程即將離開安全區域時,會檢查JVM是否已經完成GC,如果完成了就繼續運行;否則線程必須等待直到收到可以安全離開安全區域的信號爲止

強引用

類似Object obj = new Object()這種引用關係,obj就是new Object()對象的強引用。

只要強引用關係還存在,GC就永遠不會回收被引用的對象

 

特點:永遠不回收

 

強引用是默認的引用類型,強引用的對象是可觸及的,GC永遠不會回收被強引用引用的對象,所以強引用是造成OOM的主要原因之一

軟引用

系統要發生OOM之前,將會把只被軟引用引用的對象放入回收範圍之內進行二次回收。如果回收完之後還沒有足夠的內存,才拋出OOM。

 

特點:內存不足就回收

 

軟引用通常用來實現內存敏感的緩存,比如高速緩存裏就用到了軟引用。被軟引用關聯的對象是軟可觸及的,當GC發生時,JVM會把軟引用引用的對象放入軟引用隊列中,在內存不夠、決定清除軟可觸及對象時,清空這個隊列的元素及其引用的對象即可。

 

如果二次回收後內存還不夠,那就只能報OOM了

 

舉例

HandGC test = new HandGC();
SoftReference<HandGC> softReference = new SoftReference<HandGC>(test); // 建立對test引用的對象的軟引用
test = null; // 斷開強引用

// 以上三行代碼等價於
// SoftReference<HandGC> softReference = new SoftReference<HandGC>(new HandGC());

test = softReference.get(); // 使用軟引用對象
if (test!= null) {
    test.localVarGc();
}

弱引用

弱引用對象只能存活到下一次GC之前,GC時,不管內存空間是否足夠,都會將其回收。

 

特點:發現就回收

 

只被弱引用引用的對象只能生存到下一次GC前,由於GC線程優先級很低,所以弱引用對象也可以存在很長時間。

軟引用弱引用都很適合保存那些可有可無的緩存數據

 

一個安卓端使用弱引用優化Handler的例子如下所示

public class WeakHandler extends Handler {
    private WeakReference<IHandler> mWeakHandler;


    public WeakHandler(IHandler handler) {
        mWeakHandler = new WeakReference<>(handler);
    }


    @Override
    public void handleMessage(Message msg) {
        IHandler handler = mWeakHandler.get();
        if (handler != null) {
            handler.handleMessage(msg);
        }
    }
}

IHandler是接口,實現類通常就是Activity。Activity在覆寫onCreate()方法時new一個WeakHandler,把自己傳進去即可

當activity要被銷燬時,WeakHandler裏的mWeakHandler持有activity的弱引用,所以不影響activity對象的回收,因此避免了內存泄漏

虛引用

虛引用存在的唯一目的,就是在此對象被回收時,收到一個系統通知

 

如果一個對象僅持有虛引用,那它和沒有引用基本是一樣的,如果要使用虛引用的get()方法獲取對象時,結果總是null

Object obj = new Object();
ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj, phantomQueue);

obj = null;

特點:回收時通知

 

以下是一個檢測虛引用是否被回收的例子

1)、定義靜態屬性obj,強引用一個堆中對象

private static HandGC obj = new HandGC();

2)、覆寫finalize()方法,使其復活一次

@Override
protected void finalize() throws Throwable {
    super.finalize();
    System.out.println("finalize方法被調用");
    obj = this;
}

3)、在main()方法裏,構造引用隊列和虛引用,並斷開強引用

final ReferenceQueue phantomQueue = new ReferenceQueue();
PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj, phantomQueue);

System.out.println(phantomReference.get());
obj = null;

4)、創建檢測線程,檢查引用隊列是否爲空,並將此線程設置爲守護線程

Thread checkThread = new Thread(new Runnable() {
    public void run() {
        while (true) {
            if (phantomQueue != null) {
                PhantomReference<Object> reference = null;
                try {
                    reference = (PhantomReference<Object>) phantomQueue.remove(); // 如果虛引用沒有被回收,則一直阻塞
                } catch (Exception e) {
                    e.printStackTrace();
                }


                if (reference != null) {
                    System.out.println("追蹤GC,HandGC對象被回收了");
                }
            }
        }
    }
});
checkThread.setDaemon(true);
checkThread.start();

5)、在主線程裏執行兩次gc,並打印日誌

try {
    System.out.println(phantomReference.get());


    obj = null;
    System.gc();


    Thread.sleep(1000);
    if (obj == null) {
        System.out.println("obj is null");
    } else {
        System.out.println("obj可用");
    }


    System.out.println("第二次gc");


    obj = null;
    System.gc();


    Thread.sleep(1000);
    if (obj == null) {
        System.out.println("obj is null");
    } else {
        System.out.println("obj可用");
    }
} catch (Exception e) {
    e.printStackTrace();
}

完整代碼如下

package jvm.gcDemos;


import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;


public class HandGC {
    private static HandGC obj = new HandGC();

    public static void main(String[] args) {
        final ReferenceQueue phantomQueue = new ReferenceQueue();
        PhantomReference<Object> phantomReference = new PhantomReference<Object>(obj, phantomQueue);


        Thread checkThread = new Thread(new Runnable() {
            public void run() {
                while (true) {
                    if (phantomQueue != null) {
                        PhantomReference<Object> reference = null;
                        try {
                            reference = (PhantomReference<Object>) phantomQueue.remove();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }


                        if (reference != null) {
                            System.out.println("追蹤GC,HandGC對象被回收了");
                        }
                    }
                }
            }
        });
        checkThread.setDaemon(true);
        checkThread.start();


        try {
            System.out.println(phantomReference.get());


            obj = null;
            System.gc();


            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj is null");
            } else {
                System.out.println("obj可用");
            }


            System.out.println("第二次gc");


            obj = null;
            System.gc();


            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj is null");
            } else {
                System.out.println("obj可用");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize方法被調用");
        obj = this;
    }
}

運行後,日誌輸出如下

null
finalize方法被調用
obj可用
第二次gc
追蹤GC,HandGC對象被回收了
obj is null


Process finished with exit code 0

第一個"null"表示虛引用的get()一直爲空

第二個"finalize方法被調用"表示第一次gc後,由於強引用斷開,堆中對象被回收finalize方法被調用,對象被重新強引用,所以復活

第三個"obj可用"是自然的,因爲引用了堆中對象

再次把obj置爲null,並執行System.gc()後,堆中對象沒有強引用,徹底被回收。此時檢測線程終於從引用隊列phantomQueue.remove()方法的阻塞中出來,獲取虛引用對象,所以輸出了對應的日誌"追蹤GC,HandGC對象被回收了"

由於finalize()方法只能被調用一次,所以obj此刻爲空,輸出"obj is null"

由於引用檢測線程爲守護線程,當用戶線程執行完後,它也結束了。

終結器引用

終結器引用(FinalReference類),主要用來實現對象的finalize()方法。

終結器引用無需手動實現,其內部和虛引用一樣,配合引用隊列使用

在GC時,終結器引用入隊,由Finalizer線程通過終結器引用找到被引用的對象並調用它的finalize()方法,第二次GC時纔對其回收

垃圾回收器

GC分類與性能指標

分類

1)、按回收線程數來分,可以分爲串行回收器(僅限於client模式)和並行回收器

2)、按工作模式來分:可以分爲併發式回收器(用戶線程和GC線程併發,STW短)和獨佔式回收器(STW長)

3)、按碎片處理方式來分:可分爲壓縮式(標記-壓縮算法、複製算法、指針碰撞再分配)和非壓縮式(標記-清除算法、空閒列表再分配)

4)、按工作的內存區間來分:年輕代回收器和老年代回收器

性能指標

1)、吞吐量:運行用戶代碼的時間佔總運行時間的比例

2)、暫停時間STW:GC時,程序的工作線程被暫停的時間

3)、內存佔用:java堆區所佔內存大小

4)、GC開銷:吞吐量的補數,垃圾回收所用時間佔總運行時間的比例

5)、收集頻率:相對於程序執行,收集操作發生的頻率

6)、快速:一個對象從誕生到被回收所經歷的時間

 

1)2)3)共同構成一個不可能三角,三者總體的表現會隨着技術進步而越來越好,一款優秀的收集器最多同時滿足其中兩項

主要抓住兩項:吞吐量和暫停時間

吞吐量

運行用戶代碼時間 / (運行用戶代碼時間 + GC時間)

如果吞吐量優先,意味着STW時間縮短,而且可以容忍更長的暫停時間

暫停時間

一個時間段內,應用程序暫停讓GC執行的時間。如果注重暫停時間(低延遲),那麼GC頻率就高,吞吐量可能會低

 

注重吞吐量和注重低延遲的GC策略示意圖如下所示

 

高吞吐量和低延遲是相互矛盾的,現在採用的標準是:在最大吞吐量優先的情況下,降低停頓時間

不同的垃圾回收器概述

串行回收器:Serial、Serial Old

並行回收器:ParNew、Parallel Scavenge、Parallel Old

併發回收器:CMS、G1

 

幾種類型的典型回收器如下圖所示,其中Serial爲串行回收器,Parallel爲並行回收器,CMS爲併發回收器,G1爲分區回收器

 

幾種經典收集器和垃圾分代之間的關係如下圖所示

新生代收集器:Serial、ParNew、Parallel Scanvenge

老年代收集器:Serial Old、Parallel Old、CMS

G1既能收集新生代,也能收集老年代

 

垃圾收集器的組合關係如下圖所示,紅線表示jdk8廢棄的組合,綠線表示jdk14廢棄的組合,青色邊框的CMS在jdk14裏直接刪除了

jdk8以前,CMS和Serial Old的組合爲後備組合,因爲CMS併發回收器不能在老年代滿的時候回收,所以老年代滿時需要Serial Old代替它

現在(jdk14)只剩下Serial GC-Serial Old GC、Parallel Scavenge-Parallel Old和G1三種組合了,由於64位PC只能是server模式,所以就只剩下G1和Parallel Scavenge-Parallel Old兩組搭檔

 

爲何要有這麼多回收器呢?因爲java使用場景很多,需要針對不同的場景,提供不同的收集器,提高收集的性能

 

可以使用-XX:+PrintCommandLineFlags查看命令行相關參數,裏面包含使用的回收器

-XX:InitialHeapSize=265990592 -XX:MaxHeapSize=4255849472 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

可見使用的是ParallelGC回收器,也就是Parallel Scavange-Parallel Old組合

 

也可以使用jinfo -flag 參數查看使用的回收器

C:\Users\songzeceng>jps
17568 RemoteMavenServer36
960 KotlinCompileDaemon
10852 Launcher
18772 RebornObj
13368
12460 Jps

C:\Users\songzeceng>jinfo -flag UseParallelGC 9756
-XX:+UseParallelGC

C:\Users\songzeceng>jinfo -flag UseParallelOldGC 9756
-XX:+UseParallelOldGC

C:\Users\songzeceng>jinfo -flag UseG1GC 18772
-XX:-UseG1GC

可見沒有使用G1回收器

Serial回收器:串行回收

Serial收集器是HotSpot中Client模式下的默認新生代收集器,採用複製算法、串行回收和STW機制進行GC

Serial Old專門回收老年代,使用標記-壓縮算法、串行、STW機制進行GC,是Client模式下默認的老年代回收器

Serial Old在Server模式下主要有兩個問題:與新生代的Parallel Scavenge配合使用;作爲老年代CMS收集器的後備方案

 

Serial-Serial Old組合工作模式如下圖所示

此收集器是一個單線程收集器,只用一個GC線程去回收垃圾,並在回收時進行STW

 

優勢:簡單高效,適用於Client模式下的JVM(可用內存不大、GC回收時間短、頻率低)

 

可以使用-XX:+UseSerialGC來使用Serial-Serial Old來使用串行收集器收集新生代和老年代

ParNew回收器:並行回收

採用並行回收、複製算法、STW機制回收新生代內存

ParNew是很多JVM在Server模式下新生代的默認垃圾回收器

 

ParNew-Serial Old搭檔收集器示意圖如下

對於新生代,回收次數頻繁,使用並行方式高效;對於老年代,回收次數少,使用串行方式節省資源

 

在單個CPU環境下,ParNew收集器不比Serial收集器更高效

現在除Serial外,只有ParNew能與CMS配合工作

 

可以使用-XX:+UseParNewGC使能ParNew對年輕代進行GC,使用-XX:ParallelGCThreads限制並行線程數量,默認和CPU核數一樣

Parallel回收器:吞吐量優先

Parallel Scavenge(簡稱Parallel)也採用了複製算法、並行方式和STW進行GC。

Parallel和ParNew的不同是,Parallel目的是達到一個可控的吞吐量,自適應調節策略也是Parallel的一個改進點

 

高吞吐量可以高效利用CPU時間,適合後臺運行的任務。

 

對於老年代,Parallel提供了Parallel Old收集器,使用標記-壓縮算法、並行回收、STW機制進行GC,用以替代Serial Old收集器。

 

Parallel-Parallel Old組合工作示意圖如下

 

jdk8中,Parallel-Parallel Old是默認的回收器

 

可以使用-XX:+UseParallelGC和-XX:+UseParallelOldGC使能Parallel新生代回收器和Parallel Old老年代回收器

使用-XX:ParallelGCThreads設置年輕代並行收集器線程數量,最好和CPU數相等。默認時,如果CPU數≤8,那麼此值=CPU個數;如果CPU數>8,此值爲3 + 5 * CPU_NUM / 8

另外,-XX:MaxGCPauseMillis用來設置STW最大時間,單位毫秒,-XX:GCTimeRatio用來設置垃圾收集時間佔總時間的比例,默認99,也就是GC時間佔比不超過百分之一。這兩個參數是此消彼長的關係

C:\Users\songzeceng>jinfo -flag MaxGCPauseMillis 16480
-XX:MaxGCPauseMillis=18446744073709551615


C:\Users\songzeceng>jinfo -flag GCTimeRatio 16480
-XX:GCTimeRatio=99

最後,-XX:+UseAdaptiveSizePolicy使能Parallel的自適應調節策略,自適應調節策略也用在了年輕代裏伊甸園區和倖存區的空間佔比設置上,也是這個參數

CMS回收器:低延遲

CMS(Concurrent Mask Sweep)收集器是HotSpot中第一款真正意義上的併發收集器,它首次實現了讓GC線程和用戶線程的同時工作

它採用標記-清除算法,自然也會STW

 

CMS不能和Parallel配合,只能和ParNew或Serial合作

 

其工作原理如下圖所示

在初始標記和重新標記環節,都出現了STW

 

初始標記階段:只標記出根結點能直接關聯的對象,此階段會進行STW。由於直接關聯的對象比較少,所以這一步速度非常快

併發標記階段:從根結點的直接關聯對象開始遍歷整個對象圖,此過程耗時較長,但不需要暫停用戶線程

重新標記階段:修正併發標記階段,因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,此過程比初始階段耗時長,但比並發標記時間短

併發清除階段:清理刪除掉標記階段被判斷已經死亡的對象,釋放內存空間。此過程不需要移動活對象,所以可以和用戶線程併發執行

 

由於最耗費時間的併發標記和併發清除階段都不用STW,所以整體回收是低停頓的。在CMS回收過程中,還要確保用戶線程有足夠的內存可用,所以當堆內存使用率達到某一閾值時,便開始進行回收。如果CMS運行期間內存無法滿足用戶需要,就會出現一個Concurrent Mode Failure,此時,JVM將臨時起用Serial Old來重新進行老年代收集,這樣停頓時間就長了。

 

由於CMS採用的是標記-清除算法,所以會產生內存碎片,爲新對象分配內存時只能使用空閒列表法來進行分配。下圖帶陰影的就是產生的不連續的內存碎片

 

 

爲何CMS不能使用標記-壓縮算法呢?因爲CMS清理垃圾線程和用戶線程是並行的,用戶線程不能停,那麼使用的對象地址就不能變,自然就不能爲了整理內存空間而移動對象地址,所以只能使用標記-清除算法

 

CMS優點:併發收集、低延遲

CMS缺點:會產生內存碎片;對CPU資源非常敏感(佔用了一部分線程,所以導致吞吐量降低);無法處理浮動垃圾(如果併發標記階段產生新的垃圾對象,CMS將無法對這些垃圾對象進行標記,從而不能將其回收)

 

可以使用-XX:+UseConcMarkSweepGC使能CMS,使能後,將自行打開-XX:+UseParNewGC。也就是ParNew+CMS+Serial Old的組合

使用-XX:CMSInitiatingOccupancyFraction設置堆內存使用閾值,達到此閾值,就開始回收。jdk6及以上版本默認值爲92%

可以使用-XX:ParallelCMSThreads(jdk8及以後叫做-XX:ConcGCThreads)設置CMS線程數量,CMS默認啓動的線程數是(ParallelGCThreads + 3)  / 4

-XX:+UseCMSCompactAtFullCollection用於指定在CMS的Full GC後進行內存整理,注意整理過程不能和用戶線程並行執行,所以帶來了比較長的停頓時間

-XX:CMSFullGCsBeforeCompaction用來設置在多少次執行Full GC後進行內存整理

 

在此,對Serial GC、Parallel GC、 CMS進行總結:

如果要最小化使用內存和並行開銷,使用Serial GC

如果要最大化吞吐量,使用Parallel GC

如果要最小化停頓時間,使用CMS

 

jdk9開始,廢棄了CMS,但還能用;jdk14直接刪除了CMS

G1回收器:區域化分代式

G1的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才擔當起全功能收集器的重任

G1把堆內存分割爲很多不相關的區域(Region)(物理上是不連續的),用這些區域來表示伊甸園區、倖存者區、老年代等。它有計劃地避免在整個堆中進行全區域的垃圾收集,跟蹤各個區域裏垃圾堆積的價值大小(回收所得空間大小,以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的區域。由於這種方式的側重點在於回收垃圾最大量的區間,所以G1的名字就是Garbage First

 

G1主要針對多核CPU以及大容量內存的機器,是jdk9以後的默認回收器,取代了CMS、Parallel + Parallel Old組合,被稱爲全功能的垃圾收集器

 

jdk8中還不是默認的,需要使用-XX:+UseG1GC來使能

分區算法的特點

G1的分區算法有以下幾個特點:

1)、並行與併發

並行性:G1回收期間,可以用多個GC線程同時工作,有效利用多核計算能力,此時用戶線程被暫停

併發性:G1有與應用程序交替執行的能力,部分工作可以和應用程序同時執行,因此在GC時不會完全阻塞應用程序

G1回收器可以採用應用線程承擔後臺運行的GC工作,也就是當GC線程處理速度慢時,系統會調用應用程序線程幫助加速GC過程

2)、分代收集

G1依舊會區分年輕代和老年代,年輕代依舊分爲伊甸園區和倖存者區。但從堆結構上看,它不要求整個伊甸園區、整個年輕代或老年代都是連續的,也不堅持固定大小和固定數量

它將堆空間分爲若干區域,這些區域中包含了邏輯上的老年代和年輕代,如下圖所示

所以,G1可以兼顧老年代和年輕代,而不像其他回收器那樣,要麼工作在年輕代,要麼工作在老年代

3)、空間整合

G1將內存劃分成一個個區域,回收以區域爲單位。區域之間採用複製算法,整體上可看成標記-壓縮算法。

4)、可預測的事件停頓模型(軟實時)

由於分區的原因,G1只選取部分區域進行回收,這樣就縮小的回收的範圍,因此對於全局停頓的情況也會得到較好的控制

G1跟蹤各個區域裏的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的區域。這樣保證了G1在有限的時間內可以獲取儘可能高的收集效率

G1未必能做到最好的情況下CMS的延時停頓,但在最差的情況下要好很多

 

G1回收器也有缺點,相比於CMS,它爲了GC產生的內存佔用還是程序運行時帶來的額外負載都比CMS要高。因此,在小內存應用上CMS的表現大概率會優於G1,而G1在大內存應用上則會發揮其優勢。平衡點在6-8GB之間

 

在下面的情況下,G1可能比CMS表現好:

1)、超過50%的堆內存被活動對象佔用

2)、對象分配頻率或年代晉升頻率變化很大

3)、GC停頓時間過長(0.5s~1s)

參數設置

G1回收器的參數設置:

1)、-XX:+UseG1GC 使能G1。使能後,可以使用以下參數進行進一步設置

2)、-XX:G1HeapRegionSize 設置每個區域大小,值應該是2的整數冪,範圍是1MB~32MB,默認值爲堆內存的1/2000

3)、-XX:MaxGCPauseMillis 設置最大GC停頓時間,默認200ms(JVM會盡力實現,但不保證達到)

4)、-XX:ParallelGCThreads 設置STW工作線程數值

5)、-XX:ConcGCThreads 設置併發標記的線程數,將值設置爲並行垃圾回收線程數(ParallelGCThreads)的1/4左右

6)、-XX:InitiatingHeapOccupancyPercent 設置觸發併發GC的java堆佔用率閾值,超過此值,就出發GC,默認45

C:\Users\songzeceng>jinfo -flag UseG1GC 16112
-XX:+UseG1GC


C:\Users\songzeceng>jinfo -flag ParallelGCThreads 16112
-XX:ParallelGCThreads=10


C:\Users\songzeceng>jinfo -flag MaxGCPauseMillis 16112
-XX:MaxGCPauseMillis=200


C:\Users\songzeceng>jinfo -flag G1HeapRegionSize 16112
-XX:G1HeapRegionSize=1048576

C:\Users\songzeceng>jinfo -flag ConcGCThreads 16112
-XX:ConcGCThreads=3


C:\Users\songzeceng>jinfo -flag InitiatingHeapOccupancyPercent 16112
-XX:InitiatingHeapOccupancyPercent=45

使用步驟:開啓G1回收器、設置堆的最大內存(-Xms和-Xmx)、設置最大停頓時間

G1有三種回收模式:Young GC、Mixed GC和FullGC,在不同的條件下被觸發

區域Region:化整爲零

將堆劃分成月2048個大小相同的獨立分區塊,每個區域大小根據堆空間的實際大小而定,整體被控制在1MB到32MB之間,且爲2的所有的整次冪,可用-XX:G1HeapRegionSize設定。區域大小相同,且在JVM生命週期內不會被改變。

此時,新生代和老年代不再是物理隔離的了,他們都是一部分不一定連續的區域的集合,通過區域的動態分配實現邏輯上的連續,如下圖所示

同一個區域在一個時刻只能承擔一個角色,但角色可以變換。當對象比較大(大於1.5個區域)時,就放到大對象區(Humongous,簡稱H),如果一個H區都放不下,就使用多個連續的H區進行存儲,如果還找不到,就進行Full GC。大對象區是專門爲短期存在的對象設置的,它在GC時被看作老年區的一部分

對於一個區域,使用指針碰撞來爲對象分配內存,也可以在頭部設置TLAB來實現對象的線程安全。關於TLAB,請參見文章JVM學習筆記之堆的TLAB部分

簡要回收過程

G1回收過程如下圖所示,分爲三部分:年輕代GC、老年代併發標記回收(也會出現年輕代GC)、混合回收(也會出現年輕代GC),如果必要,單線程、獨佔式、高強度的Full GC還是存在,這是針對GC的評估失敗提供的失敗保護機制,即強力回收

 

當年輕代的伊甸園區要用完時,就會觸發年輕代GC。年輕代回收是一個並行獨佔收集器,暫停所有應用線程,啓動多線程對年輕代進行GC。然後從年輕代區間移動存活對象到倖存者區或老年代,也可能是兩個區域都有涉及

當堆內存使用率達到閾值時,開始老年代併發標記過程

標記完成立刻開始混合回收過程。對於一個混合回收期,G1回收器從老年區移動存活對象到空閒區間,這些空閒區間也就成了老年區的一部分。G1的老年代回收器不需要回收整個老年代,一次只需要掃描/回收一小部分老年代的區域就行了。同時,老年代區域和年輕代是一起被回收的。

記憶集與寫屏障

每個區域都有一個對應的記憶集,作用是避免全局掃描。每次引用類型數據進行寫操作時,都會產生一個叫做寫屏障的暫時中斷,然後檢查將要寫入的引用指向的對象是否和該引用類型數據在不同的區域(其他收集器則是檢查老年代對象是否引用了新生代對象)。如果在不同的區域,就通過CardTable把相關的引用信息記錄到指向對象的所在區域對應的記憶集中,如下圖所示。在進行GC時,對每個根結點所在的區域,進行記憶集遍歷,就可以既不全局掃描,又不會有遺漏

圖中,區域2分別被區域1和3引用,那麼它的記憶集裏就會記錄下區域1和區域3中的引用位置,也就是哪個位置的引用指向了自己哪個位置的對象

詳細回收過程

1)、年輕代GC:當伊甸園空間耗盡時,G1會啓動一次年輕代GC。年輕代GC只會回收伊甸園區和倖存者區

YGC時,首先G1停止應用程序的執行,創建回收集,這是一個需要執行被回收的內存分代集合,包含需要回收的伊甸園區和倖存者區的內存分段。如下圖所示

如圖所示,先對伊甸園區和一個倖存者區使用複製算法,把活對象複製到空閒區域,此區域作爲第二個倖存者區,然後清空伊甸園區和原倖存者區

 

YGC具體過程如下:

1)、掃描根結點

2)、更新記憶集:處理dirty card queue中的card,更新記憶集。此階段更新後,記憶集可以準確反應老年代對新生代中對象的引用

dirty card queue(髒卡隊列)中保存的是對象引用信息,比如對於代碼object.field = obj,JVM就會把object對象和field對象的引用關係保存到髒卡隊列中。不在賦值時更新記憶集,是爲了保證性能。記憶集的處理是同步過程,使用隊列緩存可以提高性能

3)、處理記憶集:識別被老年代對象指向的伊甸園中的對象,這些被指向的伊甸園對象就是存活的對象

4)、複製對象:此階段中,對象樹被遍歷,伊甸園區中的存活對象會被複制到空閒的倖存者區內存分段中。如果倖存者區中存活對象的年齡沒達到閾值,年齡就+1;達到閾值就會被複制到老年代中空閒的內存分段。如果倖存者區空間不夠,伊甸園區的部分數據會直接晉升到老年代

5)、處理引用:對於軟引用、弱引用、虛引用、Final、JNI Weak等引用。最終伊甸園數據爲空,GC停止工作,而且目標內存中對象都是連續存儲的,沒有碎片,所以複製過程可以達到內存整理的效果,減少碎片。

 

併發標記過程如下:

1)、初始標記階段:標記從根結點直接可達的對象,STW

2)、根區域掃描:G1掃描倖存者區直接可達的老年區對象,並對之標記。此過程必須在YGC之前完成

3)、併發標記:在整個堆中進行併發標記,此過程可能被YGC中斷。併發標記時,若發現某個區域中所有對象都是垃圾,那這個區域將被立刻回收。同時,併發標記過程中,會計算每個區域的對象活性(存活對象的比例)

4)、再次標記:由於應用程序沒有在併發標記階段中停止,所以需要修正上一次標記的結果,並且是STW的。而且G1採用了比CMS中更快算法——初始快照算法

5)、獨佔清理:計算各個區域的存活對象和GC回收比例,並進行排序,識別可以混合回收的區域,STW。這個階段並不會實際去做垃圾收集

6)、併發清理:識別並清理完全空閒的區域

 

混合回收:當越來越多的對象晉升到老年代時,爲了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,也就是Mixed GC,也就是除了回收整個新生代,也會收集一部分老年代,注意這不是Full GC,如下圖所示。

具體過程如下:

併發標記結束後,老年代中百分百爲垃圾的內存分段被回收了,部分爲垃圾的內存分段被計算了出來。默認情況下,這些老年代內存段會分8詞回收(數量可以通過-XX:G1MixedGCCountTarget設置)

混合回收的回收集包括八分之一的老年代內存分段、伊甸園區內存分段和倖存者區內存分段,混合回收算法處理年輕代內存的方法和年輕代回收算法一模一樣

由於老年代中內存分段默認分8次回收,G1會優先回收垃圾多的內存分段。垃圾佔內存分段比例越高的越先被回收。並且有一個閾值(-XX:G1MixedGCLiveThresholdPercent,默認65%)會決定內存分段是否參與回收,垃圾佔內存分段比例要達到此閾值纔會被回收。

混合回收不一定要進行8次,有一個閾值-XX:G1HeapWastePercent(默認值10%),意思是允許整個堆內存中有10%的空間被浪費,如果可回收垃圾佔內存的比例不足這個閾值,就不會進行混合回收,因爲不值當

 

可選過程Full GC:當回收時沒有足夠的目的空間來存放晉升的對象或併發處理過程完成之前空間耗盡時,會發生串行獨佔的Full GC

 

補充:回收階段其實也想過設計成和用戶線程併發執行,但此需求優先級比較低,這個特性被放到後來的低延遲垃圾收集器ZGC中來實現

優化建議

1)、年輕代大小:避免使用-Xmn或-XX:NewRatio來顯示設置年輕代大小,因爲這樣會覆蓋暫停時間的目標

2)、暫停時間目標不能太苛刻:G1的吞吐量目標是90%的應用程序時間和10%的垃圾回收時間。暫停時間太短,表示要承受更多的垃圾回收開銷,從而直接影響吞吐量

垃圾回收器總結

截至jdk8,七款經典回收器的對比總結如下表所示

 

現在互聯網項目,基本都用G1

 

沒有最好的收集器,更沒有萬能的收集;

調用永遠是具體問題具體分析,不存在一勞永逸的收集器

GC日誌分析

打印GC詳細日誌使能:-XX:+PrintGCDetails

打印GC日誌使能:-XXPrintGC

打印GC時間戳(以基準時間的形式):-XX:+PrintGCTimeStamps

打印GC日期:-XX:+PrintGCDateStamps

打印GC前後的堆信息:-XX:+PrintHeapAtGC

日誌文件的輸出:-Xloggc:../logs/gc.log

 

某次年輕代GC日誌如下

[GC (Allocation Failure) [PSYoungGen: 65019K->10732K(75776K)] 65019K->61402K(249344K), 0.0077635 secs] [Times: user=0.08 sys=0.05, real=0.01 secs]

Allocation Failure表示GC發生原因是年輕代內存空間不夠存儲新數據了

PSYoungGen表示回收器,這是Parallel的年輕代回收器

65019K->10732K(75776K)表示新生代佔用內存從65019K減少到了10732K,目前總共有75776K,裏面的10732K想必就是倖存者區佔用的空間了

後面的65019K->61402K(249344K)表示原來的堆空間佔用了65019K,GC後變成了61402K,目前堆空間總共249344K。GC後堆佔用空間(61402K)比倖存者區(10732K)大,說明有些數據直接進入了老年代

[Times: user=0.08 sys=0.05, real=0.01 secs]中的user表示用戶態回收耗時,sys表示內核態回收耗時,real表示實際耗時。由於多核原因,用戶態+內核態回收總耗時可能大於實際耗時,因爲涉及狀態的切換

 

某次Full GC日誌如下

[Full GC (Ergonomics) [PSYoungGen: 860971K->461160K(915968K)] [ParOldGen: 2771781K->2771697K(2771968K)] 3632753K->3232858K(3687936K), [Metaspace: 3767K->3767K(1056768K)], 0.1855559 secs] [Times: user=1.56 sys=0.00, real=0.19 secs]

ParOldGen表示老年代的回收器是Parallel Old,2771781K->2771697K(2771968K)表示GC前後老年區佔用內存大小和老年區總大小

[Metaspace: 3767K->3767K(1056768K)表示元空間佔用大小在GC前後的值,以及元空間總大小

 

加入-XX:+PrintGCDateStamps後的輸出如下

2020-05-20T21:15:38.873+0800: [GC (Allocation Failure) [PSYoungGen: 2046K->512K(2560K)] 2046K->886K(9728K), 0.0010088 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-05-20T21:15:38.883+0800: [GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 2934K->974K(9728K), 0.0013327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-05-20T21:15:38.889+0800: [GC (Allocation Failure) [PSYoungGen: 2544K->496K(2560K)] 3022K->1030K(9728K), 0.0014997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

可見輸出了日期2020-05-20T21:15:38.873+0800

 

再加入-XX:+PrintGCTimeStamps後的輸出如下

2020-05-20T21:17:11.029+0800: 0.203: [GC (Allocation Failure) [PSYoungGen: 2046K->504K(2560K)] 2046K->893K(9728K), 0.0011316 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-05-20T21:17:11.041+0800: 0.214: [GC (Allocation Failure) [PSYoungGen: 2552K->488K(2560K)] 2941K->965K(9728K), 0.0015585 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2020-05-20T21:17:11.054+0800: 0.227: [GC (Allocation Failure) [PSYoungGen: 2536K->504K(2560K)] 3013K->1021K(9728K), 0.0012158 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

可見又續上了GC時間戳0.203,表示JVM啓動之後0.203s發生了GC

 

再加入-XX:+PrintHeapAtGC後的輸出更加複雜,因爲這會輸出GC前後的堆信息

{Heap before GC invocations=1 (full 0):
PSYoungGen      total 2560K, used 2046K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 99% used [0x00000000ffd00000,0x00000000ffeffac8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace       used 2978K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 321K, capacity 386K, committed 512K, reserved 1048576K
2020-05-20T21:20:13.237+0800: 0.202: [GC (Allocation Failure) [PSYoungGen: 2046K->504K(2560K)] 2046K->863K(9728K), 0.0009002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=1 (full 0):
PSYoungGen      total 2560K, used 504K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000fff00000)
  from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen       total 7168K, used 359K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 5% used [0x00000000ff600000,0x00000000ff659ed8,0x00000000ffd00000)
Metaspace       used 2978K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 321K, capacity 386K, committed 512K, reserved 1048576K
}

加入輸出路徑-Xloggc:C:/Users/songzeceng/Desktop/gc.log後,可以在桌面生成gc.log文件,內容如下

Java HotSpot(TM) 64-Bit Server VM (25.231-b11) for windows-amd64 JRE (1.8.0_231-b11), built on Oct  5 2019 03:11:30 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 16624412k(8666512k free), swap 19770140k(7853452k free)
CommandLine flags: -XX:InitialHeapSize=10485760 -XX:MaxHeapSize=10485760 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintStringTableStatistics -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
{Heap before GC invocations=1 (full 0):
PSYoungGen      total 2560K, used 2046K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 99% used [0x00000000ffd00000,0x00000000ffeffac8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen       total 7168K, used 0K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 0% used [0x00000000ff600000,0x00000000ff600000,0x00000000ffd00000)
Metaspace       used 2957K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 320K, capacity 386K, committed 512K, reserved 1048576K
2020-05-20T21:46:52.197+0800: 0.217: [GC (Allocation Failure) [PSYoungGen: 2046K->512K(2560K)] 2046K->881K(9728K), 0.0009109 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=1 (full 0):
PSYoungGen      total 2560K, used 512K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000fff00000)
  from space 512K, 100% used [0x00000000fff00000,0x00000000fff80000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen       total 7168K, used 369K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 5% used [0x00000000ff600000,0x00000000ff65c448,0x00000000ffd00000)
Metaspace       used 2957K, capacity 4494K, committed 4864K, reserved 1056768K
  class space    used 320K, capacity 386K, committed 512K, reserved 1048576K
}
{Heap before GC invocations=2 (full 0):
PSYoungGen      total 2560K, used 2560K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 100% used [0x00000000ffd00000,0x00000000fff00000,0x00000000fff00000)
  from space 512K, 100% used [0x00000000fff00000,0x00000000fff80000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen       total 7168K, used 369K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 5% used [0x00000000ff600000,0x00000000ff65c448,0x00000000ffd00000)
Metaspace       used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K
2020-05-20T21:46:52.206+0800: 0.226: [GC (Allocation Failure) [PSYoungGen: 2560K->496K(2560K)] 2929K->977K(9728K), 0.0013469 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
Heap after GC invocations=2 (full 0):
PSYoungGen      total 2560K, used 496K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000fff00000)
  from space 512K, 96% used [0x00000000fff80000,0x00000000ffffc010,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen       total 7168K, used 481K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 6% used [0x00000000ff600000,0x00000000ff678448,0x00000000ffd00000)
Metaspace       used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K
}
{Heap before GC invocations=3 (full 0):
PSYoungGen      total 2560K, used 2544K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 100% used [0x00000000ffd00000,0x00000000fff00000,0x00000000fff00000)
  from space 512K, 96% used [0x00000000fff80000,0x00000000ffffc010,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
ParOldGen       total 7168K, used 481K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 6% used [0x00000000ff600000,0x00000000ff678448,0x00000000ffd00000)
Metaspace       used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K
2020-05-20T21:46:52.213+0800: 0.233: [GC (Allocation Failure) [PSYoungGen: 2544K->512K(2560K)] 3025K->1041K(9728K), 0.0015696 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap after GC invocations=3 (full 0):
PSYoungGen      total 2560K, used 512K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000fff00000)
  from space 512K, 100% used [0x00000000fff00000,0x00000000fff80000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen       total 7168K, used 529K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6847e0,0x00000000ffd00000)
Metaspace       used 3235K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 349K, capacity 388K, committed 512K, reserved 1048576K
}
Heap
PSYoungGen      total 2560K, used 2156K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 80% used [0x00000000ffd00000,0x00000000ffe9b0e0,0x00000000fff00000)
  from space 512K, 100% used [0x00000000fff00000,0x00000000fff80000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen       total 7168K, used 529K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 7% used [0x00000000ff600000,0x00000000ff6847e0,0x00000000ffd00000)
Metaspace       used 3242K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K

垃圾回收器的新發展

目前G1也在不斷地改進;而隨着Serverless等雲計算新的應用場景下,Serial GC找到了新的舞臺;而CMS在jdk9中被標記爲廢棄,在jdk14中被徹底移除

EpsilonGC

jdk11中,出現了Epsilon收集器和ZGC收集器

Epsilon收集器是一個No-op收集器,只負責內存的分配,不負責內存的回收,適合內存分配完直接程序結束的場景

Shenadoah GC

jdk12出現了Shenadoah GC(約翰丹佛的《Country Roads take me home》一歌中有提及這條Shenandoah河——雪蘭多河),這個GC是紅帽公司提出來的,旨在針對JVM上的GC實現低停頓的需求(但系統吞吐量會下降),但被甲骨文公司排除在OpenJdk12之外

ZGC

ZGC算是一個革命性的新回收器,旨在儘可能對吞吐量影響不大的前提下,實現任意堆大小下都能把GC的停頓時間限制在10毫秒以內的低延遲。它使用了基於Region區域內存佈局、暫時不設分代的,使用了染色指針、讀屏障和內存多重映射等技術來實現可併發的標記-壓縮算法的,工作過程可分爲四個階段:併發標記——併發預備重分配——併發重分配——併發重映射

ZGC除了在初始標記階段是STW的,其餘所有地方都是併發執行,所以耗時非常短

 

jdk14開始,mac和windows上都能用ZGC了,添加參數:-XX:+UnlockExperimentalVMOpions -XX:+UseZGC

其他GC

阿里的JVM團隊基於G1算法,面向大堆應用場景,研發了AliGC

結語

JVM學習的筆記到這兒就整理完了,測試代碼都是在jdk8的環境下運行的

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