2.垃圾收集與內(nèi)存分配策略

1.哪些內(nèi)存需要回收

程序計(jì)數(shù)器,虛擬機(jī)棧,本地方法棧隨線程而生隨線程而滅。棧中棧幀隨著方法的調(diào)用與執(zhí)行完畢而入棧與出棧,每個(gè)棧幀分配的內(nèi)存基本是類結(jié)構(gòu)確定下來就已知的(盡管在運(yùn)行期會(huì)由即時(shí)編譯器進(jìn)行一些優(yōu)化,但在基于概念模型的討論中,大體可以認(rèn)為編譯其是可知的),因此這幾個(gè)區(qū)域的內(nèi)存分配和回收具備確定性。堆與方法區(qū)則不能確定接口類的內(nèi)存,只有運(yùn)行期間才會(huì)知道創(chuàng)建了哪些對象并動(dòng)態(tài)分配內(nèi)存。垃圾回收器關(guān)注的也就是這部分內(nèi)存。

2.垃圾對象的判定

(1) 引用計(jì)數(shù)法

給對象中添加一個(gè)引用計(jì)數(shù)器,有引用則+1,引用失效-1,任何時(shí)刻計(jì)數(shù)器為0該對象不可用。缺陷:兩個(gè)對象互相循環(huán)引用沒法判斷如objA.instance = objB。objB.instance = objA。

引用計(jì)數(shù)法缺陷demo:

/**
 * 引用計(jì)數(shù)算法的缺陷
 * testGC方法執(zhí)行后,objA和objB會(huì)不會(huì)被GC呢
 * jvm args: -XX:+PrintGCDetails
 *
 * 會(huì)GC,虛擬機(jī)不是通過引用計(jì)數(shù)算法來判斷對象存活的
 */
public class ReferenceCountuinGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**
     * 該成員屬性的唯一意義就是占點(diǎn)內(nèi)存,以便能在GC日志中看清楚是否被回收過
     */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void testGC() {
        ReferenceCountuinGC objA = new ReferenceCountuinGC();
        ReferenceCountuinGC objB = new ReferenceCountuinGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        //假設(shè)在這里發(fā)生GC,objA和objB能回收嗎
        System.gc();
    }

    public static void main(String[] args) {
        testGC();
    }
}

結(jié)果:GC日志顯示虛擬機(jī)并沒有因?yàn)閮蓚€(gè)對象互相引用就不回收他們,這也從側(cè)面證明了虛擬機(jī)并不是通過引用計(jì)數(shù)法來判斷對象是否存活的。

[GC (System.gc()) [PSYoungGen: 7433K->712K(38400K)] 7433K->720K(125952K), 0.0860753 secs] [Times: user=0.14 sys=0.00, real=0.09 secs] 
[Full GC (System.gc()) [PSYoungGen: 712K->0K(38400K)] [ParOldGen: 8K->654K(87552K)] 720K->654K(125952K), [Metaspace: 3475K->3475K(1056768K)], 0.0049854 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 

附 GC日志分析

l GC和Full GC說明了這次垃圾收集的停頓類型。如果有“Full”,說明這次GC發(fā)生了STW(Stop-The-World),因?yàn)槭钦{(diào)用了System.gc()方法觸發(fā)的收集,所以會(huì)顯示”[Full GC
(System.gc())”。
l PSYoungGen是采用Parallel
Scavenge收集器的年輕代,ParOldGen是采用Parallel
Old收集器的老年代,Tenured是采用Serial Old收集器的老年代。
l [PSYoungGen:
7433K->712K(38400K)] 表示GC前該內(nèi)存區(qū)域已使用的容量7433K->GC后該內(nèi)存區(qū)域已使用的容量712K(該內(nèi)存區(qū)域總?cè)萘?8400K)
l 7433K->720K(125952K)表示GC前Java堆已使用容量7433K->GC后Java堆已使用容量720K(Java堆總?cè)萘?25952K)”
l 0.0860753secs 代表該內(nèi)存區(qū)域GC所占用的時(shí)間,單位是秒 secs

查看垃圾收集器
java -XX:+PrintCommandLineFlags –version 查看默認(rèn)垃圾回收器命令

附日志屬性解析及垃圾收集器參數(shù)傳送門
日志屬性分析:https://blog.csdn.net/qiaqia609/article/details/50912683
垃圾收集器參數(shù)參照表:https://blog.csdn.net/MakeContral/article/details/79119050

(2) 可達(dá)性分析法

通過一系列的稱為“GC Roots“的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個(gè)對象到 GC Roots 沒有任何引用鏈相連時(shí),就證明此對象是不可用的。



Java中可作為 GC Roots 的對象包括下面幾種:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象,如各個(gè)線程被調(diào)用的方法堆棧中使用的參數(shù),局部變量,臨時(shí)變量等。
  • 方法區(qū)中的類靜態(tài)屬性引用的對象,如java類的引用類型靜態(tài)變量
  • 方法區(qū)中的常量引用的對象,如字符串常量池里的引用。
  • 本地方法棧中 JNI(Native 方法)的引用對象。
  • java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對應(yīng)的Class對象,一些常駐的異常對象(如NullPointException,OutOfMemoryError)等,還有系統(tǒng)類加載器
  • 所有被同步鎖(Synchronized)持有的對象
  • 反應(yīng)java虛擬機(jī)內(nèi)部情況的JMXBean、JMTI中注冊的回調(diào)、本地代碼緩存等

(3) 對象引用

