Java垃圾回收

java JVM內(nèi)部結(jié)構(gòu)

JVM Components
memory.png

java對象創(chuàng)建過程

/**
 * 
 * 關(guān)于Java對象創(chuàng)建的過程:
 * new關(guān)鍵字創(chuàng)建對象的3個(gè)步驟:
 * 1.在堆內(nèi)存中創(chuàng)建出對象的實(shí)例。
 * 2.為對象的實(shí)例成員變量賦初值。
 * 3.將對象的引用返回
 * 指針碰撞(前提是堆中的空間通過一個(gè)指針進(jìn)行分割,一側(cè)是已經(jīng)被占用的空間,另一側(cè)是未被占用的空間)
 * 空閑列表(前提是堆內(nèi)存空間中已被使用與未被使用的空間是交織在一起的,這時(shí),虛擬機(jī)就需要通過一個(gè)列表來記錄哪些空間是可以使用的,
 * 哪些空間是已被使用的,接下來找出可以容納下新創(chuàng)建對象的且未被使用的空間,在此空間存放該對象,同時(shí)還要修改列表上的記錄)
 * 對象在內(nèi)存中的布局:
 * 1.對象頭.
 * 2.實(shí)例數(shù)據(jù)(即我們在一個(gè)類中所聲明的各項(xiàng)信息)
 * 3.對齊填充(可選) !
 * 引用訪問對象的方式:
 * 1.使用句柄的方式。
 * 2.使用直接指針的方式。
 */
public class MemoryTest1 {
    public static void main(String[] args) {
        //-Xms5m -Xmx5m -XX:+HeapDumpOnOutOfMemoryError 設(shè)置jvm對空間最小和最大以及遇到錯(cuò)誤時(shí)把堆存儲文件打印出來
        //打開jvisualvm裝在磁盤上的轉(zhuǎn)存文件
        List<MemoryTest1> list = new ArrayList<>();
        while (true) {
            list.add(new MemoryTest1());
            System.gc();
        }
    }
}

虛擬機(jī)棧溢出測試

/**
 * 
 * 虛擬機(jī)棧溢出測試
 */
public class MemoryTest2 {

    private int length;

    public int getLength() {
        return length;
    }

    public void test() throws InterruptedException {
        length++;
        Thread.sleep(1);
        test();
    }

    public static void main(String[] args) {
        //測試調(diào)整虛擬機(jī)棧內(nèi)存大小為:  -Xss160k,此處除了可以使用JVisuale監(jiān)控程序運(yùn)行狀況外還可以使用jconsole
        MemoryTest2 memoryTest2 = new MemoryTest2();
        try {
            memoryTest2.test();
        } catch (Throwable e) {
            System.out.println(memoryTest2.getLength());//打印最終的最大棧深度為:2587
            e.printStackTrace();
        }
    }
}

元空間溢出測試

/**
 *
 * 元空間內(nèi)存溢出測試
 * 設(shè)置元空間大小:-XX:MaxMetaspaceSize=100m
 * 關(guān)于元空間參考:https://www.infoq.cn/article/java-permgen-Removed
 */
public class MemoryTest3 {
    public static void main(String[] args) {
        //使用動態(tài)代理動態(tài)生成類
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(MemoryTest3.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, ags, proxy) -> proxy.invokeSuper(obj, ags));
            System.out.println("Hello World");
            enhancer.create();// java.lang.OutOfMemoryError: Metaspace
        }
    }

JVM命令使用

/**
 *
 * jmam命令的使用 -clstats<pid>進(jìn)程id  to print class loader statistics
 * jmap -clstats 3740
 *
 * jstat -gc 3740
 *  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
 * 512.0  512.0   0.0    0.0   24064.0   9626.0   86016.0     1004.1   4864.0 3758.2 512.0  409.1     144    0.064   0      0.000    0.064
 * MC元空間總大小,MU元空間已使用的大小
 */
public class MemoryTest4 {
    public static void main(String[] args) {
        while (true)
            System.out.println("hello world");
    }
    //查看java進(jìn)程id jps -l
    // 使用jcmd查看當(dāng)前進(jìn)程的可用參數(shù):jcmd 10368 help
    //查看jvm的啟動參數(shù) jcmd 10368 VM.flags
   // 10368:-XX:CICompilerCount=3 -XX:InitialHeapSize=132120576 -XX:MaxHeapSize=2111832064 -XX:MaxNewSize=703594496
    // -XX:MinHeapDeltaBytes=524288 -XX:NewSize=44040192 -XX:OldSize=88080384 -XX:+UseCompressedClassPointers
    // -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC

}

JVM常用命令

jcmd (從JDK 1. 7開始增加的命令)
1. jcmd pid VM.flags: 查看JVM的啟動參數(shù)
2. jcmd pid help: 列出當(dāng)前運(yùn)行的Java進(jìn)程可以執(zhí)行的操作
3. jcmd pid helpJFR.dump:查看具體命令的選項(xiàng)
4. jcmd pid PerfCounter.print:看JVm性能相關(guān)的參數(shù)
5. jcmd pid VM.uptime:查有JVM的啟動時(shí)長
6. jcmd pid GC.class_ histogram: 查看系統(tǒng)中類的統(tǒng)計(jì)信息
7. jcmd pid Thread.print: 查看線程堆棧信息
8. jcmd pid GC.heap dump filename 導(dǎo)出Heap dump文件, 導(dǎo)出的文件可以通過jvisualvm查看
9. jcmd pid VM.system_ properties:查看JVM的屬性信息

