JVM G1(Garbage-First Garbage Collector)收集器全过程剖析

G1垃圾收集器的设计原则是“首先收集尽可能多的垃圾(Garbage First)”,目标是为了尽量缩短处理超大堆(超过4GB)产生的停顿。

因此,G1并不会等内存耗尽(比如Serial 串行收集器、Parallel并行收集器 )者快耗尽(CMS)的时候才开始垃圾回收,而是在内部采用了启发式算法,在老年代中找出具有高收集收益的分区(Region)进行收集。

同时 G1 可以根据用户设置的STW(Stop-The-World)停顿时间目标(响应时间优先)自动调整年轻代和总堆的大小,停顿时间越短年轻代空间就可能越小,总堆空间越大。

G1相对于CMS一个比较明显的优势是,内存碎片的产生率大大降低。

G1在 JDK7u4以上都可以使用,在JDK9开始,G1为默认的垃圾收集器,以替代CMS

G1算法

算法:三色标记 + SATB

G1的特性

  • 面向服务端应用的垃圾收集器
  • 并行与并发:G1能充分利用多CPU、多核环境使用多个CPU或CPU核心来缩短STW(Stop-The-World)停顿时间。
  • 分代收集:G1物理上不分代,但逻辑上仍然有分代的概念。
  • 空间整合:不会产生内存空间碎片,收集后可提供规整的可用内存,整理空闲空间更快。
  • 可预测的停顿(它可以有计划的避免在整个JAVA堆中进行全区域的垃圾收集)
  • 适用于不需要实现很高吞吐量的场景
  • JAVA堆内存布局与其它收集器存在很大差别,它将整个JAVA堆划分为多个大小相等的独立区域或分区(Region)。
  • G1收集器中,虚拟机使用Remembered Set来避免全堆扫描。

G1的内存模型

分区概念

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),

这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:

而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。

Region (区域,分区)

G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。

虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。

因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;

每个分区Region也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。

启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区。

Card (卡片)

在每个分区Region 内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度。

所有分区Region 的卡片将会记录在全局卡片表(Global Card Table)中。

分配的对象会占用物理上连续的若干个卡片。

当查找对分区Region 内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。

每次对内存的回收,都是对指定分区的卡片进行处理。

Heap (堆)

G1同样可以通过-Xms/-Xmx来指定堆空间大小。

当发生年轻代收集(YGC)或混合收集(Mixed GC)时,通过计算GC与应用的耗费时间比,自动调整堆空间大小。

如果GC频率太高,则通过增加堆尺寸,来减少GC频率,相应地GC占用的时间也随之降低;

目标参数-XX:GCTimeRatio即为GC与应用的耗费时间比,G1默认为12(JDK7,8为99,JDK11+开始为12),而CMS默认为99,因为CMS的设计原则是耗费在GC上的时间尽可能的少。

另外,当空间不足,如对象空间分配或转移失败时,G1会首先尝试增加堆空间,如果扩容失败,则发起担保的Full GC

Full GC后,堆尺寸计算结果也会调整堆空间。

分代概念

Generation (分代 )

分代垃圾收集可以将关注点集中在最近被分配的对象上,而无需整堆扫描,避免长命对象的拷贝,同时独立收集有助于降低响应时间。

虽然分区使得内存分配不再要求紧凑的内存空间,但G1依然使用了分代的思想。

与其他垃圾收集器类似,G1将内存在逻辑上划分为年轻代和老年代,其中年轻代又划分为Eden空间和Survivor空间。

但年轻代空间并不是固定不变的,当现有年轻代分区占满时,JVM会分配新的空闲分区加入到年轻代空间。

整个年轻代内存会在初始空间-XX:NewSize与最大空间-XX:MaxNewSize之间动态变化,且由参数目标暂停时间-XX:MaxGCPauseMillis、需要扩缩容的大小以及分区的已记忆集合(RSet)计算得到。

当然,G1依然可以设置固定的年轻代大小(参数-XX:NewRatio-Xmn),但同时暂停目标将失去意义。