無論使用引用計(jì)數(shù)算法判斷對象的引用數(shù)量,還是使用可達(dá)性分析算法判斷對象的引用鏈?zhǔn)欠窨蛇_(dá),判斷對象是否存活都與“引用”有關(guān)。jdk1.2后對象引用與垃圾會(huì)后的關(guān)聯(lián)如下:

  • 強(qiáng)引用:永遠(yuǎn)不會(huì)回收。
    如“Object obj = new Object()”,這類引用是 Java 程序中最普遍的。只要強(qiáng)引用還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對象。
  • 軟引用:內(nèi)存溢出之前進(jìn)行回收。
    用來描述一些可能還有用,但并非必須的對象。在系統(tǒng)內(nèi)存不夠用時(shí),這類引用關(guān)聯(lián)的對象將被垃圾收集器二次回收,回收后內(nèi)存還是不足,才會(huì)拋出內(nèi)存溢出。場景:如上一個(gè)頁面的鏈接緩存這種。SoftReference 類實(shí)現(xiàn)軟引用SoftReference ref=new SoftReference(new User());
  • 弱引用:被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾回收之前。
    用來描述非必須對象的,但它的強(qiáng)度比軟引用更弱些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對象。WeakReference 類實(shí)現(xiàn)弱引用。WeakReference sr = new WeakReference(new User());
  • 虛引用:任何時(shí)候都可能被垃圾回收器回收。
    最弱的一種引用關(guān)系,完全不會(huì)對其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對象實(shí)例。為一個(gè)對象設(shè)置虛引用關(guān)聯(lián)的唯一目的是希望能在這個(gè)對象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。PhantomReference 類來實(shí)現(xiàn)虛引用。
    PhantomReference pr = new PhantomReference(new User(), new ReferenceQueue());

附對象引用描述傳送門
四大引用的具體描述:https://www.cnblogs.com/JamesWang1993/p/9347218.html

(4) 對象的自我救贖(了解)

即使在可達(dá)性分析算法中不可達(dá)的對象,可不是“非死不可“的,此時(shí)他們處于“緩刑”
階段。
要真正宣告一個(gè)對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對象在進(jìn)行根搜索后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選(篩選的條件是此對象是否有必要執(zhí)行 finalize()方法)。當(dāng)對象沒有覆蓋 finalize()方法,或 finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為沒有必要執(zhí)行。如果該對象被判定為有必要執(zhí)行 finalize()方法,那么這個(gè)對象將會(huì)被放置在一個(gè)名為 F-Queue 隊(duì)列中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級的 Finalizer 線程去執(zhí)行 finalize()方法。finalize()方法是對象逃脫死亡命運(yùn)的最后一次機(jī)會(huì)(因?yàn)橐粋€(gè)對象的 finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次),稍后 GC 將對 F-Queue 中的對象進(jìn)行第二次小規(guī)模的標(biāo)記,如果要在 finalize()方法中成功拯救自己,只要在 finalize()方法中讓該對象重新引用鏈上的任何一個(gè)對象建立關(guān)聯(lián)即可。而如果對象這時(shí)還沒有關(guān)聯(lián)到任何鏈上的引用,那它就會(huì)被回收掉。

finalize自我救贖demo:

/**
 * finalize 對象自我拯救
 * 1對象在被GC時(shí)自我拯救
 * 2.這種自救的機(jī)會(huì)只有一次,因?yàn)橐粋€(gè)對象的 finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次
 * @author wb-szy546826
 * @date 2019-6-3 14:34:04
 */
public class FinalizzeEscapeGC {

    public static FinalizzeEscapeGC SAVE_HOOK = null;

    public void isAlive(){
        System.out.println("yes, i am still alive");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method excuted!");
        FinalizzeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizzeEscapeGC();

        //對象第一次成功救贖自己
        SAVE_HOOK = null;
        System.gc();
        //因?yàn)閒inalize方法優(yōu)先級很低,暫停0.5秒等待
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no, i am dead");
        }

        //下面這段代碼與上面完全相同,但是自我自救卻失敗了
        SAVE_HOOK = null;
        System.gc();
        //因?yàn)閒inalize方法優(yōu)先級很低,暫停0.5秒等待
        Thread.sleep(500);
        if (SAVE_HOOK != null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no, i am dead");
        }
    }
}

結(jié)果:

finalize method excuted!
yes, i am still alive
no, i am dead

finalize 對象自我拯救
(1)對象在被GC時(shí)自我拯救
(2)自救的機(jī)會(huì)只有一次,因?yàn)橐粋€(gè)對象的 finalize()方法最多只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次
(3)不建議使用finalize方法

(5) 方法區(qū)回收

方法區(qū)回收內(nèi)容:廢棄的常量,不再使用的類型(無用的類)。
類需要同時(shí)滿足下面3個(gè)條件才能算是 “無用的類” :

1.該類所有的實(shí)例都已經(jīng)被回收,也就是 Java 堆中不存在該類的任何實(shí)例。
2.加載該類的 ClassLoader 已經(jīng)被回收。
3.該類對應(yīng)的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

3.垃圾回收算法

分代收集理論
弱分代假說:絕大多數(shù)對象都是朝生夕滅的
強(qiáng)分代假說:熬過多次垃圾收集過程的對象就越難消亡
這兩個(gè)假說奠定了常用垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將java堆劃分出不同的區(qū)域,然后將回收對象依據(jù)其年齡分配到不同的區(qū)域存儲(chǔ)(新生代、老年代)