JVM內(nèi)存舉例說明

   public void method() {
        Object object = new Object();

        /*生成了2部分的內(nèi)存區(qū)域,1)object這個(gè)引用變量,因?yàn)?        是方法內(nèi)的變量,放到JVM Stack里面,2)真正Object
        class的實(shí)例對象,放到Heap里面
        上述 的new語句一共消耗12個(gè)bytes, JVM規(guī)定引用占4
        個(gè)bytes (在JVM Stack), 而空對象是8個(gè)bytes(在Heap)
        方法結(jié)束后,對應(yīng)Stack中的變量馬上回收,但是Heap
        中的對象要等到GC來回收、*/
    }

JVM垃圾識別(根搜索算法( GC RootsTracing ))

  • 在實(shí)際的生產(chǎn)語言中(Java、 C#等),都是使用根搜索算法判定對象是否存活。

  • 算法基本思路就是通過一系列的稱為“GCRoots"的點(diǎn)作為起始進(jìn)行向下搜索,當(dāng)一個(gè)對象到GCRoots沒有任何引用鏈( Reference Chain)相連,則證明此對象
    是不可用的

  • 在Java語言中,GC Roots包括
    ●在VM棧(幀中的本地變量)中的引用
    ●方法區(qū)中的靜態(tài)引用
    ●JNI (即一般說的Native方法) 中的引用

方法區(qū)

  • Java虛擬機(jī)規(guī)范表示可以不要求虛擬機(jī)在這區(qū)實(shí)現(xiàn)GC,這區(qū)GC的“性價(jià)比”一般比較低
    在堆中,尤其是在新生代,常規(guī)應(yīng)用進(jìn)行I次GC一般可以回收70%~95%的空間,而方法區(qū)的GC效率遠(yuǎn)小于此

  • 當(dāng)前的商業(yè)JVM都有實(shí)現(xiàn)方法區(qū)的GC,主要回收兩部分內(nèi)容:廢棄常量與無用類

  • 主要回收兩部分內(nèi)容:廢棄常量與無用類

  • 類回收需要滿足如下3個(gè)條件:

    • 該類所有的實(shí)例都已經(jīng)被GC,也就是JVM中不存在該Class的任何實(shí)例
    • 加載該類的ClassL oader已經(jīng)被GC
    • 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,如不能在任何地方通過反射訪問該類的方法
  • 在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架、動態(tài)生成JSP以及OSGi這類頻繁自定義Classloader的場景都需要JVM具備類卸載的支持以保證方法區(qū)不會溢出

垃圾判斷與GC算法

  • 垃圾判斷的算法

    • 引用計(jì)數(shù)算法(Reference Counting)
    • 根搜索算法( GC RootsTracing )
    • 在實(shí)際的生產(chǎn)語言中(Java、 C#等)都是使用根搜索算法判定對象是否存活
    • 算法基本思路就是通過一一系列的稱為GCRoots"的點(diǎn)作為起始進(jìn)行向下搜索,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈(Reference Chain)相連,則證明此對象是不可用的
  • 在Java語言中,可作為GC Roots的對象包括下面幾種:

    • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
    • 方法區(qū)中類靜態(tài)屬性引用的對象。
    • 方法區(qū)中常量引用的對象。
    • 本地方法棧中JNI(即一般說的Native方法)引用的對象
根搜索算法(Root Tracing)
  • 標(biāo)記-清除算法(Mark Sweep)
  • 標(biāo)記-整理算法(Mark-Compact)
  • 復(fù)制算法(Copying)
  • 分代算法(Generational)

標(biāo)記一清除算法(Mark-Sweep)

  • 算法分為“標(biāo)記”和“清除”兩個(gè)階段,
    首先標(biāo)記出所有需要回收的對象,然后回
    收所有需要回收的對象

  • 缺點(diǎn)
    效率問題,標(biāo)記和清理兩個(gè)過程效率都不高
    空間問題,
    標(biāo)記清理之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致后續(xù)使用中無法找到足夠的連續(xù)內(nèi)存而提前觸發(fā)另一次的垃圾搜集動作

  • 效率不高,需要掃描所有對象。堆越大,GC越慢
    存在內(nèi)存碎片問題。GC次數(shù)越多,碎片越為嚴(yán)重

標(biāo)記一清除算法(Mark-Sweep)

復(fù)制(Copying) 搜集算法

  • 將可用內(nèi)存劃分為兩塊,每次只使用其中的一塊,當(dāng)一半?yún)^(qū)內(nèi)存用完了,僅將還存活
    的對象復(fù)制到另外一塊上面,然后就把原來整塊內(nèi)存空間一次性清理掉,

  • 這樣使得每次內(nèi)存回收都是對整個(gè)半?yún)^(qū)的回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存就可以了,實(shí)現(xiàn)簡單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來的一半,代價(jià)高昂

  • 現(xiàn)在的商業(yè)虛擬機(jī)中都是用了這一種收集算法來回收新生代

  • 將內(nèi)存分為一塊較大的eden空間和2塊較少的survivor空間,每次使用eden和其中一塊
    survivor, 當(dāng)回收時(shí)將eden和survivor還存活的對象一次性拷 貝到另外一塊survivor空間上,然后清理掉eden和用過的survivor

  • Oracle Hotspot虛擬機(jī)默認(rèn)eden和survivor的大小比例是8:1,也就是每次只有10%的內(nèi)存是“浪費(fèi)”的

  • 復(fù)制收集算法在對象存活率高的時(shí)候,效率有所下降

  • 如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保用于應(yīng)付半?yún)^(qū)內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法

