看完这篇,G1垃圾收集器就差不多了~

基础概念

定义: Garbage First,垃圾优先,主要面向服务端应用的垃圾收集器。
开启命令: -XX:+UseG1GC
目标: “停顿时间模型”的收集器:能够支持指定所在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标。
适用场景: 适用于大内存、多CPU的机器。

设计理念

跳出之前要不收集新生代,要不收集老年代的樊笼,G1面向所有的堆内存任何部分来组成回收集进行回收,衡量标准不再是它属于哪个分代,而是哪块区域中存放的垃圾数量最多,回收效益最大。

区域划分

Region区域

定义: 将Java堆划分为多个大小相等的Region,每个Region都可以是新生代、老年代。G1收集器根据角色的不同采用不同的策略去处理
大小: 2 的N次幂,1MB~32MB
配置参数: -XX:G1HeapRegionSize,不指定G1会根据堆大小自行指定

Humongous区域(Region中的一部分)

定义: 专门用来存储大对象(超过Region容量一半的对象即为大对象),超过整个Region区域的会放在多个连续的Humongous区。
回收身份: G1把Humongous当做老年代的一部分
在这里插入图片描述

垃圾收集

借助R大的回答,从最高层看,G1的Collector一侧就是两个大部分,并且这两个部分可以相对独立执行。

  • 全局并发标记(global concurrent marking)
  • 拷贝存活对象(evacuation)

全局并发标记(global concurrent marking)

基于SATB(Snapshot At The Begining)形式的并发标记。