Local allocation buffer (LAB) (本地分配缓冲)

值得注意的是,由于分区的思想,每个线程均可以"认领"某个分区Region用于线程本地的内存分配,而不需要顾及分区是否连续。

因此,每个应用线程和GC线程都会独立的使用分区,进而减少同步时间,提升GC效率,这个分区Region称为本地分配缓冲区(LAB)。

  • 应用线程本地缓冲区TLAB
    应用线程可以独占一个本地缓冲区(TLAB)来创建的对象,而大部分都会落入Eden区域(巨型对象或分配失败除外),因此TLAB的分区属于Eden空间;
  • GC线程本地缓冲区GCLAB
    每次垃圾收集时,每个GC线程同样可以独占一个本地缓冲区(GCLAB)用来转移对象,每次回收会将对象复制到Suvivor空间或老年代空间;
  • 晋升本地缓冲区PLAB
    对于从Eden/Survivor空间晋升(Promotion)到Survivor/老年代空间的对象,同样有GC独占的本地缓冲区进行操作,该部分称为晋升本地缓冲区(PLAB)。

分区模型

Humongous Object (巨型对象)

一个大小达到甚至超过分区Region 50%以上的对象称为巨型对象(Humongous Object)。
巨型对象会独占一个、或多个连续分区,其中第一个分区被标记为开始巨型(StartsHumongous),相邻连续分区被标记为连续巨型(ContinuesHumongous)。
Humongous Object 有以下特点:

  • Humongous Object直接分配到了 老年代,防止了反复拷贝移动。

当线程为巨型分配空间时,不能简单在TLAB进行分配,因为巨型对象的移动成本很高,而且有可能一个分区不能容纳巨型对象。
因此,巨型对象会直接在老年代分配,所占用的连续空间称为巨型分区(Humongous Region)。

  • Humongous ObjectYGC阶段, Global Concurrent Marking 阶段的 CleanupFGC 阶段 回收。

由于无法享受LAB带来的优化,并且确定一片连续的内存空间需要扫描整堆Heap,因此确定巨型对象开始位置的成本非常高,如果可以,应用程序应避免生成巨型对象。

  • 在分配Humongous Object 之前先检查是否超过 initiating heap occupancy percent (由参数-XX:InitiatingHeapOccupancyPercent控制) 和 the marking threshold。
    如果超过的话,就启动并发收集周期Concurrent Marking Cycle ,为的是提早回收,防止 Evacuation FailureFull GC
RSetRemember Set,已记忆集合)

在串行和并行收集器中,GC通过整堆扫描,来确定对象是否处于可达路径中。

然而G1为了避免STW式的整堆Heap扫描,在每个分区Region记录了一个已记忆集合(RSet),内部类似一个反向指针,记录引用分区Region内对象的卡片Card的索引。

当要回收该分区Region时,通过扫描分区的RSet,来确定引用本分区内的对象是否存活,进而确定本分区内的对象存活情况。

事实上,并非所有的引用都需要记录在RSet中,如果一个分区Region确定需要扫描,那么无需RSet也可以无遗漏的得到引用关系。

那么引用源自本分区Region的对象,当然不用落入RSet中;

同时,G1 GC每次都会对年轻代进行整体收集,因此引用源自年轻代的对象,也不需要在RSet记录。

最后只有老年代的分区Region可能会有RSet记录,这些分区称为拥有RSet分区(an RSet’s owning region)。

Per Region Table (PRT)

RSet在内部使用Per Region Table(PRT)记录分区Region的引用情况。
由于RSet的记录要占用分区Region的空间,如果一个分区非常"受欢迎",那么RSet占用的空间会上升,从而降低分区Region的可用空间。
G1应对这个问题采用了改变RSet的密度的方式,在PRT中将会以三种模式记录引用:

  • 稀少:直接记录引用对象的卡片Card的索引
  • 细粒度:记录引用对象的分区Region的索引
  • 粗粒度:只记录引用情况,每个分区对应一个比特位

由上可知,粗粒度的PRT只是记录了引用数量,需要通过整堆Heap扫描才能找出所有引用,因此扫描速度也是最慢的。