復(fù)制(Copying) 搜集算法
  • 只需要掃描存活的對象,效率更高
  • 不會產(chǎn)生碎片
  • 需要浪費(fèi)額外的內(nèi)存作為復(fù)制區(qū)
  • 復(fù)制算法非常適合生命周期比較短的對象,因?yàn)槊看蜧C總能回收大部分的對象,復(fù)制的開銷比較小
  • 根據(jù)IBM的專i研究,98%的Java對象只會存活1個(gè)GC周期,對這些對象很適合用復(fù)制算法。而且
    不用1: 1的劃分工作區(qū)和復(fù)制區(qū)的空間

標(biāo)記一整理( Mark-Compact )算法

  • 標(biāo)記過程仍然樣,但后續(xù)步驟不是進(jìn)行直接清理,而是令所有存活的對象一端移動,然后直接清理掉這端邊界以外的內(nèi)存。

  • 沒有內(nèi)存碎片

  • 比Mark-Sweep耗費(fèi)更多的時(shí)間進(jìn)行compact

分代收集。( GenerationalCollecting)算法

  • 當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都是采用“分代收集”( Generational Collecting)算法,根據(jù)對象不同的存活周期將內(nèi)存劃分為幾塊。
  • 一般是把Java堆分作新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴?,譬如新生代每次GC都有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本,就可以完成收集。

Hotspot JVM 6中共劃分為三個(gè)代:

  • 年輕代(Young Generation)
  • 老年代(Old Generation)和
  • 永久代( Permanent Generation)
Hotspot JVM 6中共劃分為三個(gè)代
  • 年輕代(Young Generation)
    新生成的對象都放在新生代。年輕代用復(fù)制算法進(jìn)行GC (理論上年輕代對象的生命周期非常短,所以適合復(fù)制算法)

  • 年輕代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè)Survivor區(qū)(可以通過參數(shù)設(shè)置Survivor個(gè)數(shù))。對象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對象將被復(fù)制到一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活對象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)?shù)诙€(gè)Survivor區(qū)也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過來的并且此時(shí)還存活的對象,將被復(fù)制到老年代。2個(gè)Survivor是完全對稱,輪流替換。

  • Eden和2個(gè)Survivor的缺省比例是8:1:1,也就是10%的空間會被
    浪費(fèi)??梢愿鶕?jù)GClog的信息調(diào)整大小的比例

  • 老年代(Old Generation)

    • 存放了經(jīng)過一次或多次GC還存活的對象
    • 一般采用Mark-Sweep或者M(jìn)ark-Compact算法進(jìn)行GC
    • 有多種垃圾收集器可以選擇。每種垃圾收集器可以看作一個(gè)GC算法的具體實(shí)現(xiàn)??梢愿鶕?jù)具體應(yīng)用的需求選用合適的垃圾收集器(追求吞吐量?追求最短的響應(yīng)時(shí)間?)
  • 永久代

    • 并不屬于堆(Heap).但是GC也會涉及到這個(gè)區(qū)域
    • 存放了每個(gè)Class的結(jié)構(gòu)信息, 包括常量池、字段描述、方法描述。與垃圾收集要收集的Java對象關(guān)系不大

內(nèi)存分配與回收

  • 堆上分配
    大多數(shù)情況在eden上分配,偶爾會直接在old上分配細(xì)節(jié)取決于GC的實(shí)現(xiàn)

  • 棧上分配
    原子類型的局部變量

  • GC要做的是將那些dead的對象所占用的內(nèi)存回收掉

    • Hotspot認(rèn)為沒有引用的對象是dead的
    • Hotspot將引用分為四種: Strong、 Soft、Weak、Phantom
      Strong 即默認(rèn)通過Object o=new Object()這種方式賦值的引用
      Soft、Weak、 Phantom這 三種則都是繼承Reference
  • 在Full GC時(shí)會對Reference類型的引用進(jìn)行特殊處理

    • Soft:內(nèi)存不夠時(shí)一定會被GC、長期不用也會被GC
    • Weak: - 定會被GC, 當(dāng)被mark為dead, 會在ReferenceQueue中通知
    • Phantom: 本來就沒引用,當(dāng)從jvm heap中釋放時(shí)會通知

垃圾回收器

垃圾回收器

GC回收的時(shí)機(jī)

  • 在分代模型的基礎(chǔ)上,GC從時(shí)機(jī)上分為兩種: Scavenge GC和Full GC
    • Scavenge GC (Minor GC)
      觸發(fā)時(shí)機(jī):新對象生成時(shí),Eden空間滿了理論上Eden區(qū)大多數(shù)對象會在ScavengeGC回收,復(fù)制算法的執(zhí)
      行效率會很高,ScavengeGC時(shí)間比較短。
    • Full GC
      對整個(gè)JVM進(jìn)行整理,包括Young、Old 和Perm主要的觸發(fā)時(shí)機(jī): 1) Old滿了2) Perm滿了3) system.gc()效率很低,盡量減少Full GC。

垃圾回收器(Garbage Collector)

  • 分代模型: GC的宏觀愿景;
  • 垃圾回收器: GC的具體實(shí)現(xiàn)
  • Hotspot JVM提供多種垃圾回收器,我們需要根據(jù)具體應(yīng)用的需要采用不同的回收器
  • 沒有萬能的垃圾回收器,每種垃圾回收器都有自己的適用場景

垃圾收集器的‘并行”和并發(fā)

  • 并行(Parallel):指多個(gè)收集器的線程同時(shí)工作,但是用戶線程處于等待狀態(tài)
  • 并發(fā)(Concurrent):指收集器在工作的同時(shí),可以允許用戶線程工作。并發(fā)不代表解決了GC停頓的問題,在關(guān)鍵的步驟還是要停頓。比如在收集器標(biāo)記垃圾的時(shí)候。但在清除垃圾的時(shí)候,用戶線程可以和GC線程并發(fā)執(zhí)行。

