G1 GC垃圾收集流程

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了G1 GC垃圾收集流程相关的知识,希望对你有一定的参考价值。

参考技术A

从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为:

YoungGC 触发时机
在分配一般对象(非巨型对象)时,当所有 eden region 使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次young gc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。因为YoungGC会进行根扫描,所以会 stop the world 。

YoungGC的回收过程如下
1.根扫描root scan,跟CMS类似, Stop the world ,扫描GC Roots对象。
2.处理Dirty card,更新RSet.
3.扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。
4.拷贝扫描出的存活的对象到survivor2/old区
5.处理引用队列,软引用,弱引用,虚引用

值得高兴的情况是, cleanup 可以释放老年代的整个regions. 但是并不是每次都是这样, 当并发标记成功结束后,G1会预定一个混合的收集来收集年轻代regions的垃圾,也会在收集集合中加入一部分老年代的regions.

一个混合的Evacuation Pause并不总是并发标记阶段结束后立即开始. 有一系列的规则和启发式算法来决定这个. 比如, 可以释放掉老年代的一大部分空间, 那么就没必要做这个了.

因此,就是在并发标记结束和混合Evacuation Pause之间加入很多fully-young的Evacuation Pause.

具体放入收集集合的老年代区的region,以及它们被加入的顺序都基于一系列的规则选择出来的. 这些规则包括:应用设定的软的实时性能指标, 存活统计以及并发标记阶段垃圾回收的效率, 还有一系列可配的JVM 选项. 混合式收集大体上与我们前面看到fully-young相同, 但这次我们讲到新的对象 remembered sets .

remembered sets 用来支持在不同heap regions上的独立收集. 比如当收集region A,B,C, 我们只需要知道从region D和E中是否有引用到它们来决定它们的存活性.因为遍历整个堆会消耗很久的时间并且打破了我们增量收集的意义, 所以在G1中也采用了与在其他算法中采用Card Table来独立收集年轻代区域类似的优化算法, 叫做remember sets.

如下图所示, 每个region都有一个RSet保存从其他region到这个region中对象的引用. 这些对象会被当做额外的GC roots. 注意在并发标记阶段, 老年代被认为是垃圾的对象会被忽略, 即便有外部对象还在引用它们, 因为它们的对象也会被当做垃圾.