CSetCollection Set,收集集合)

收集集合(CSet)代表每次GC暂停时回收的一系列目标分区Region

在任意一次收集暂停中,CSet所有分区都会被释放,内部存活的对象都会被转移到分配的空闲分区中。

因此无论是年轻代收集,还是混合收集,工作的机制都是一致的。

年轻代收集(YGC)的CSet只容纳年轻代分区,而混合收集(Mixed GC)会通过启发式算法,在老年代候选回收分区中,筛选出回收收益最高的分区添加到CSet中。

  • 候选老年代分区的CSet准入条件,可以通过活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%)进行设置,从而拦截那些回收开销巨大的对象;

  • 同时,每次混合收集可以包含候选老年代分区,可根据CSet对堆的总大小占比-XX:G1OldCSetRegionThresholdPercent(默认10%)设置数量上限。

由上述可知,G1的收集都是根据CSet进行操作的,年轻代收集(YGC)与混合收集(Mixed GC)没有明显的不同,最大的区别在于两种收集的触发条件。

年轻代收集集合 CSet of Young Collection

应用线程不断活动后,年轻代空间会被逐渐填满。当JVM分配对象到Eden区域失败(Eden区已满)时,便会触发一次STW式的年轻代收集。
在年轻代收集中,Eden分区存活的对象将被拷贝到Survivor分区;
原有Survivor分区存活的对象,将根据任期阈值(tenuring threshold)分别晋升到PLAB中,新的survivor分区和老年代分区。而原有的年轻代分区将被整体回收掉。

同时,年轻代收集还负责维护对象的年龄(存活次数),辅助判断老化(tenuring)对象晋升的时候是到Survivor分区还是到老年代分区。
年轻代收集首先先将晋升对象尺寸总和、对象年龄信息维护到年龄表中,再根据年龄表、Survivor尺寸、Survivor填充容量-XX:TargetSurvivorRatio(默认50%)、最大任期阈值-XX:MaxTenuringThreshold(默认15),计算出一个恰当的任期阈值,凡是超过任期阈值的对象都会被晋升到老年代。

混合收集集合 CSet of Mixed Collection

年轻代收集不断活动后,老年代的空间也会被逐渐填充。当老年代占用空间超过整堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1就会启动一次混合垃圾收集周期。

为了满足暂停目标,G1可能不能一口气将所有的候选分区收集掉,因此G1可能会产生连续多次的混合收集与应用线程交替执行,每次STW的混合收集与年轻代收集过程相类似。

  • 为了确定包含到年轻代收集集合CSet的老年代分区,JVM通过参数混合周期的最大总次数-XX:G1MixedGCCountTarget(默认8)、堆废物百分比-XX:G1HeapWastePercent(默认5%)。

通过候选老年代分区总数与混合周期最大总次数,确定每次包含到CSet的最小分区数量;

根据堆废物百分比,当收集达到参数时,不再启动新的混合收集。而每次添加到CSet的分区,则通过计算得到的GC效率进行安排。

G1的活动周期