Serial收集器

  • 最早的收集器,單線程進(jìn)行GC, New和Old Generation都可以使用,在新生代,采用復(fù)制算法;
  • 在老年代,采用Mark-Compact算法因?yàn)槭菃尉€程GC,沒有多線程切換的額外開銷,簡單實(shí)用
    Hotspot Client模式默認(rèn)的收集器
Serial收集器

ParNew收集器

  • ParNew收集器就是Serial的多線程版本,除了使用多個(gè)收集線程外,其余行為包括算法、STW、對象分配規(guī)則、回收策略等都與Seria收集器一模一樣。

  • 對應(yīng)的這種收集器是虛擬機(jī)運(yùn)行在Server模式的默認(rèn)新生代收集器,在單CPU的環(huán)境中,ParNew收集器并不會比Serial收集器有更好的效果

  • Serial收集器在新生代的多線程版本

  • 使用復(fù)制算法(因?yàn)獒槍π律?只有在多CPU的環(huán)境下,效率才會比Serial收集器高

  • 可以通過-XX:ParallelGC Threads來控制GC線程數(shù)的多少。需要結(jié)合具體CPU的個(gè)數(shù)Server模式下新生代的缺省收集器

ParNew收集器

Parallel Scavenge收集器

  • Parallel Scavenge收集器也是一個(gè)多線程收集器,也是使用復(fù)制算法,但它的對象分配規(guī)則與回收策略都與ParNew收集器有所不同,它是以吞吐量最大化(即GC時(shí)間占總運(yùn)行時(shí)間最小)為目標(biāo)的收集器實(shí)現(xiàn),它允許較長時(shí)間的STW換取總吞吐量最大化

CMS ( Concurrent Mark Sweep )收集器

  • CMS是一種以最短停頓時(shí)間為目標(biāo)的收集器,使用CMS并不能達(dá)到GC效率最高(總體GC時(shí)間最小),但它能盡可能降低GC時(shí)服務(wù)的停頓時(shí)間,CMS收集器使用的是標(biāo)記一清除算法

  • 特點(diǎn):

    • 追求最短停頓時(shí)間,非常適合Web應(yīng)用
    • 只針對老年區(qū),一般結(jié)合ParNew使用
    • Concurrent, GC線程和用戶線程并發(fā)工作(盡量并發(fā) )
    • Mark-Sweep
    • 只有在多CPU環(huán)境下才有意義
    • 使用-XX:+UseConcMarkSweepGC打開
  • CMS收集器的缺點(diǎn)

    • CMS以犧牲CPU資源的代價(jià)來減少用戶線程的停頓。當(dāng)CPU個(gè)數(shù)少于4的時(shí)候,有可能對吞吐量影響非常大
    • CMS在并發(fā)清理的過程中,用戶線程還在跑。這時(shí)候需要預(yù)留一部分空間給用戶線程
    • CMS用Mark-Sweep,會帶來碎片問題。碎片過多的時(shí)候會容易頻繁觸發(fā)FullGC
CMS收集器

G1收集器

G1
  • heap被劃分為一個(gè)個(gè)相等的不連續(xù)的內(nèi)存區(qū)域(regions) ,每個(gè)region都有一個(gè)分代的角色: eden、 survivor、 old

  • 對每個(gè)角色的數(shù)量并沒有強(qiáng)制的限定,也就是說對每種分代內(nèi)存的大小,可以動態(tài)變化

  • G1最大的特點(diǎn)就是高效的執(zhí)行回收,優(yōu)先去執(zhí)行那些大量對象可回收的區(qū)域(region)

  • G1使用了gc停頓可預(yù)測的模型,來滿足用戶設(shè)定的gc停頓時(shí)間,根據(jù)用戶設(shè)定的目標(biāo)時(shí)間,G1會自動地選擇哪些region要清除,次清除多少個(gè)region

  • G1從多個(gè)region中復(fù)制存活的對象,然后集中放入一個(gè)region中,同時(shí)整理、清除內(nèi)存(copying收集算法)

  • 對比使用mark-sweep的CMS, G1使用的copying算法不會造成內(nèi)存碎片;

  • 對比Parallel Scavenge(基于copying )、Parallel Old收集器(基于mark-compact-sweep),Parallel會對整個(gè)區(qū)域做整理導(dǎo)致gc停頓會比較長,而G1只是特定地整理幾個(gè)region。

  • G1并非一個(gè)實(shí)時(shí)的收集器,與parallelScavenge-樣,對gc停頓時(shí)間的設(shè)置并不絕對生效,只是G1有較高的幾率保證不超過設(shè)定的gc停頓時(shí)間。與之前的gc收集器對比,G1會根據(jù)用戶設(shè)定的gc停頓時(shí)間,智能評估哪幾個(gè)region需要被回收可以滿足用戶的設(shè)定