GC名詞定義-深入理解java虛擬機(jī)第三版

  • 部分收集(Partial GC):目標(biāo)不是完整收集整個(gè)java堆的垃圾收集
    1.新生代(Minor GC/Young GC):目標(biāo)只是新生代的垃圾收集
    2.老年代(Major GC/Old GC):目標(biāo)只是老年代的垃圾收集,目前只有CMS收集器會(huì)有單獨(dú)收集老年代的行為。
    3.混合(Mixed GC):目標(biāo)是收集整個(gè)新生代以及部分老年代的垃圾收集,目前只有G1收集器有這樣的行為。
  • 整堆收集(FullGC):收集整個(gè)java堆和方法區(qū)的垃圾收集。

標(biāo)記-清除算法-老年代

首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對象,也可以反過來,標(biāo)記存活的對象,統(tǒng)一回收所有未被標(biāo)記的對象。
缺點(diǎn)有兩個(gè):第一個(gè)是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過 程的執(zhí)行效率都隨對象數(shù)量增長而降低;第二個(gè)是內(nèi)存空間的碎片化問題,標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過程中需要分配較大對象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

復(fù)制算法-新生代

將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。如果內(nèi)存中多數(shù)對象都是存活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開銷,但對于多數(shù)對象都是可回收的情況,算法需要復(fù)制的就是占少數(shù)的存活對象,而且每次都是針對整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,分配內(nèi)存時(shí)也就不用考慮有空間碎片的復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配即可。
缺點(diǎn):將可用內(nèi)存縮小為了原來的一半

當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對象時(shí),就需要依賴其他內(nèi)存區(qū)域(實(shí) 際上大多就是老年代)進(jìn)行分配擔(dān)保(Handle Promotion)


標(biāo)記-整理算法-老年代

標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
缺點(diǎn):移動(dòng)存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動(dòng)存活對象并更新所有引用這些對象的地方將會(huì)是一種極為負(fù)重的操作,而且這種對象移動(dòng)操作必須全程暫停用戶應(yīng)用程序才能進(jìn)行。

標(biāo)記清除與標(biāo)記整理對比

移動(dòng)存活對象,尤其是在老年代這種每次回收都有大量對象存活區(qū)域,移動(dòng)存活對象并更新所有引用這些對象的地方將會(huì)是一種極為負(fù)重的操作,而且這種移動(dòng)操作必須全程暫停用戶應(yīng)用程序才能進(jìn)行(STW)
不移動(dòng)和整理存活對象,彌漫于堆中的存貨對象導(dǎo)致的空間碎片化問題就只能依賴更為復(fù)雜的內(nèi)存分配器和內(nèi)存訪問器來解決。
從對象分配空間方式來看,移動(dòng)對象(規(guī)整堆內(nèi)存),使用指針碰撞(簡單位移指針即可分配內(nèi)存);不移動(dòng)對象(不規(guī)整堆內(nèi)存),使用空閑列表(使用過的與空閑的混在一起,需要維護(hù)一個(gè)列表記錄)。詳見上一章
從垃圾收集停頓時(shí)間來看,不移動(dòng)對象停頓時(shí)間更短-側(cè)面驗(yàn)證CMS基于標(biāo)記-清除注重低停頓,低延遲。
從整個(gè)程序吞吐量來看,移動(dòng)對象更劃算-側(cè)面驗(yàn)證Parallel Scavenge基于標(biāo)記-整理注重吞吐量。

4.垃圾收集器

下面一張圖是HotSpot虛擬機(jī)包含的所有收集器,如果兩個(gè)收集器之間有連線,說明可以搭配使用。

Serial 收集器(復(fù)制算法 -XX:+UseSerialGC)

新生代單線程收集器,標(biāo)記和清理都是單線程,在進(jìn)行垃圾收集時(shí),必須要暫停其他所有的工作線程,直到它收集結(jié)束。優(yōu)點(diǎn)是簡單高效,由于是單線程,此處可以獲取最高的單線程收集效率,但是其中STW停頓時(shí)間長。


Serial Old收集器(標(biāo)記-整理算法 -XX:+UseSerialOldGC)

老年代的單線程收集器,使用標(biāo)記 - 整理算法,運(yùn)行過程同Serial收集器。

ParNew收集器(復(fù)制算法 -XX:+UseParNewGC)

ParNew是Serial的多線程版本,除了使用多線程進(jìn)行垃圾收集外,其他行為與Serial完全一樣,默認(rèn)收集線程數(shù)與cpu核數(shù)相同。除了Serial收集器外,只有ParNew能與CMS收集器配合工作,開啟CMS后默認(rèn)該新生代收集器。
JDK9取消ParNew+Serial Old組合以及Serial+CMS組合,取消-XX:+UseParNewGC參數(shù),這意味著ParNew和CMS只能搭配使用,沒有其他收集器能夠與他們配合了。也可以認(rèn)為ParNew合并到了CMS,成為其處理新生代的組成部分。

Parallel Scavenge收集器(復(fù)制算法 -XX:+UseParallelGC)

Server 模式(內(nèi)存大于2G,2個(gè)cpu)下的 默認(rèn)收集器(Parallel Scavenge+Parallel Old)。
并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 用戶線程時(shí)間/(用戶線程時(shí)間+GC線程時(shí)間),高吞吐量可以高效率地利用CPU時(shí)間,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù),并且虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量,這種調(diào)節(jié)方式稱為GC自適應(yīng)調(diào)節(jié)策略。


Parallel Old收集器(標(biāo)記-整理算法 XX:+UseParallelOldGC)

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量優(yōu)先。