G1的垃圾回收包括了以下几种:

  • Concurrent Marking Cycle (并发收集)
    类似 CMS的并发收集过程。

  • Young Collection (YGC,年轻代收集,STW

  • Mixed Collection Cycle (混合收集,STW

  • Full GC(FGC, STW
    JDK10以前FGC是串行回收,JDK10+可以是并行回收。

并发标记周期 Concurrent Marking Cycle

并发标记周期是G1中非常重要的阶段,这个阶段将会为混合收集周期识别垃圾最多的老年代分区。

整个周期完成根标记、识别所有(可能)存活对象,并计算每个分区的活跃度,从而确定GC效率等级。

当达到IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,便会触发并发标记周期。

整个并发标记周期将由初始标记(Initial Mark)、根分区扫描(Root Region Scanning)、并发标记(Concurrent Marking)、重新标记(Remark)、清除(Cleanup)几个阶段组成。

其中,初始标记(随年轻代收集一起活动)、重新标记、清除是STW的,而并发标记如果来不及标记存活对象,则可能在并发标记过程中,G1又触发了几次年轻代收集(YGC)。

Initial Marking (初始标记, STW)

它标记了从GC Root开始直接可达的对象。

事实上,当达到IHOP阈值时,G1并不会立即发起并发标记周期,而是等待下一次年轻代收集,利用年轻代收集的STW时间段,完成初始标记,这种方式称为借道(Piggybacking)。

Root region scanning (根分区扫描)

在初始标记暂停结束后,年轻代收集也完成的对象复制到Survivor的工作,应用线程开始活跃起来。此时为了保证标记算法的正确性,所有新复制到Survivor分区的对象,都需要被扫描并标记成根,这个过程称为根分区扫描(Root Region Scanning),同时扫描的Suvivor分区也被称为根分区(Root Region)。

Concurrent Marking(并发标记)

这个阶段从GC Root开始对heap中的对象标记,标记线程与应用程序线程并行执行,并且收集各个Region的存活对象信息。
和应用线程并发执行,并发标记线程在并发标记阶段启动,由参数-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4)控制启动数量,
每个线程每次只扫描一个分区Region,从而标记出存活对象图。

所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经历了几次年轻代收集。
如果堆满前没有完成标记任务,则会触发担保机制,经历一次长时间的串行Full GC。

Remark ( 重新标记,STW)

标记那些在并发标记阶段发生变化的对象,将被回收。
这个阶段也是并行执行的,通过参数-XX:ParallelGCThread可设置GC暂停时可用的GC线程数。

Cleanup (清理,STW)

清除阶段主要执行以下操作:

  • RSet梳理,启发式算法会根据活跃度和RSet尺寸对分区定义不同等级,同时RSet数理也有助于发现无用的引用。参数-XX:+PrintAdaptiveSizePolicy可以开启打印启发式算法决策细节;
  • 整理堆分区,为混合收集周期识别回收收益高(基于释放空间和暂停目标)的老年代分区集合;
  • 识别所有空闲分区,即发现无存活对象的分区。该分区可在清除阶段直接回收,无需等待下次收集周期。

年轻代收集 Young Collection /混合收集周期 Mixed Collection Cycle

当应用运行开始时,堆内存可用空间还比较大,只会在年轻代满时,触发年轻代收集;

随着老年代内存增长,当到达IHOP阈值-XX:InitiatingHeapOccupancyPercent(老年代占整堆比,默认45%)时,G1开始着手准备收集老年代空间。

首先经历并发标记周期 Concurrent Marking Cycle,识别出高收益的老年代分区,前文已述。

但随后G1并不会马上开始一次混合收集,而是让应用线程先运行一段时间,等待触发一次年轻代收集。

在这次STW中,G1将保准整理混合收集周期。接着再次让应用线程运行,当接下来的几次年轻代收集时,将会有老年代分区加入到CSet中,

即触发混合收集,这些连续多次的混合收集称为混合收集周期(Mixed Collection Cycle)。

年轻代收集 Young Collection,YGC

每次收集过程中,既有并行执行的活动,也有串行执行的活动,但都可以是多线程的。

在并行执行的任务中,如果某个任务过重,会导致其他线程在等待某项任务的处理,需要对这些地方进行优化。

以下部分部分可以结合日志查看

  • 并行活动

    • 外部根分区扫描 Ext Root Scanning:
      此活动对堆外的根(JVM系统目录、VM数据结构、JNI线程句柄、硬件寄存器、全局变量、线程对栈根)进行扫描,发现那些没有加入到暂停收集集合CSet中的对象。如果系统目录(单根)拥有大量加载的类,最终可能其他并行活动结束后,该活动依然没有结束而带来的等待时间。

    • 更新已记忆集合 Update RS:
      并发优化线程会对脏卡片的分区进行扫描更新日志缓冲区来更新RSet,但只会处理全局缓冲列表。作为补充,所有被记录但是还没有被优化线程处理的剩余缓冲区,会在该阶段处理,变成已处理缓冲区(Processed Buffers)。为了限制花在更新RSet的时间,可以设置暂停占用百分比-XX:G1RSetUpdatingPauseTimePercent(默认10%,即-XX:MaxGCPauseMills/10)。值得注意的是,如果更新日志缓冲区更新的任务不降低,单纯地减少RSet的更新时间,会导致暂停中被处理的缓冲区减少,将日志缓冲区更新工作推到并发优化线程上,从而增加对Java应用线程资源的争夺。

    • RSet扫描 Scan RS:
      在收集当前CSet之前,考虑到分区外的引用,必须扫描CSet分区的RSet。如果RSet发生粗化,则会增加RSet的扫描时间。
      开启诊断模式-XX:UnlockDiagnosticVMOptions后,
      通过参数-XX:+G1SummarizeRSetStats可以确定并发优化线程是否能够及时处理更新日志缓冲区,并提供更多的信息,来帮助为RSet粗化总数提供窗口。
      参数-XX:G1SummarizeRSetStatsPeriod=n可设置RSet的统计周期,即经历多少此GC后进行一次统计

    • 代码根扫描 Code Root Scanning:对代码根集合进行扫描,扫描JVM编译后代码Native Method的引用信息(nmethod扫描),进行RSet扫描。事实上,只有CSet分区中的RSet有强代码根时,才会做nmethod扫描,查找对CSet的引用。

    • 转移和回收 Object Copy:
      通过选定的CSet以及CSet分区完整的引用集,将执行暂停时间的主要部分:CSet分区存活对象的转移、CSet分区空间的回收。通过工作窃取机制来负载均衡地选定复制对象的线程,并且复制和扫描对象被转移的存活对象将拷贝到每个GC线程分配缓冲区GCLAB。G1会通过计算,预测分区复制所花费的时间,从而调整年轻代的尺寸。

    • 终止 Termination:
      完成上述任务后,如果任务队列已空,则工作线程会发起终止要求。如果还有其他线程继续工作,空闲的线程会通过工作窃取机制尝试帮助其他线程处理。而单独执行根分区扫描的线程,如果任务过重,最终会晚于终止。

    • GC外部的并行活动 GC Worker Other:
      该部分并非GC的活动,而是JVM的活动导致占用了GC暂停时间(例如JNI编译)。

  • 串行活动

    • 代码根更新 Code Root Fixup:根据转移对象更新代码根。

    • 代码根清理 Code Root Purge:清理代码根集合表。

    • 清除全局卡片标记 Clear CT:在任意收集周期会扫描CSet与RSet记录的PRT,扫描时会在全局卡片表中进行标记,防止重复扫描。在收集周期的最后将会清除全局卡片表中的已扫描标志。

    • 选择下次收集集合 Choose CSet:该部分主要用于并发标记周期后的年轻代收集、以及混合收集中,在这些收集过程中,由于有老年代候选分区的加入,往往需要对下次收集的范围做出界定;但单纯的年轻代收集中,所有收集的分区都会被收集,不存在选择。

    • 引用处理 Ref Proc:主要针对软引用、弱引用、虚引用、final引用、JNI引用。当Ref Proc占用时间过多时,可选择使用参数-XX:ParallelRefProcEnabled激活多线程引用处理。G1希望应用能小心使用软引用,因为软引用会一直占据内存空间直到空间耗尽时被Full GC回收掉;即使未发生Full GC,软引用对内存的占用,也会导致GC次数的增加。

    • 引用排队 Ref Enq:此项活动可能会导致RSet的更新,此时会通过记录日志,将关联的卡片标记为脏卡片。

    • 卡片重新脏化 Redirty Cards:重新脏化卡片。

    • 回收空闲巨型分区 Humongous Reclaim:G1做了一个优化:通过查看所有根对象以及年轻代分区的RSet,如果确定RSet中巨型对象没有任何引用,则说明G1发现了一个不可达的巨型对象,该对象分区会被回收。

    • 释放分区 Free CSet:回收CSet分区的所有空间,并加入到空闲分区中。

    • 其他活动 Other:GC中可能还会经历其他耗时很小的活动,如修复JNI句柄等。

并发标记周期后的年轻代收集 Young Collection Following Concurrent Marking Cycle

当G1发起并发标记周期之后,并不会马上开始混合收集。
G1会先等待下一次年轻代收集,然后在该收集阶段中,确定下次混合收集的CSet(Choose CSet)。

混合收集周期 Mixed Collection Cycle, Mixed GC

单次的混合收集与年轻代收集并无二致。

根据暂停目标,老年代的分区可能不能一次暂停收集中被处理完,G1会发起连续多次的混合收集,称为混合收集周期(Mixed Collection Cycle)。

G1会计算每次加入到CSet中的分区数量、混合收集进行次数,并且在上次的年轻代收集、以及接下来的混合收集中,G1会确定下次加入CSet的分区集(Choose CSet),并且确定是否结束混合收集周期。

转移失败的担保机制 Full GC

转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程(JDK10支持多线程)的Full GC。

Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。参数-XX:G1ReservePercent(默认10%)可以保留空间,来应对晋升模式下的异常情况,最大占用整堆50%,更大也无意义。

G1在以下场景中会触发Full GC,同时会在日志中记录to-space exhausted以及Evacuation Failure

  • 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
  • 从老年代分区转移存活对象时,无法找到可用的空闲分区
  • 分配巨型对象Humongous Object 时在老年代无法找到足够的连续分区

由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。

问题

  • 什么时候触发concurrent marking ?
# 启动并发周期 Concurrent Marking Cycle (以及后续的混合周期 MixedGC)时的堆内存占用百分比. G1用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比例。默认45%
# 当堆存活对象占用堆的45%,就会启动G1 中并发标记周期 Concurrent Marking Cycle
-XX:InitiatingHeapOccupancyPercent
  • 什么时候发生Mixed GC?
    concurrent marking 主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。

    由一些参数控制,另外也控制着哪些老年代Region会被选入CSet(收集集合)。

# 一次 concurrent marking之后,最多执行Mixed GC的次数(默认8)
-XX:G1MixedGCCountTarget
# 堆废物百分比(默认5%),在每次YGC之后和再次发生Mixed GC之前,会检查垃圾占比是否达到此参数,只有达到了,下次才会发生Mixed GC。
-XX:G1HeapWastePercent
# old generation region中的存活对象的占比,只有在此参数之下,才会被选入CSet。
-XX:G1MixedGCLiveThresholdPercent
# 一次Mixed GC中能被选入CSet的最多old generation region数量。
-XX:G1OldCSetRegionThresholdPercent

GC日志详解

并发标记周期 Concurrent Marking Cycle
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0094252 secs]
# 根分区扫描,可能会被 YGC 打断,那么结束就是如:[GC pause (G1 Evacuation Pause) (young)[GC concurrent-root-region-scan-end, 0.0007157 secs]
[GC concurrent-mark-start]
[GC concurrent-mark-end, 0.0203881 secs]
# 并发标记阶段
[GC remark [Finalize Marking, 0.0007822 secs] [GC ref-proc, 0.0005279 secs] [Unloading, 0.0013783 secs], 0.0036513 secs]
#  重新标记,STW
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC cleanup 13985K->13985K(20480K), 0.0034675 secs]
 [Times: user=0.00 sys=0.00, real=0.00 secs] 