分區(qū)(Region):

  • G1采取了不同的策略來解決并行、串行和CMS收集器的碎片、暫停時(shí)間不可控等問題一G1將 整個(gè)堆分成相同大小的分區(qū)(Region)

  • 每個(gè)分區(qū)都可能是年輕代也可能是老年代,但是在同,時(shí)刻只能屬于某個(gè)代。年輕代、幸存區(qū)、老年代這些概念還存在,成為邏輯上的概念,這樣方便復(fù)用之前分代框架的邏輯。

  • 在物理,上不需要連續(xù),則帶來了額外的好處有的分區(qū)內(nèi)垃圾對象特別多,有的分區(qū)內(nèi)垃圾對象很少,G1會優(yōu)先回收垃圾對象特別多的分區(qū),這樣可以花費(fèi)較少的時(shí)間來回收這些分區(qū)的垃圾,這也就是G1名字的由來,即首先收集垃圾最多的分區(qū)。

  • 依然是在新生代滿了的時(shí)候,對整個(gè)新生代進(jìn)行回收整個(gè)新生代中的對象,要么被回收、要么晉升,至于新生代也采取分區(qū)機(jī)制的原因,則是因?yàn)檫@樣跟老年代的策略統(tǒng)一,方便調(diào)整代的大小

  • G1還是一種帶壓縮的收集器,在回收老年代的分區(qū)時(shí),是將存活的對象從一個(gè)分區(qū)拷貝到另一個(gè)可用分區(qū),這個(gè)拷貝的過程就實(shí)現(xiàn)了局部的壓縮。

收集集合(CSet)

  • 一組可被回收的分區(qū)的集合。在CSet中存活的數(shù)據(jù)會在GC過程中被移動到另一個(gè)可用分區(qū),CSet中的分區(qū)可以來自eden空間、survivor空間、 或者老年代

已記憶集合(RSet) :

  • RSet記錄了其他Region中的對象引用本Region中對象的關(guān)系,屬于points-into結(jié)構(gòu)( 誰引用了我的對象)RSet的價(jià)值在于使得垃圾收集器不需要掃描整個(gè)堆找到誰引用了當(dāng)前分區(qū)中的對象,只需要掃描RSet即可。

  • Region1和Region3中的對象都引用了Region2中的對象,因此在Region2的RSet中記錄了這兩個(gè)引用。

region
  • G1 GC是在points-out的card table之上再加了一層結(jié)構(gòu)來構(gòu)成points-into RSet:每個(gè)region會記錄下到底哪些別的
    region有指向自己的指針,而這些指針分別在哪些card的范圍內(nèi)。

  • 這個(gè)RSet其實(shí)是一個(gè)hash table,key是別的region的起始地址,value是一個(gè)集合,里面的元素是card table的index.
    舉例來說,如果region A的RSet里有一項(xiàng)的key是region B,value里有index為1234的card,它的意思就是region B的
    一個(gè)card里 有引用指向region A。所以對region A來說,該RSet記錄的是points-into的關(guān)系;而card table仍然記錄了points-out的關(guān)系。

  • Snapshot-AtThe-Beginning(SATB):SATB是G1 GC在并發(fā)標(biāo)記階段使用的增量式的標(biāo)記算法,

  • 并發(fā)標(biāo)記是并發(fā)多線程的,但并發(fā)線程在同一時(shí)刻只掃描一個(gè)分區(qū)

參考鏈接:https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html

G1相對于CMS的優(yōu)勢

  • G1在壓縮空間方面有優(yōu)勢
  • G1通過將內(nèi)存空間分成區(qū)域(Region) 的方式避免內(nèi)存碎片問題Eden、Survivor、 Old區(qū)不再固定,在內(nèi)存使用效率上來說更靈活
  • G1可以通過設(shè)置預(yù)期停頓時(shí)間( Pause Time) 來控制垃圾收集時(shí)間,避免應(yīng)用雪崩現(xiàn)象
  • G1在回收內(nèi)存后會馬上同時(shí)做合并空閑內(nèi)存的工作,而CMS默認(rèn)是在STW ( stop the world) 的時(shí)候做
  • G1會在Young GC中使用,而CMS只能在Old區(qū)使用

G1的適合場景

  • 服務(wù)端多核CPU、JVM內(nèi)存占用較大的應(yīng)用
  • 應(yīng)用在運(yùn)行過程中會產(chǎn)生大量內(nèi)存碎片、需要經(jīng)常壓縮空間
  • 想要更可控、可預(yù)期的GC停頓周期:防止高并發(fā)下應(yīng)用的雪崩現(xiàn)象

G1 GC模式

  • G1提供了兩種GC模式,Young GC和Mixed GC, 兩種都是完全Stop The World的

  • Young GC:選定所有年輕代里的Region。通過控制年輕代的Region個(gè)數(shù),即年輕代內(nèi)存大小,來控制Young GC的時(shí)間開銷。

  • Mixed GC:選定所有年輕代里的Region,外加根據(jù)global concurrent marking統(tǒng)計(jì)得出收集收益高的若干老年代Region。在用戶指定的開銷目標(biāo)范圍內(nèi)盡可能選擇收益高的老年代Region

  • Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC實(shí)在無法跟上程序分配內(nèi)存的速度,導(dǎo)致老年代填滿無法繼續(xù)進(jìn)行MixedGC,就會使用serialold GC (Full GC)來收集整個(gè)GC heap。所以本質(zhì)上,G1是不提供Full GC的

global concurrent marking

  • 初始標(biāo)記( initial mark, STW) :它標(biāo)記了從GCRoot開始直接可達(dá)的對象。

  • 并發(fā)標(biāo)記( Concurrent Marking) :這個(gè)階段從GC Root開始對heap中的對象進(jìn)行標(biāo)記,標(biāo)記線
    程與應(yīng)用程序線程并發(fā)執(zhí)行,并且收集各個(gè)Region的存活對象信息。

  • 重新標(biāo)記( Remark, STW) :標(biāo)記那些在并發(fā)標(biāo)記階段發(fā)生變化的對象,將被回收。

  • 清理(Cleanup) :清除空Region (沒有存活對象的),加入到free list。

  • 第一階段initial mark是共用了Young GC的暫停,這是因?yàn)樗麄兛梢詮?fù)用rootscan操作,所以可以說global concurrent marking是伴隨Young GC而發(fā)生的

  • 第四階段Cleanup只是回收了沒有存活對象的Region,所以它并不需要STW。