并行:多條垃圾收集線程并行工作,但是此時(shí)用戶線程仍然處理等待狀態(tài)。
并發(fā):用戶線程與垃圾收集線程同時(shí)執(zhí)行(不一定是并行的,可能交替執(zhí)行),用戶程序在繼續(xù)執(zhí)行,而垃圾收程序運(yùn)行在另一個(gè)CPU上

CMS(Concurrent Mark Sweep)收集器(標(biāo)記-清除算法)

以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。特點(diǎn)是高并發(fā)、低停頓,追求最短GC回收停頓時(shí)間,cpu占用比較高,響應(yīng)時(shí)間快,停頓時(shí)間短。

過程:
初始標(biāo)記(initial mark,STW): 暫停所有的其他線程,僅僅標(biāo)記下gc roots直接能關(guān)聯(lián)到的對象,速度很快 ;
并發(fā)標(biāo)記(concurrent mark): 從GC Roots的直接聯(lián)系對象開始遍歷整個(gè)對象圖的過程,這個(gè)過程耗時(shí)較長,但是不需要停頓用戶線程,可以與垃圾收集線程一起并發(fā)進(jìn)行。
重新標(biāo)記(remark, STW): 重新標(biāo)記階段就是為了修正并發(fā)標(biāo)記期間,因?yàn)橛脩舫绦蚶^續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段的時(shí)間稍長,遠(yuǎn)遠(yuǎn)比并發(fā)標(biāo)記階段時(shí)間短
并發(fā)清理(concurrent sweep): 清理刪除掉標(biāo)記階段判斷的已經(jīng)死亡的對象,由于不需要移動(dòng)存活對象,所以這個(gè)階段也是可以與用戶線程同時(shí)并發(fā)的。


缺點(diǎn)
1.對CPU資源敏感,可能導(dǎo)致應(yīng)用程序變慢;雖然不會(huì)導(dǎo)致用戶線程停頓,但卻會(huì)占用一部分線程導(dǎo)致應(yīng)用程序變慢,降低總吞吐量。
2.無法處理浮動(dòng)垃圾,因?yàn)樵诓l(fā)清理階段用戶線程還在運(yùn)行,自然就會(huì)產(chǎn)生新的垃圾,而在此次收集中無法收集他們,只能留到下次收集,這部分垃圾為浮動(dòng)垃圾,同時(shí),由于用戶線程并發(fā)執(zhí)行,所以需要預(yù)留一部分老年代空間提供并發(fā)收集時(shí)程序運(yùn)行使用。要是CMS預(yù)留的內(nèi)存無法滿足程序分配新對象的需要,會(huì)出現(xiàn)一次“并發(fā)失敗”(Concurrent Mode Failure),這時(shí)候虛擬機(jī)將啟動(dòng)后被預(yù)案,凍結(jié)用戶線程,臨時(shí)啟動(dòng)Serial Old收集器老重新進(jìn)行老年代的垃圾收集,這樣停頓時(shí)間就很長了。(-XX:CMSInitiatingOccupancyFraction設(shè)置預(yù)留空間占比,默認(rèn)92%)
3.由于采用的標(biāo)記 - 清除算法,會(huì)產(chǎn)生大量的內(nèi)存碎片,當(dāng)無法找到足夠大的連續(xù)空間來分配當(dāng)前對象,可能會(huì)提前觸發(fā)一次Full GC。當(dāng)JVM不得不進(jìn)行FULL GC時(shí),JVM會(huì)提供參數(shù)開啟碎片合并整理(-XX:+UseCMSCompactAtFullCollection),內(nèi)存整理必須移動(dòng)存活對象,肯定是無法并發(fā)的,停頓時(shí)間自然會(huì)增加。jvm又提供了另一個(gè)參數(shù)解決上邊出現(xiàn)的停頓時(shí)間增加的問題(-XX:+CMSFullGCsBeforeCompaction),該參數(shù)要求CMS收集器執(zhí)行FullGC提前進(jìn)行碎片整理(默認(rèn)0,即每次進(jìn)入FullGC都進(jìn)行碎片整理),以上兩個(gè)參數(shù)Jdk9都已廢棄。

參數(shù)信息

  1. -XX:+UseConcMarkSweepGC:啟用cms
  2. -XX:ConcGCThreads:并發(fā)的GC線程數(shù)
  3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做內(nèi)存碎片整理
  4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后做內(nèi)存碎片整理,默認(rèn)是0,代表每次 FullGC后都會(huì)整理一次碎片
  5. -XX:CMSInitiatingOccupancyFraction: 當(dāng)老年代使用達(dá)到該比例時(shí)會(huì)觸發(fā)FullGC(默認(rèn)68%)
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用設(shè)定的回收閾值(XX:CMSInitiatingOccupancyFraction設(shè)定的值),如果不指定,JVM僅在第一次使用設(shè)定值,后續(xù)則會(huì)自動(dòng)調(diào)整
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前啟動(dòng)一次minor gc,目的在于減少老年代對年輕代的引用,降低CMS GC的標(biāo)記階段時(shí)的開銷,一般CMS的GC耗時(shí) 80%都在 remark階段

Garbage First(G1)收集器(分區(qū)/標(biāo)記-整理算法)

G1收集器是垃圾收集器技術(shù)發(fā)展歷史上的里程碑式的成果,開創(chuàng)了收集器面向局部收集的設(shè)計(jì)思路和基于Region的內(nèi)存布局形式。
jdk9發(fā)布之日,G1宣告取代Parallel Scavenge+Parallel Old組合,成為服務(wù)端模式下默認(rèn)垃圾收集器,而CMS則淪落至不推薦使用收集器(Deprecate)。