1、初始标记(STW

G1对根进行标记。扫描根集合,标记所有从根集合可直接到达的对象(CMS的初始标记也是类似)并将它们的字段压入扫描栈(marking stack)中等到后续扫描。G1使用外部的bitmap来记录mark信息,而不是用对象头的mark word里的mark bit。在分代式G1模式中,初始标记阶段借用 yong GC的暂停,因而没有额外的、单独的暂停阶段。

为什么初始标记阶段是借用Yong GC的暂停做的?

从逻辑上将“全局并发标记”和“拷贝存活对象”是相对独立的,但是“全局并发标记”阶段的“初始标记”阶段又和Yong GC要做的事情有重叠—遍历根集合,所以在实现上把他们安排在一起做,Yong GC期间可以顺带做,也可以不做。

2、并发标记

G1在整个堆中查找可以访问的(存活的)对象,递归扫描整个堆里的对象图。每扫描到一个对象就对其进行标记,并压入扫描栈中。重复扫描过程直到扫描栈清空。过程中还会扫描SATB 写屏障(write barrier)所记录下的引用,SATB相关下文会介绍。

3、最终/重新标记(STW

处理在并发标记阶段剩余未处理的SATB写屏障的记录。同时此阶段也进行弱引用处理(reference proccessing),**这个暂停与CMS的remark有一个本质的区别,这个暂停只需要扫描SATB buffer(将这些旧引用作为根重新扫描一遍,避免漏标),而CMS的remark需要重新扫描mod-union table里的dirty card外加整个根集合,**而此时整个Yong 区不管对象死活都会被当做根集合的一部分,因而CMS remark有可能会非常慢。

4、清理(cleanup)(STW)

清点和重置标记状态,与mark-sweep中的sweep阶段类似,但不是在堆上sweep实际对象,而是在marking bitmap里统计每个Region被标记为活的对象有多少。这个阶段如果发现完全没有活对象的Region,就会将其整体回收到可分配Region列表中(空闲列表)。

拷贝存活对象 (Evacuiation)(STW)

也叫筛选回收/清理(STW),负责把一部分Region里的活对象拷贝到空Region里去,然后回收原本的Region的空间。
此阶段可以自由选择任意多个Region来独立收集构成收集集合(Collection Set,CSet),靠每个Region的RSet实现。这是Regional garbage collector的特点。
确定完CSet后肯定就要复制了,其实就和ParallelScavenge的Young GC算法类似,采用并行复制算法把CSet里每个Region里的活对象拷贝到新的Region里,整个过程完全暂停。
“Garbage-First Garbage Collection”论文中讲到,CSet的选定完全靠统计模型找出收益最高、开销不超过用户指定的上限的若干个Region,由于每个Region都有RSet覆盖,要单独evacuate任意一个或者多个Region都没问题。


分代式G1模式下有两种选定CSet的子模式:

  • Yong GC:选定所有Yong区里的Region。通过控制Yong区的Region个数来控制GC的开销。
  • Mixex GC:选定所有Yong Gen里的Region,外加根据global concurrent marking统计得出收益最高的若干old区的Region。在用户指定的开销目标范围内尽可能选择收益高的old区Region

可以看到Yong区Region总是在CSet内,因此分代式G1不维护从Yong区Region出发的引用设计的RSet更新。

工作流程总结

分代式G1(只有分代式G1,其它的目前还没有)的正常工作流程就是在YongGC与Mixed GC之间视情况进行切换,背后定期做全局并发标记。初始化标记默认搭在YongGC上执行;
当全局并发标记正在工作时,G1不会选择做Mixed GC,反之MixedGC正在进行中G1也不会启动初始化标记。
在正常的工作流程中没有Full GC的概念,Old区的收集完全靠MixedGC来完成

问题

如何保证收集线程与用户线程互不干扰的运行?

算法实现细节中说过了“三色标记”算法,这个算法阐明了对象在垃圾收集过程中所有的状态,白、黑、灰。垃圾收集过程中,对象的状态可能会出现黑色对象引用了白色对象或者灰色对象与白色对象之间的引用断开了(当两种条件同时满足时就会出现漏标/错标的情况),其实也就是垃圾收集过程中原有的对象结构被打破了,解决这种情况的方案有两种:增量更新、原始快照,G1 GC使用的是原始快照(SATB),CMS使用的是增量更新(incremental update)

SATB( Snapshot At The Begining)

SATB是维持并发GC正确性的一个手段,抽象的说就是

  • 在一次GC开始的时候活的对象就被认为是活的,此时的对象图形成一个逻辑“快照”;
  • GC过程中新分配的对象都当做是活的,其他不可到达的对象就是死的。
G1如何知道哪些对象是GC开始后新分配的呢?

每个Region记录着两个TAMS(Top At Mark Start)指针,分别为prevTAMS和nextTAMS,G1在并发标记期间会让新分配的对象在TAMS上分配。在TAMS以上的对象就是新分配的,因而被视为隐式标记


G1的并发标记用了两个bitmap:

  • prevBitmap记录第n-1轮并发标记所得的对象存活状态,由于第n-1轮并发标记已经完成,这个bitmap的信息可以直接使用
  • nextBitmap记录第n轮concurrent marking的结果,这个bitmap是当前将要或者正在进行并发标记的结果,还不能使用

对应的每个Region都有这么几个指针:
在这里插入图片描述

top是该Region的当前分配指针,[bottom,top)是当前Region已用的部分,[top,end)是尚未使用的可分配空间。

  1. [bottom,preTAMS)这里的对象存活信息可以通过prevBitmap来得知
  2. [prevTAMS,nextTAMS)这部分里的对象在第n-1轮并发标记中隐式存活的
  3. [nextTAMS,top)这部分的对象是在第n轮并发标记中隐式存活的

G1如何处理在并发标记阶段用户线程对对象引用的修改呢?

SATB write barrier,是对“对引用类型字段赋值”这个动作的环切,也就是说赋值前后都在barrier覆盖的范畴内。在赋值前的部分叫做pre-write barrier,在赋值后的叫作post-write barrier。在JVM记忆集文章中我们也讲过,在G1 GC之前其他的垃圾收集器都只是使用了post-write barrier。


SATB要维持“在GC开始时活的对象”的状态这个逻辑snapshot。除了从root出发把整个对象图mark下来之外,其实只需要用pre-write barrier把每次引用关系变化时旧的引用值记下来就好了。这样等并发标记到某个对象时,这个对象的所有引用类型字段的变化全都有记录,就不会漏掉任何在snapshot里活着的对象。当然,很有可能有对象在snapshot中是活的,但是随着并发GC的进行,它已经死了但SATB还是会让它活过这次GC,这时候就会产生floal garbage.


因此在G1 GC中,整个write barrier+oop_field_store是这样的:

void oop_field_store(oop* field, oop new_value) {  
  pre_write_barrier(field);             // pre-write barrier: for maintaining SATB invariant  
  *field = new_value;                   // the actual store  
  post_write_barrier(field, new_value); // post-write barrier: for tracking cross-region reference  
}  

pre-write barrier的过程如下:

void pre_write_barrier(oop* field) {  
  oop old_value = *field;  
  if (old_value != null) {  
    if ($gc_phase == GC_CONCURRENT_MARK) { // SATB invariant only maintained during concurrent marking  
      $current_thread->satb_mark_queue->enqueue(old_value);  
    }  
  }  
}

enqueue动作的实际代码是在G1SATBCardTableModRefBS::enqueue(oop pre_val),它判断当前是否在并发标记阶段用的是JavaThread::satb_mark_queue_set().is_active(),SATBMarkQueueSet只有在并发标记阶段才会被置为active。
CMS的增量更新设计使得它在Remark阶段必须重新扫描所有线程栈和整个Yong区作为Root;而G1的SATB设计在Remark阶段则只需要扫描剩下的satb_mark_queue

为何在pre-write barrier中只是把旧的引用放入了SATBMarkQueue,为何没有压入标记栈中?

为了减少write barrier对用户线程性能的影响,G1将一部分原本要在barrier里做的事情挪到了别的线程中并发执行,实现这种分离的方式就是通过logging形式的 write barrier实现的,用户线程只在barrier里把要做的事情信息记录到一个队列中,由另外的线程从队列中取出信息批量完成剩余的动作。

以SATB write barrier为例,每个Java线程由一个独立的、定长的SATBMarkQueue,用户线程在barrier里只把old_value压入该队列中。一个队列满了之后,这个队列就会被加到全局的SATB队列集合SATBMarkQueueSet里等待处理,然后给对应的Java线程换一个新的、干净的队列继续执行下去。
并发标记过程中会定期检查全局SATB队列集合的大小。当SATBMarkQueueSet****中队列数量超过一定阈值后,并发标记线程就会处理集合里所有的队列,****把队列里记录的每个对象都标记上,并将其引用字段压到标记栈上等后边做进一步标记。
在这里插入图片描述

跨Region引用如何解决?

JVM使用记忆集(Remember Set)来避免全堆最为GC Roots扫描,关于记忆集的内容前边的文章中已经讲过,如果你已经忘记了可以返回去看看,JVM记忆集相关。

Remember Set

G1的堆与其它GC一样有一个覆盖整个堆的cart table,从逻辑上说,G1的RSet是应该每个Region都有一份,这个RSet记录的是从别的Region指向该Region的card。所以说这是一种“ponits-into”的RSet。
用card table实现的RSet通常是points-out的,也是就是说card table要记录的是从它覆盖的范围出发指向别的范围的指针,以分代式GC的card table为例,要记录old区指向yong区的跨代指针,被标记的card是old区范围内的。


G1 GC在ponits-out的card table之上再加了一层结构来构成points-into RSet:每个Region会记录下到底哪些别的Region有指向自己的指针,而这些指针分别在哪些card的范围内。这个RSet其实就是一个Hash Table,key是别的Region的起始地址,value是一个集合,里面的元素是card table的index,card table又对应一个card page。下图形象的秒数了points-into RSet的关系,原文地址为Tips for Tuning the Garbage First Garbage Collector
在这里插入图片描述

举例:
如果Region A的RSet中有一个key是Region B,value里有index为1234的card,那么它的意思就是RegionB的一个card里有引用指向Region A。所以对A来说,该RSet记录的是points-into的关系,而card table仍然记录了points-out的关系(不要太纠结points-into和points-out)。

缺点

G1的Region区域过多会导致G1收集器比其他收集器占有的内存多,据经验,G1收集器要耗费大约相当于Java堆内存10%到20%的额外内存来维持工作。

RSet是如何被更新的

前边也已经讲过了,通过write-barrier来实现,G1通过post-write barrier来更新RSet。
理论上post-write barrier的逻辑

  1. 首先会先获取指向该字段卡卡页
  2. 判断卡页是否已经被标记为脏页,如果已经为脏页,则不处理
  3. 将当前卡页变“脏”,以防多个字段同属于一个卡页重复执行此逻辑
  4. 判断是否为Yong 区,如果为Yong区则不处理,前边已经讲过了分代式G1下,Yong GC和Mixed GC都会的处理Yong GC,因此过滤到从Yong区出发的引用涉及的RSet的维护(既然GC都会扫描,干嘛还浪费时间区更新呢
  5. 找到卡页所属的Region
  6. 找到堆中引用Region的Region
  7. 更新cart table及Region的RSet

实际上post-write barrier也利用了前边讲SATB中的logging barrier,与SATBMarkQueue类似,每个Java线程由一个DirtyCardQueue,有一个全局的DirtyCardQueueSet,更新RSet的动作交由多个ConcurrentG1RefineThread并发完成,当全局集合中的队列个数超过指定阈值后,ConcurrentG1RefineThread就会取出若干个队列,遍历每个队列记录的card并将card加到对应的Region的RSet里去。

如何建立起可靠的停顿预测模型?

可以通过-XX:MaxGCPauseMillis参数指定预期的停顿时间,G1 GC的停顿预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。“衰减平均值”是指它会比普通的平均值更容易受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。也就是说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益。

优点

1、G1收集器并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐量。设计理念与之前的垃圾收集器也不相同,由以前追求一次把整个Java堆全部清理干净到追求能够应付应用的内存分配速率。
2、可指定期望停顿时间
3、不会产生空间碎片,在程序为大对象分配内存时不容易出现找不到连续内存而提前触发下一次GC或者Full GC
4、处理跨代引用时使用原始快照,避免了在重标记阶段像CMS一样扫描所有的“脏”页,只需要扫描SATB buffer中的引用即可。

缺点

  1. 如果Mixed GC无法跟上程度分配内存的速度,导致Old区域填满无法分配内存时,就会切换到G1之外的Serial Old GC来收集整个Java堆(包括Yong、OId、Permgen),也就是Full GC,这种状态的G1就和-XX:+UseSerialGC的Full GC一样(背后的核心代码是两者公用的

G1 GC的System.gc()默认是Full GC,也就是Serial Old GC,只有加上-XX:+ExplicitGCInvokesConcurrent时才会用自身的并发GC来执行System.gc(),此时System.gc()的作用是强行启动一次global concurrent marking;一般情况下暂停中只会做初始标记然后就返回了,接下来的并发标记还是照常并发执行。

注意事项

不要把 -XX:MaxGCPauseMillis设置的太低,设置的太低会导致G1垃圾收集跟不上对象的分配,可能会导致垃圾堆积,最后引发Full GC。

相关参数

参数含义
-XX:G1HeapRegionSize=n设置Region的大小,并非最终值
-XX:MaxGCPauseMills设置G1收集过程的目标时间,默认值200,不是硬性条件
-XX:G1NewSizePercent新生代最小值,默认值5%
-XX:G1MaxNewSizePercent新生代最大值,默认值60%
-XX:ParallelGCThreadsSTW期间,并行GC线程数
-XX:ConcGCThreads=n并发标记阶段,并行执行的线程数
-XX:InitiatingHeapOccupancyPercent设置触发标记周期的 Java 堆占用率阈值。默认值是45%。这里的java堆占比指的是non_young_capacity_bytes,包括old+humongous
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能写出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重中之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手写类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹氿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值