G1在運(yùn)行過程中的主要模式

  • YGC(不同于CMS)
    • G1 YGC在Eden充滿時(shí)觸發(fā),在回收之后所有之前屬于Eden的區(qū)塊全部變成空白,即不屬于任何一個(gè)分區(qū)( Eden、Survivor、Old )
    • YGC執(zhí)行步驟:
      • 階段1:根掃描
        靜態(tài)和本地對象被描
      • 階段2:更新RS
        處理dirty card隊(duì)列更新RS
      • 階段3:處理RS
        檢測從年輕代指向老年代的對象
      • 階段4:對象拷貝
        拷貝存活的對象到survivor/old區(qū)域
      • 階段5:處理引用隊(duì)列
        軟引用,弱引用,虛引用處理
  • 并發(fā)階段(global concurrent marking)
  • 混合模式
  • Full GC (一 般是G1出現(xiàn)問題時(shí)發(fā)生,本質(zhì)上不屬于G1,G1進(jìn)行的回退策略(回退為:Serial Old GC))

什么時(shí)候發(fā)生MixedGC?

  • 由一些參數(shù)控制,另外也控制著哪些老年代Region會被選入CSet (收集集合)
    • G1HeapWastePercent:在globalconcurrent marking結(jié)束之后,我們可以知道oldgenregions中有多少空間要被回收,在每次YGC之后和再次發(fā)生MixedGC之前,會檢查垃圾占比是否達(dá)到此參數(shù),只有達(dá)到了,下次才 會發(fā)生Mixed GC
    • G1MixedGCLiveThresholdPercent: oldgeneration region中的存活對象的占比,只有在此參數(shù)之下,才會被選入CSet
    • G1MixedGCCountTarget:一 次globalconcurrent marking之后,最多執(zhí)行Mixed GC的次數(shù)
    • G1OldCSetRegionThresholdPercent:次Mixed GC中能被選入CSet的最多old generation region數(shù)量

三色標(biāo)記算法

提到并發(fā)標(biāo)記,我們不得不了解并發(fā)標(biāo)記的三色標(biāo)記算法。它是描述追蹤式回收器的一種有效的方法,利用它可以推演回收器的正確性

  • 我們將對象分成三種類型:
    • 黑色:根對象,或者該對象與它的子對象都被掃描過(對象被標(biāo)記了,且它的所有field也被標(biāo)記完了)
    • 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象( 它的field還沒有被標(biāo)記或標(biāo)記完)
    • 白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達(dá)對象,即垃圾對象(對象沒有被標(biāo)記到)

提到并發(fā)標(biāo)記,我們不得不了解并發(fā)標(biāo)記的三色標(biāo)記算法。它是描述追蹤式回收器的一種有效的方法,利用它可以推演回收器的正確性

遍歷了所有可達(dá)的對象后,所有可達(dá)的對象都變成了黑色。不可達(dá)的對象即為白色,需要被清理,如圖:

三色標(biāo)記算法
  • 但是如果在標(biāo)記過程中,應(yīng)用程序也在運(yùn)行,那么對象的指針就有可能改變。這樣的話,我們就會遇到一個(gè)問題:對象丟失問題

這時(shí)候應(yīng)用程序執(zhí)行了以下操作:
A.c=C
B.c=null
這樣,對象的狀態(tài)圖變成如下情形:

這時(shí)候垃圾收集器再標(biāo)記掃描的時(shí)候就會變成下圖這樣

  • 很顯然,此時(shí)C是白色,被認(rèn)為是垃圾需要清理掉,顯然這是不合理的

SATB

  • 在G1中,使用的是SATB ( Snapshot-At-The- Beginning)的方式,刪除的時(shí)候記錄所有的對象
  • 它有3個(gè)步驟
    • 在開始標(biāo)記的時(shí)候生成一個(gè)快照圖,標(biāo)記存活對象
    • 在并發(fā)標(biāo)記的時(shí)候所有被改變的對象入隊(duì)(在writebarrier里把所有舊的引用所指向的對象都變成非白的)
    • 可能存在浮動垃圾,將在下次被收集

G1混合式回收

  • G1到現(xiàn)在可以知道哪些老的分區(qū)可回收垃圾最多。當(dāng)全局并發(fā)標(biāo)記完成后,在某個(gè)時(shí)刻,就開始了Mixed GC。這些垃圾回收被稱作“混合式”是因?yàn)樗麄儾粌H僅進(jìn)行正常的新生代垃圾收集,同時(shí)也回收部分后臺掃描線程標(biāo)記的分區(qū)混合式GC也是采用的復(fù)制清理策略,當(dāng)GC完成后,會重新釋放空間

SATB詳解

  • SATB是維持并發(fā)GC的一種手段。G1并發(fā)的基礎(chǔ)就是SATB。SATB可以理解成在GC開始之前對堆內(nèi)存里的對象做次快照,此時(shí)活的對象就認(rèn)為是活的,從而形成了一個(gè)對象圖。
  • 在GC收集的時(shí)候,新生代的對象也認(rèn)為是活的對象,除此之外其他不可達(dá)的對象都認(rèn)為是垃圾對象