面向局部的設(shè)計(jì)思想

“停頓時(shí)間模型”收集器:指能夠支持在一個(gè)長度為M毫秒內(nèi)的時(shí)間片段內(nèi),消耗在垃圾收集器上的時(shí)間大概率不超過N毫秒這樣的目標(biāo)。
G1之前的垃圾收集器,目標(biāo)范圍要么是整個(gè)新生代,要么是整個(gè)老年代,要么是整個(gè)java堆。G1則跳出該思想,它可以面向堆內(nèi)存任何部分來組成回收集進(jìn)行回收,衡量標(biāo)準(zhǔn)不再是它屬于哪個(gè)分代,而是那塊內(nèi)存存放的垃圾數(shù)量最多,回收受益最大,這就是G1收集器的Mixed GC模式,實(shí)現(xiàn)上邊的停頓時(shí)間模型目標(biāo)。

基于Region的內(nèi)存布局形式

G1也仍是遵循分代收集理論設(shè)計(jì)的,但是其堆內(nèi)存的布局與其他收集器有明顯差異,G1不再堅(jiān)持固定大小以及固定數(shù)量的分代區(qū)域劃分,而是把java堆華為多個(gè)大小相等的獨(dú)立區(qū)域(Region),每個(gè)Region都可以根據(jù)需要,扮演新生代的Eden,Survivor或者老年代空間(不再固定,不需要連續(xù))。收集器對扮演不同角色的Region采用不同的策略處理。
Region還有一群特殊的Humongous區(qū)域,專門存儲(chǔ)大對象。G1認(rèn)為只要大小超過一個(gè)Region容量一般的對象,即可判定為大對象。每個(gè)Region大小通過參數(shù)-XX:G1HeapRegionSize設(shè)定,取值范圍1MB~32MB,且應(yīng)為2的N次冪。對于超哥整個(gè)Region的超級大對象,將會(huì)被存放在N個(gè)連續(xù)的Humongous Region中,G1的大多數(shù)行為都把Humongous Region作為老年代的一部分進(jìn)行看待。

G1收集器Region分區(qū)示意圖

具有優(yōu)先級的區(qū)域回收方式,建立可預(yù)測的停頓時(shí)間模型

  • G1之所以能建立可預(yù)測的停頓時(shí)間模型,是因?yàn)樗鼘egion作為單次回收的最小單元(化整為零),即每次收集到的內(nèi)存空間都是Region大小的整數(shù)倍,這樣可以有計(jì)劃的避免在整個(gè)java堆中進(jìn)行全區(qū)域的垃圾收集。
  • 具體思路就是讓G1去跟蹤每個(gè)Region里面垃圾堆積的價(jià)值大小(回收所獲得的的空間大小以及回收所需要時(shí)間的經(jīng)驗(yàn)值),然后再后臺(tái)維護(hù)一個(gè)優(yōu)先級列表,每次根據(jù)用戶設(shè)定允許的收集停頓時(shí)間(-XX:MaxGCPauseMillis,默認(rèn)200ms),優(yōu)先處理回收價(jià)值is后以最大的Region,這就是“Garbage First”名字的由來。

難點(diǎn)

  • 1.跨Region引用對象
    記憶集RSet避免全堆作為GC Roots掃描。
    存在于新生代引用老年代對象,老年代引用老年代對象。
    如某個(gè)新生代對象存在跨代引用,由于老年代對象難以消亡,該引用會(huì)使得新生代對象在收集時(shí)同樣得以存活。我們就不應(yīng)該為了少量的跨代引用去掃描整個(gè)老年代,只需在新生代建立一個(gè)記憶集RSet,RSet標(biāo)識(shí)出老年代的哪一塊內(nèi)存會(huì)存在跨代引用。此后當(dāng)發(fā)生MinorGC時(shí),只有包含了跨代引用的小塊內(nèi)存里的對象才會(huì)被加入到GC Roots進(jìn)行掃描。
  • 2.并發(fā)標(biāo)記階段如何保證收集線程與用戶線程互不干擾的運(yùn)行
    1).首先要解決的是用戶線程改變對象引用關(guān)系時(shí),必須保證不能打破原來的對象圖結(jié)構(gòu),導(dǎo)致標(biāo)記結(jié)果出現(xiàn)錯(cuò)誤。CMS通過增量更新算法實(shí)現(xiàn),G1通過原始快照(SATB)實(shí)現(xiàn) --待了解
    2).此外,垃圾收集對用戶線程的影響還體現(xiàn)在回收過程中新創(chuàng)建對象的內(nèi)存分配上。G1為每個(gè)Region設(shè)計(jì)了兩個(gè)TAMS指針,把Region中的一部分內(nèi)容劃分出來用于并發(fā)回收過程中的新對象分配,并發(fā)回收時(shí)新分配的對象地址必須在這兩個(gè)指針之上。該指針之上的默認(rèn)存活,不回收。

特點(diǎn)