接下来发生的与其他收集器类似:多个并行的GC线程会找出哪些是存活的哪些是垃圾. 最后, 所有存活对象会被移动到survivor区(如有必要创建新的).所有的空region会被释放后被用来存放对象. [[图片上传失败.
为了在应用程序运行期间维护RSets, 任何时候对域的更新都会触发一个Post-Write屏障. 如果关联的引用是跨region的, 比如从一个region到另一个region,一个对应的记录也会在目标region的RSet中添加. 将记录(cards)加入到RSet是异步的并应用了很多优化.简单来说它用Write屏障来将脏记录放到本地buffer中, 一个特殊的GC线程会选择这些记录,然后传播信息给其他region的RSet.

Young GC发生的时机大家都知道,那什么时候发生Mixed GC呢?其实是由一些参数控制着的,另外也控制着哪些老年代Region会被选入CSet。

gc_handbook_zh

第一阶段initial mark是共用了Young GC的暂停,这是因为他们可以复用root scan操作,所以可以说global concurrent marking是伴随Young GC而发生的。
第四阶段Cleanup只是回收了没有 任何存活对象的Region ,所以它并不需要STW

图是来自 oracle 上对 gc 周期的描述,实心圆都表示一次 GC 停顿

除了以上的参数,G1 GC相关的其他主要的参数有:
参数 含义
-XX:G1HeapRegionSize=n 设置Region大小,并非最终值
-XX:MaxGCPauseMillis 设置G1收集过程目标时间,默认值200ms,不是硬性条件
-XX:G1NewSizePercent 新生代最小值,默认值5%
-XX:G1MaxNewSizePercent 新生代最大值,默认值60%
-XX:ParallelGCThreads STW期间,并行GC线程数
-XX:ConcGCThreads=n 并发标记阶段,并行执行的线程数
-XX:InitiatingHeapOccupancyPercent 设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous
-XX:G1ReservePercent,通过-XX:G1ReservePercent指定G1为分配担保预留的空间比例,默认10%。也就是老年代会预留10%的空间来给新生代的对象晋升,如果经常发生新生代晋升失败而导致Full GC,那么可以适当调高此阈值。但是调高此值同时也意味着降低了老年代的实际可用空间
-XX:G1HeapWastePercent
通过-XX:G1HeapWastePercent指定触发Mixed GC的堆垃圾占比,默认值5%,也就是在全局标记结束后能够统计出所有Cset内可被回收的垃圾占整对的比例值,如果超过5%,那么就会触发之后的多轮Mixed GC,如果不超过,那么会在之后的某次Young GC中重新执行全局并发标记。可以尝试适当的调高此阈值,能够适当的降低Mixed GC的频率

其他参数

因此AlwaysPreTouch,JVM就会先访问所有分配给它的内存,让操作系统把内存真正的分配给JVM.后续JVM就可以顺畅的访问内存了

关于AlwaysPreTouch找了一些资料,这个参数属于比较偏门的优化项
JAVA进程启动的时候,虽然我们可以为 JVM 指定合适的内存大小,但是这些内存操作系统并没有真正的分配给JVM,而是等JVM访问这些内存的时候,才真正分配,这样会造成以下问题:

配置-XX:+AlwaysPreTouch参数可以优化这个问题,不过这个参数也有副作用,它会影响启动时间,那影响到底有多大呢?请接着往下看。

配置这个参数后这么耗时其中一个原因是,这个特性在JDK8版本以前都不是并行处理的,到了JDK9才是并行。可以戳链接Parallelize Memory Pretouch: https://bugs.openjdk.java.net/browse/JDK-815795

配置-XX:+AlwaysPreTouch参数后,JVM进程启动时间慢了几个数量级的根本原因呢?

在没有配置-XX:+AlwaysPreTouch参数即默认情况下,JVM参数-Xms申明的堆只是在虚拟内存中分配,而不是在物理内存中分配:它被以一种内部数据结构的形式记录,从而避免被其他进程使用这些内存。这些内存页直到被访问时,才会在物理内存中分配。当JVM需要内存的时候,操作系统将根据需要分配内存页。

配置-XX:+AlwaysPreTouch参数后,JVM将-Xms指定的堆内存中每个字节都写入’0’,这样的话,除了在虚拟内存中以内部数据结构保留之外,还会在物理内存中分配。并且由于touch这个行为是单线程的,因此它将会让JVM进程启动变慢。所以,要么选择减少接下来对每个缓存页的第一次访问时间,要么选择减少JVM进程启动时间,这是一种trade-off。

在某些情况下,G1触发了Full GC,这时G1会退化使用Serial收集器来完成垃圾的清理工作,它仅仅使用单线程来完成GC工作,GC暂停时间将达到秒级别的。整个应用处于假死状态,不能处理任何请求,我们的程序当然不希望看到这些。那么发生Full GC的情况有哪些呢?

并发模式失败
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。

晋升失败或者疏散失败
G1在进行GC的时候没有足够的内存供存活对象或晋升对象使用,由此触发了Full GC。可以在日志中看到(to-space exhausted)或者(to-space overflow)。解决这种问题的方式是:

巨型对象分配失败
当巨型对象找不到合适的空间进行分配时,就会启动Full GC,来释放空间。这种情况下,应该避免分配大量的巨型对象,增加内存或者增大-XX:G1HeapRegionSize,使巨型对象不再是巨型对象。

参考

分享文档
Java Hotspot G1 GC的一些关键技术- 美团
Plumbr Handbook Java Garbage Collection.pdf 翻译
G1GC 概念与性能调优- oppo
hbase G1

https://www.jianshu.com/p/5d4e319582f7

GC原理---垃圾收集器

垃圾收集器

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现
Serial收集器
  • 串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
  • 参数控制:-XX:+UseSerialGC 串行收集器
  • 技术图片
ParNew收集器
  • ParNew收集器其实就是Serial收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
  • 参数控制:-XX:+UseParNewGC ParNew收集器
    -XX:ParallelGCThreads 限制线程数量
  • 技术图片
Parallel Scavenge收集器
  • Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;
  • 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效地利用CPU时间,主要适合在后台运算而不需要太多交互的任务。
  • Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis以及直接设置吞吐量大小的-XX:GCTimeRatio。
  • 新生代复制算法、老年代标记-压缩
  • 参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
Parallel Old 收集器
  • Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
  • 参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
CMS收集器
  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
  • 目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
  • 从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
    • 初始标记(CMS initial mark)
      并发标记(CMS concurrent mark)
      重新标记(CMS remark)
      并发清除(CMS concurrent sweep)
  • 其中初始标记、重新标记这两个步骤仍然需要“Stop The World”(STW)。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
  • 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)
  • 优点:并发收集、低停顿
  • 缺点:产生大量空间碎片、并发阶段会降低吞吐量
  • .CMS收集器对CPU资源非常敏感。默认启动的回收线程数是(CPU+3)/4. 当CPU 4个以上时,并发回收垃圾收集线程不少于25%的CPU资源。
Serial Old收集器
  • Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器。给Client模式下的虚拟机使用。

  • 新生代采用复制算法,暂停所有用户线程;

  • 老年代采用标记-整理算法,暂停所有用户线程;

    G1收集器
  • 空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  • 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
  • 使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合
  • G1将新生代,老年代的物理空间划分取消了。
  • 在java 8中,持久代也移动到了普通的堆内存空间中,改为元空间。

jdk8环境下,默认使用 Parallel Scavenge(新生代)+ Serial Old(老年代)
  • java -XX:+PrintCommandLineFlags -version
  • 技术图片




以上是关于G1 GC垃圾收集流程的主要内容,如果未能解决你的问题,请参考以下文章

G1 垃圾收集器

G1 垃圾收集器

G1 垃圾收集器

G1垃圾收集器入门

JVMJVM内存结构之——垃圾收集器(并发与并行/ GC性能指标/ 垃圾收集器组合关系/ CMS收集器/ G1收集器)

JVM19_G1垃圾收集器概述特点常用参数Region详解记忆集与写屏障年轻代GC并发标记过程Mixed GCFull GC