如何找到在GC過程中分配的對象呢?

  • 每個(gè)region記錄著兩個(gè)top-at-mark-start ( TAMS 指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。

  • 通過這種方式我們就找到了在GC過程中新分配的對象,并把這些對象認(rèn)為是活的對象。

  • 解決了對象在GC過程中分配的問題,那么在GC過程中引用發(fā)生變化的問題怎么解決呢?

  • G1給出的解決辦法是通過WriteBarrier.Write Barrier就是對引用字段進(jìn)行賦值做了額外處理。通過Write Barrier就可以了解到哪些引用對象發(fā)生了什么樣的變化

mark的過程就是遍歷heap標(biāo)記live object的過程,

  • 采用的是三色標(biāo)記算法,這三種顏色為white(表示還未訪問到)、gray(訪問到但是它用到的引用還沒有完全掃描、black( 訪問到而且其用到的引用已經(jīng)完全掃描完)

  • 整個(gè)三色標(biāo)記算法就是從GCroots出發(fā)遍歷heap,針對可達(dá)對象先標(biāo)記white為gray,然后再標(biāo)記gray為black;遍歷完成之后所有可達(dá)對象都是black的,所有white都是可以回收的

  • SATB僅僅對于在marking開始階段進(jìn)行"snapshot"(marked all reachable at markstart),但是concurrent的時(shí)候并發(fā)修改可能造成對象漏標(biāo)記

  • 對black新引用了一個(gè)white對象,然后又從gray對象中刪除了對該white對象的引用,這樣會造成了該white對象漏標(biāo)記

  • 對black新引用了一個(gè)white對象,然后從gray對象刪了一個(gè)引用該white對象的white對象,這樣也會造成了該white對象漏標(biāo)記,

  • 對black新引用了一個(gè)剛new出來的white對象,沒有其他gray對象引用該white對象,這樣也會造成了該white對象漏標(biāo)記

  • 對于三色算法在concurrent的時(shí)候可能產(chǎn)生的漏標(biāo)記問題,SATB在marking階段中,對于從gray對象移除的目標(biāo)引用對象標(biāo)記為gray,對于black引用的新產(chǎn)生的對象標(biāo)記為black;由于是在開始的時(shí)候進(jìn)行snapshot,因而可能存在Floating Garbage

漏標(biāo)與誤標(biāo)

  • 誤標(biāo)沒什么關(guān)系,頂多造成浮動垃圾,在下次gc還是可以回收的,但是漏標(biāo)的后果是致命的,把本應(yīng)該存活的對象給回收了,從而影響的程序的正確性

  • 漏標(biāo)的情況只會發(fā)生在白色對象中,且滿足以下任意一個(gè)條件

    • 并發(fā)標(biāo)記時(shí),應(yīng)用線程給一個(gè)黑色對象的引用類型字段賦值 了該白色對象

    • 并發(fā)標(biāo)記時(shí),應(yīng)用線程刪除所有灰色對象到該白色對象的引用

  • 對于第一種情況,利用post-write barrier,記錄所有新增的引用關(guān)系,然后根據(jù)這些引用關(guān)系為根重新掃描一-遍

  • 對于第二種情況,利用pre-write barrier,將所有即將被刪除的引用關(guān)系的舊引用記錄下來,最后以這些舊引用為根重新掃描一遍

停頓預(yù)測模型

  • G1收集器突出表現(xiàn)出來的一點(diǎn)是通過一個(gè)停頓預(yù)測模型根據(jù)用戶配置的停頓時(shí)間來選擇CSet的大小,從而達(dá)到用戶期待的應(yīng)用程序暫停時(shí)間。

  • 通過-XX:MaxGCPauseMillis參數(shù)來設(shè)置。這一點(diǎn)有點(diǎn)類似于ParallelScavenge收集器。 關(guān)于停頓時(shí)間的設(shè)置并不是越短越好。

  • 設(shè)置的時(shí)間越短意味著每次收集的CSet越小,導(dǎo)致垃圾逐步積累變多,最終不得不退化成SerialGC;停頓時(shí)間設(shè)置的過長,那么會導(dǎo)致每次都會產(chǎn)生長時(shí)間的停頓,影響了程序?qū)ν獾捻憫?yīng)時(shí)間

G1的收集模式

  • G1的運(yùn)行過程是這樣的:會在Young GC和Mixed GC之間不斷地切換運(yùn)行,同時(shí)定期地做全局并發(fā)標(biāo)記,在實(shí)在趕不上對象創(chuàng)建速度的情況下 使用Full GC(Serial GC)。
  • 初始標(biāo)記是在Young GC.上執(zhí)行的,在進(jìn)行全局并發(fā)標(biāo)記的時(shí)候不會做MixedGC,在做MixedGC的時(shí)候也不會啟動初始標(biāo)記階段。
  • 當(dāng)MixedGC趕不上對象產(chǎn)生的速度的時(shí)候就退化成FullGC,這一點(diǎn)是需要重點(diǎn)調(diào)優(yōu)的地方

G1最佳實(shí)踐

  • 不要設(shè)置新生代和老年代的大小,G1收集器在運(yùn)行的時(shí)候會調(diào)整新生代和老年代
    的大小。通過改變代的大小來調(diào)整對象晉升的速度以及晉升年齡,從而達(dá)到我們?yōu)槭占髟O(shè)置的暫停時(shí)間目標(biāo)。

  • 設(shè)置了新生代大小相當(dāng)于放棄了G1為我們做的自動調(diào)優(yōu)。我們需要做的只是設(shè)置整個(gè)堆內(nèi)存的大小,剩下的交給G1自已去分配各個(gè)代的大小即可。

  • 不斷調(diào)優(yōu)暫停時(shí)間指標(biāo)

    • 通過-XX:MaxGCPauseMillis=x可以設(shè)置啟動應(yīng)用程序暫停的時(shí)間,G1在運(yùn)行的時(shí)候會根據(jù)這個(gè)參數(shù)選擇CSet來滿足響應(yīng)時(shí)間的設(shè)置。一般情況下這個(gè)值設(shè)置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設(shè)置成50ms就不太合理。暫停時(shí)間設(shè)置的太短,就會導(dǎo)致出 現(xiàn)G1跟不上垃圾產(chǎn)生的速度。最終退化成Full GC。所以對這個(gè)參數(shù)的調(diào)優(yōu)是一個(gè)持續(xù)的過程,逐步調(diào)整到最佳狀態(tài)。
  • 關(guān)注Evacuation Failure

    • Evacuation(表示copy) Failure類似于CMS里面的晉升失敗,堆空間的垃圾太多導(dǎo)致無法完成Region之間的拷貝,于是不得不退化成Full GC來做一次全局范圍內(nèi)的垃圾收集