并行與并發(fā):G1能充分利用CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個(gè)CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時(shí)間。部分其他收集器原本需要停頓Java線程來執(zhí)行GC動(dòng)作,G1收集器仍然可以通過并發(fā)的方式讓java程序繼續(xù)執(zhí)行。
分代收集:雖然G1可以不需要其他收集器配合就能獨(dú)立管理整個(gè)GC堆,但是還是保留了分代的概念,采用不同的方式處理新建的對象、已經(jīng)存活了一段時(shí)間和熬過多次GC的舊對象獲取更好的收集效果。
空間整合:與CMS的“標(biāo)記--清理”算法不同,G1從整體來看是基于“標(biāo)記整理”算法 實(shí)現(xiàn)的收集器;從局部上來看是基于“復(fù)制”算法實(shí)現(xiàn)的。即G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后仍可提供規(guī)整的可用內(nèi)存。
可預(yù)測的停頓:這是G1相對于CMS的另一個(gè)大優(yōu)勢,降低停頓時(shí)間是G1 和 CMS 共同 的關(guān)注點(diǎn),但G1 除了追求低停頓外,還能建立可預(yù)測的停頓時(shí)間模型,能讓使用者明確指 定在一個(gè)長度為M毫秒的時(shí)間片段(通過參數(shù)"-XX:MaxGCPauseMillis"指定)內(nèi)完成垃圾收集。但是也不能完全無限度的降低停頓時(shí)間,每次停頓目標(biāo)時(shí)間太短,導(dǎo)致每次選出來的回收集只占堆內(nèi)存的一小部分,收集器收集的速度跟不上分配器分配的速度,導(dǎo)致垃圾慢慢堆積。

回收集集合CSet
在垃圾收集過程中收集的Region集合可以稱為收集集合CSet,也就是在垃圾收集暫停過程中被回收的目標(biāo)。G1可以面向堆內(nèi)任何部分來組成CSet進(jìn)行回收。
年輕代收集CSet只收集年輕代分區(qū),而混合收集則會(huì)通過啟發(fā)算法(GC效率),在老年代候選分區(qū)中,篩選出回收收益最高的分區(qū)到CSet中。

G1的分代收集
minorGC
開始生成對象時(shí),G1會(huì)選一個(gè)分區(qū)并指定他為eden分區(qū)。
不是現(xiàn)有的Eden滿了就會(huì)馬上觸發(fā)minorGC,G1會(huì)計(jì)算現(xiàn)在Eden區(qū)回收大概需要多久時(shí)間,如果回收時(shí)間遠(yuǎn)小于(-XX:MaxGCPauseMillis)參數(shù)值,那么動(dòng)態(tài)給Eden增加新的空閑的Region給新對象存放,-XX:G1MaxNewSizePercent參數(shù)代表eden內(nèi)存可以動(dòng)態(tài)添加的最大空間默認(rèn)60%,直到下一次Eden區(qū)存滿, G1重新計(jì)算回收時(shí)間與XX:MaxGCPauseMillis參數(shù)值對比,接近該參數(shù)才會(huì)觸發(fā)minorGC。

minorGC后存活的對象,轉(zhuǎn)移到survivor中,其余的過程與分代收集器中的minorGC一樣

minor gc內(nèi)部回收過程:stw,創(chuàng)建回收集CSet
1掃描根,主要是靜態(tài)和棧幀本地變量表的本地變量被掃描
2更新處理RSet,主要檢測跨region,從年輕代指向老年代的對象
3對象復(fù)制,eden->survivor/old
4處理引用,軟引用,弱引用,虛引用

mixedGC
年輕代收集不斷活動(dòng)后,老年代的空間也會(huì)被逐漸填充。當(dāng)老年代占用空間超過整堆比閾值IHOP(-XX:InitiatingHeapOccupancyPercent默認(rèn)45%)觸發(fā)MixedGC,會(huì)觸發(fā)新生代minorGC,也會(huì)收集部分老年代(根據(jù)期望的GC停頓時(shí)間確定old區(qū)垃圾回收的優(yōu)先順序)。MixedGC主要使用復(fù)制算法,需要把各個(gè)Region存活的對象拷貝到別的Region中,如果拷貝過程發(fā)現(xiàn)沒有足夠的空Region能夠承載拷貝對象則觸發(fā)FullGC。

RSset記錄了什么
新生代對老年代的跨region引用,防止整堆掃描
不需要記錄老年代對新生代的RSet嗎?
不需要,因?yàn)樾律膍inor GC在mixed GC之前,可以保證新生代會(huì)先被清除掉。
老年代對老年代的RSet呢?
可以記錄,防止整堆掃面
cms 重新標(biāo)記 增量更新
g1 最終標(biāo)記 原始快照(STAB)

G1分代收集整體流程
年輕代滿->G1判斷收集時(shí)間->動(dòng)態(tài)擴(kuò)容新生代Region->再次滿,且收集時(shí)間接近配置時(shí)間->觸發(fā)minorGC->老年代增長->老年代占用空間(老年代+Humongous)達(dá)到閾值IHOP(-XX:InitiatingHeapOccupancyPercent)默認(rèn)45%->G1開始準(zhǔn)備收集老年代->mixed gc->初始標(biāo)記->并發(fā)標(biāo)記,最終標(biāo)記->篩選回收(年輕代全部回收,百分百都是垃圾的region全部回收,部分垃圾的region計(jì)算出來,按優(yōu)先機(jī)制多次回收,回收情況按參數(shù)分配-XX:G1MixedGCCountTarget 回收次數(shù)默認(rèn)8,--XX:G1HeapWastePercent:堆浪費(fèi)百分比,當(dāng)G1發(fā)現(xiàn)可被回收的空間小于5%時(shí),就不會(huì)再進(jìn)行混合收集,也就是會(huì)結(jié)束當(dāng)前的回收過程)->拷貝存活對象到新的Region時(shí),沒有足夠空間會(huì)觸發(fā)fullGC