# 清除
年轻代收集 YGC
[GC pause (G1 Evacuation Pause) (young), 0.0022483 secs]
# young -> 年轻代      Evacuation-> 复制存活对象 
   [Parallel Time: 1.0 ms, GC Workers: 10] # 并发执行的GC线程数,以下阶段是并发执行的
      [GC Worker Start (ms): Min: 109.0, Avg: 109.1, Max: 109.1, Diff: 0.2] 
      [Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 2.3] # 外部根分区扫描
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] # 更新已记忆集合 Update RSet,检测从年轻代指向老年代的对象
         [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]# RSet扫描
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] # 代码根扫描
      [Object Copy (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 3.5] # 转移和回收,拷贝存活的对象到survivor/old区域
      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] # 完成上述任务后,如果任务队列已空,则工作线程会发起终止要求。
         [Termination Attempts: Min: 1, Avg: 5.8, Max: 9, Diff: 8, Sum: 58]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] # GC外部的并行活动,该部分并非GC的活动,而是JVM的活动导致占用了GC暂停时间(例如JNI编译)。
      [GC Worker Total (ms): Min: 0.5, Avg: 0.6, Max: 0.7, Diff: 0.2, Sum: 5.9]
      [GC Worker End (ms): Min: 109.7, Avg: 109.7, Max: 109.7, Diff: 0.0]
   [Code Root Fixup: 0.0 ms] # 串行任务,根据转移对象更新代码根
   [Code Root Purge: 0.0 ms] #串行任务, 代码根清理
   [Clear CT: 0.5 ms] #串行任务,清除全局卡片 Card Table 标记
   [Other: 0.8 ms]
      [Choose CSet: 0.0 ms] # 选择下次收集集合  CSet
      [Ref Proc: 0.4 ms] # 引用处理 Ref Proc,处理软引用、弱引用、虚引用、final引用、JNI引用
      [Ref Enq: 0.0 ms] # 引用排队 Ref Enq
      [Redirty Cards: 0.3 ms] # 卡片重新脏化 Redirty Cards:重新脏化卡片
      [Humongous Register: 0.0 ms] 
      [Humongous Reclaim: 0.0 ms] # 回收空闲巨型分区 Humongous Reclaim,通过查看所有根对象以及年轻代分区的RSet,如果确定RSet中巨型对象没有任何引用,该对象分区会被回收。
      [Free CSet: 0.0 ms]  # 释放分区 Free CSet
   [Eden: 12288.0K(12288.0K)->0.0B(11264.0K) Survivors: 0.0B->1024.0K Heap: 12288.0K(20480.0K)->832.0K(20480.0K)]
 [Times: user=0.01 sys=0.00, real=0.00 secs] 