G1日志解析:


/**
 * 
 * G1日志分析
 * 虛擬機(jī)相關(guān)參數(shù):
 * -verbose:gc
 * -Xms10m
 * -Xmx10m
 * -XX:+UseG1GC 表示垃圾收集器使用G1
 * -XX:+PrintGCDetails
 * -XX:+PrintGCDateStamps
 * -XX:MaxGCPauseMillis=200m 設(shè)置垃圾收集最大停頓時(shí)間
 */
public class G1LogAnalysis {

    public static void main(String[] args) {
        int size = 1024 * 1024;
        byte[] bytes1 = new byte[size];
        byte[] bytes2 = new byte[size];
        byte[] bytes3 = new byte[size];
        byte[] bytes4 = new byte[size];
        System.out.println("hello world");
    }
}
/**
 * GC日志:
 * 2020-04-14T16:13:41.663+0800: [GC pause (G1 Humongous Allocation【說明分配的對象超過了region大小的50%】) (young) (initial-mark), 0.0014516 secs]
 * [Parallel Time: 1.1 ms, GC Workers: 4【GC工作線程數(shù)】]
 * [GC Worker Start (ms): Min: 167.0, Avg: 167.1, Max: 167.1, Diff: 0.1]【幾個(gè)垃圾收集工作的相關(guān)信息統(tǒng)計(jì)】
 * [Ext Root Scanning (ms): Min: 0.4, Avg: 0.4, Max: 0.4, Diff: 0.1, Sum: 1.6]
 * [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
 * [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
 * [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
 * [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
 * [Object Copy (ms): Min: 0.6, Avg: 0.6, Max: 0.6, Diff: 0.0, Sum: 2.4]
 * [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
 * [Termination Attempts: Min: 1, Avg: 1.3, Max: 2, Diff: 1, Sum: 5]
 * 【上面的幾個(gè)步驟為YOUNG GC的固定執(zhí)行步驟】
 * 階段1:根掃描
 * 靜態(tài)和本地對象被描
 * 階段2:更新RS
 * 處理dirty card隊(duì)列更新RS
 * 階段3:處理RS
 * 檢測從年輕代指向老年代的對象
 * 階段4:對象拷貝
 * 拷貝存活的對象到survivor/old區(qū)域
 * 階段5:處理引用隊(duì)列
 * 軟引用,弱引用,虛引用處理
 * [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]
 * [GC Worker Total (ms): Min: 1.0, Avg: 1.1, Max: 1.1, Diff: 0.1, Sum: 4.2]
 * [GC Worker End (ms): Min: 168.1, Avg: 168.1, Max: 168.1, Diff: 0.0]
 * [Code Root Fixup: 0.0 ms]
 * [Code Root Purge: 0.0 ms]
 * [Clear CT: 0.1 ms]【清楚cardTable所花費(fèi)時(shí)間】
 * [Other: 0.3 ms]
 * [Choose CSet: 0.0 ms]
 * [Ref Proc: 0.1 ms]
 * [Ref Enq: 0.0 ms]
 * [Redirty Cards: 0.1 ms]
 * [Humongous Register: 0.0 ms]
 * [Humongous Reclaim: 0.0 ms]
 * [Free CSet: 0.0 ms]
 * [Eden: 2048.0K(4096.0K)->0.0B【新生代清理后】(2048.0K) Survivors: 0.0B->1024.0K Heap: 3800.2K(10.0M)->2752.1K(10.0M)]
 * [Times: user=0.00 sys=0.00, real=0.01 secs]
 * 2020-04-14T16:13:41.671+0800: [GC concurrent-root-region-scan-start]
 * 2020-04-14T16:13:41.671+0800: [GC concurrent-root-region-scan-end, 0.0008592 secs]
 * 2020-04-14T16:13:41.671+0800: [GC concurrent-mark-start]
 * 2020-04-14T16:13:41.672+0800: [GC concurrent-mark-end, 0.0000795 secs]
 * 2020-04-14T16:13:41.672+0800: [GC remark 2020-04-14T16:13:41.672+0800: [Finalize Marking, 0.0001170 secs] 2020-04-14T16:13:41.672+0800: [GC ref-proc, 0.0002159 secs] 2020-04-14T16:13:41.672+0800: [Unloading, 0.0005800 secs], 0.0011024 secs]
 * [Times: user=0.00 sys=0.00, real=0.00 secs]
 * 2020-04-14T16:13:41.673+0800: [GC cleanup 4800K->4800K(10M), 0.0003239 secs]
 * [Times: user=0.00 sys=0.00, real=0.00 secs]
 * hello world
 * Heap
 * garbage-first heap   total 10240K, used 4800K [0x00000000ff600000, 0x00000000ff700050, 0x0000000100000000)
 * region size 1024K【說明region默認(rèn)大小】, 2 young (2048K), 1 survivors (1024K)
 * Metaspace       used 3224K, capacity 4496K, committed 4864K, reserved 1056768K
 * class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
 */
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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