很多博客gc流程
初始標(biāo)記,并發(fā)標(biāo)記,最終標(biāo)記,清理并稱為并發(fā)標(biāo)記過程,并發(fā)標(biāo)記過程后有混合回收(多次)過程;本文按深入理解java虛擬機(jī)中說法,將其理解為為初始標(biāo)記,并發(fā)標(biāo)記,最終標(biāo)記,篩選回收(清理+混合回收多次,篩選范圍為所有年輕代,全是垃圾的老年代region(包含humongous),部分垃圾的老年代region(按gc效率做優(yōu)先級多次回收))

mixed gc內(nèi)部回收過程同minor gc內(nèi)部回收過程

G1整體流程
首先G1把Java分成多個(gè)Region,每個(gè)Region中存放著RSet,G1收集的時(shí)候掃描根節(jié)點(diǎn)GC Roots(如方法棧中的臨時(shí)變量),然后由GC Roots找到直連的對象,然后找到RSet中引用的對象,以這兩類對象進(jìn)行堆的引用標(biāo)記。標(biāo)記完成后把新生代中所有的Region放到CSet,有時(shí)會(huì)觸發(fā)全局標(biāo)記然后選出部分收集效率高的老年代Region加入到Cset中區(qū),然后清理CSet的Region,完成清理。

在R大的帖子中,給出了一個(gè)假象的G1垃圾收集運(yùn)行過程,如下圖所示,在結(jié)合上一小節(jié)的細(xì)節(jié),就可以將G1 GC的正常過程理解清楚了。

fullGC
1.轉(zhuǎn)移失敗的擔(dān)保機(jī)制,如從年輕代轉(zhuǎn)移存活對象,從老年代分區(qū)轉(zhuǎn)移存活對象或者分配巨型對象,無法找到可用的空閑分區(qū)
2.當(dāng)發(fā)生第一個(gè)條件后,G1會(huì)嘗試增加堆使用量,如果擴(kuò)展失敗,那么會(huì)觸發(fā)安全措施機(jī)制同時(shí)發(fā)生full GC
停止系統(tǒng)程序,采用單線程標(biāo)記-清除算法GC,非常耗時(shí)

初始標(biāo)記(initial mark,STW):暫停所有的其他線程,并記錄下gc roots直接能引用的對象,并記錄TAMS的值,讓下一階段用戶線程并發(fā)進(jìn)行時(shí),能正確的在可用的Region分配對象。這個(gè)階段需要停頓線程,但耗時(shí)很短,且是借用進(jìn)行MinorGC的時(shí)候同步的,所以G1在這個(gè)階段沒有額外停頓;
并發(fā)標(biāo)記(concurrent Marking):從GC Root開始對堆中對象進(jìn)行可達(dá)性分析,隊(duì)規(guī)掃描整個(gè)堆中的對象圖,找出要會(huì)后的對象,耗時(shí)較長,不過可與用戶程序并發(fā)執(zhí)行。對象圖掃描完成后,要重新處理STAB記錄下的在并發(fā)時(shí)有引用變動(dòng)的對象。
最終標(biāo)記(remark,STW): 對用戶線程做一個(gè)短暫的暫停,用于處理并打階段結(jié)束后仍遺留下來的最后那少量的STAB記錄。
篩選回收(cleanup,STW):負(fù)責(zé)更新Region的統(tǒng)計(jì)數(shù)據(jù),對各個(gè)Region的回收價(jià)值和成本進(jìn)行 排序,根據(jù)用戶所期望的GC停頓時(shí)間(可以用JVM參數(shù) -XX:MaxGCPauseMillis指定)來制定回收計(jì)劃,可以自由選擇多個(gè)Region構(gòu)成回收集,然后把決定回收的那一部分Region的存活對象復(fù)制到空的region中,再清理掉舊Region的全部空間。這里操作涉及存活對象的移動(dòng),必須暫停用戶線程,由多條收集器線程并行完成。

參數(shù)信息
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的線程數(shù)量
-XX:G1HeapRegionSize:指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認(rèn)將整堆劃分為 2048個(gè)分區(qū)
-XX:MaxGCPauseMillis:目標(biāo)暫停時(shí)間(默認(rèn)200ms)
-XX:G1NewSizePercent:新生代內(nèi)存初始空間(默認(rèn)整堆5%)
-XX:G1MaxNewSizePercent:新生代內(nèi)存最大空間
-XX:TargetSurvivorRatio:Survivor區(qū)的填充容量(默認(rèn)50%),Survivor區(qū)域里的一批對象(年齡 1+年齡2+年齡n的多個(gè)年齡對象)總和超過了Survivor區(qū)域的50%,此時(shí)就會(huì)把年齡n(含)以上的對象都放入老年代
-XX:MaxTenuringThreshold:最大年齡閾值(默認(rèn)15)
-XX:InitiatingHeapOccupancyPercent:老年代占用空間達(dá)到整堆內(nèi)存閾值(默認(rèn)45%),則執(zhí)行 新生代和老年代的混合收集(MixedGC),比如我們之前說的堆默認(rèn)有2048個(gè)region,如果有接近 1000個(gè)region都是老年代的region,則可能就要觸發(fā)MixedGC了
-XX:G1HeapWastePercent(默認(rèn)5%): gc過程中空出來的region是否充足閾值,在混合回收的時(shí) 候,對Region回收都是基于復(fù)制算法進(jìn)行的,都是把要回收的Region里的存活對象放入其他 Region,然后這個(gè)Region中的垃圾對象全部清理掉,這樣的話在回收過程就會(huì)不斷空出來新的 Region,一旦空閑出來的Region數(shù)量達(dá)到了堆內(nèi)存的5%,此時(shí)就會(huì)立即停止混合回收,意味著 本次混合回收就結(jié)束了。
-XX:G1MixedGCLiveThresholdPercent(默認(rèn)85%) region中的存活對象低于這個(gè)值時(shí)才會(huì)回收 該region,如果超過這個(gè)值,存活對象過多,回收的的意義不大。
-XX:G1MixedGCCountTarget:在一次回收過程中指定做幾次混合回收(默認(rèn)8次),在混合回收階段可以回收一會(huì),然后暫?;厥?,恢復(fù)系統(tǒng)運(yùn)行,一會(huì)再開始回收,這樣可以讓系統(tǒng)不至于單次停頓時(shí)間過長。