# 从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
# 从老年代分区转移存活对象时,无法找到可用的空闲分区 这两种情况之一导致的 YGC
[GC pause (G1 Evacuation Pause) (young) (to-space exhausted), 0.0916534 secs]
# 并发标记周期 Concurrent Marking Cycle 中的 根分区扫描阶段,被 YGC中断
[GC pause (G1 Evacuation Pause) (young)[GC concurrent-root-region-scan-end, 0.0007157 secs]
混合收集周期 Mixed Collection Cycle, Mixed GC
# 并发标记周期 Concurrent Marking Cycle 的开始
[GC pause (G1 Evacuation Pause) (young) (initial-mark) , 0.0443460 secs]
Full GC
[Full GC (Allocation Failure) 20480K->9656K(20480K), 0.0189481 secs]
   [Eden: 0.0B(1024.0K)->0.0B(5120.0K) Survivors: 0.0B->0.0B Heap: 20480.0K(20480.0K)->9656.8K(20480.0K)], [Metaspace: 4960K->4954K(1056768K)]
 [Times: user=0.03 sys=0.00, real=0.02 secs] 

参考资料

  • https://www.oracle.com/technical-resources/articles/java/g1gc.html
  • JDK8 G1: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html
  • Other Blog:
    • https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All/
    • https://tech.meituan.com/2016/09/23/g1.html
    • https://www.infoq.com/articles/tuning-tips-G1-GC/
    • https://blog.csdn.net/coderlius/article/details/79272773
    • https://www.cnblogs.com/webor2006/p/11146273.html
    • https://www.cnblogs.com/webor2006/p/11147545.html
    • [1] Charlie H, Monica B, Poonam P, Bengt R. Java Performance Companion
    • [2] 周志明. 深入理解JVM虚拟机

by Sven Augustus https://my.oschina.net/langxSpirit

本文由博客群发一文多发等运营工具平台 OpenWrite 发布

你可能感兴趣的:(JVM G1(Garbage-First Garbage Collector)收集器全过程剖析)