g1收集器推薦文集:https://www.cnblogs.com/GrimMjx/p/12234564.html
https://blog.csdn.net/coderlius/article/details/79272773
https://www.cnblogs.com/yufengzhang/p/10571081.html
https://baijiahao.baidu.com/s?id=1663956888745443356&wfr=spider&for=pc

5.垃圾回收器特點(diǎn)總結(jié)

收集器 特點(diǎn)
serial 單線程 新生代 復(fù)制
parnew 多線程并行 新生代 復(fù)制
parallel scavenge 多線程并行 新生代 復(fù)制 注重吞吐量
serial old 單線程 老生代 標(biāo)記整理
parallel old 多線程并行 老生代 標(biāo)記整理
cms 多線程并發(fā) 老年代 標(biāo)記清除 最短停頓時(shí)間 (初始標(biāo)記+并發(fā)標(biāo)記+重新標(biāo)記+并發(fā)清理) 兩次stw(初始階段+重新標(biāo)記) 初始標(biāo)記簡單標(biāo)記1次stw,重新標(biāo)記解決并發(fā)標(biāo)記時(shí)程序運(yùn)作時(shí)產(chǎn)生的垃圾記錄2次stw

缺點(diǎn):1.cpu敏感,占用cpu線程=(cpu數(shù)量+3)/4 2.cms無法處理浮動(dòng)垃圾,出現(xiàn)concurrent mode failure導(dǎo)致fullGC,原因:并發(fā)清理用戶程序運(yùn)行產(chǎn)生新的垃圾 3.標(biāo)記-清除,產(chǎn)生不連續(xù)空間碎片,導(dǎo)致提前fullGC
g1 多線程并發(fā)+并行 老年代+新生代 全局標(biāo)記整理,局部復(fù)制(無碎片化問題) 并行并發(fā),分代收集,空間整合,可預(yù)測停頓 younggc動(dòng)態(tài)調(diào)節(jié) mixedgc(初始標(biāo)記+并發(fā)標(biāo)記+最終標(biāo)記+篩選回收) 化整為零

其他收集器
后面還有ZGC(JDK11中的垃圾收集器)、Shenandoah GC(JDK12中的垃圾收集器)、C4 GC(Zing JVM)...

6.內(nèi)存分配與回收策略

對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在新生代中 Eden區(qū)分配。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC。垃圾收集期間發(fā)現(xiàn)已有對象無法全部放入Survivor空間,會(huì)通過分配擔(dān)保機(jī)制提前轉(zhuǎn)移到老年代去。
大對象直接進(jìn)入老年代
大對象就是需要大量連續(xù)內(nèi)存空間的對象,-XX:PretenureSizeThreshold 可以設(shè)置大對象的大小,如果對象超過設(shè)置大小會(huì)直接進(jìn)入老年代,不會(huì)進(jìn)入年輕代,這個(gè)參數(shù)只在 Serial 和ParNew兩個(gè)收集器下有效
長期存活的對象進(jìn)入老年代
虛擬機(jī)給每個(gè)對象一個(gè)對象年齡(Age)計(jì)數(shù)器,存儲(chǔ)在對象頭中。通過參數(shù)-XX:MaxTenuringThreshold來設(shè)置最大年齡,默認(rèn)15。
動(dòng)態(tài)對象年齡判斷
如果在Survivor空間中相同年齡所有對象大小的總和大于這塊Survivor區(qū)域內(nèi)存大小的50%,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡。對象動(dòng)態(tài)年齡判斷機(jī)制一般是在minor gc之后觸發(fā)的。
空間分配擔(dān)保
年輕代每次minor gc之前JVM都會(huì)計(jì)算下老年代剩余可用空間
如果這個(gè)可用空間小于年輕代里現(xiàn)有的所有對象大小之和,就會(huì)看一個(gè)“-XX:-HandlePromotionFailure”(是否允許擔(dān)保失敗,jdk1.8默認(rèn)就設(shè)置)的參數(shù)是否設(shè)置了,如果有這個(gè)參數(shù),就會(huì)看看老年代的可用內(nèi)存大小,是否大于之前每一次minor gc后進(jìn)入老年代的對象的平均大小。
如果上一步結(jié)果是小于或者之前說的參數(shù)沒有設(shè)置,那么就會(huì)觸發(fā)一次Full gc,對老年代和年輕代一起回收一次垃圾,如果回收完還是沒有足夠空間存放新的對象就會(huì)發(fā)生"OOM"
當(dāng)然,如果minor gc之后剩余存活的需要挪動(dòng)到老年代的對象大小還是大于老年代可用空間,那么也會(huì)觸發(fā)full gc,full gc完之后如果還是沒有空間放minor gc之后的存活對象,則也會(huì)發(fā)生“OOM”

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容