Garbage First (G1)收集器的部分实现细节

Garbage First (G1)收集器的部分实现细节

  • G1收集器简介
    Garbage First (G1)收集器是垃圾收集器发展史上具有里程碑意义的一个收集器,它开创了面向局部收集的设计思路和Region的内存布局形式。
    作为CMS的继承者,设计者们希望做出一款能够建立起停顿时间模型(Pause Prediction Model) 的收集器,停顿时间模型的意思是能够指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间大概率不超过N毫秒这样的目标,这几乎已经是实时Java(RTSG)的中软实时垃圾收集器的特征了。

那么如何来实现这个目标呢?在G1收集器出现之前的所有垃圾收集器,收集的范围要么是整个新生代,要么是整个老年代,要么就是整个java堆。而G1脱离了这个思路。

G1可以面向堆内存任何部分来组成回收集(Collection Set,CSet) 进行回收,衡量标准不再是属于某个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的**Mixed GC(混合回收)**模式

  • Region堆内存布局和Mixed GC

Region堆内存布局是他能够实现这个停顿时间模型的关键。虽然G1也是遵循分代收集理论进行设计的,但是它的堆内存的布局与其他收集器有明显的差异:G1不再坚持固定大小和固定数量的分代区域划分,而是把连续的java堆划分为多个大小相等的,独立的区域(Region),每一个区域都可以根据需要,扮演新生代的Eden空间,Survivor空间,或者老年代空间。

收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间,熬过了多次垃圾收集的对象都能取得很好的收集效果。

而Region中海油一类特殊的Humongous区域,专门用来存储大对象。
G1认为只要大小超过了一个Region容量一半的对象即可判断为大对象。
每个Region的大小可以通过参数**-XX:G1HeapRegionSize来进行设定,取值范围为1MB到32MB,且应为2的N次幂。
而对于那些超过了整个Region的贼大对象,将会被存储在N个连续的
Humongous Region** 中,G1的大多数行为都将这个区域当做老年代看待,如图:


G1仍然保留着新生代和老年代的概念,但是新生代和老年代不再是固定的了,他们都是一系列不连续区域的动态集合。
G1收集器之所以能建立可预测的时间停顿模型,是因为它将Region作为单次回收的最小单元,即每次回收的内存空间都是Region大小的整倍数,这样可以有计划的避免在整个Java堆中进行全区域的垃圾收集。

而更具体的思路是让G1收集器去跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所获得的空间即回收所需要的时间的经验值,然后在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数 -XX:MaxGCPauseMillis 指定,默认是200毫秒),优先处理回收价值收益最大的那些Region,这也是 Garbage Girst 名字的由来。

这种使用Region划分内存空间,一级具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

  • G1的关键细节问题

G1其中的实现细节远远没有想象中的那么简单,G1收集器至少有且不限于以下这些细节问题需要妥善解决:

  • 将java堆分成多个独立的Region后,里面存在的跨Region引用怎么解决?

  • 解决的思路已经在上篇文章讲到过:使用记忆集来解决整堆扫描的问题,但是在G1收集器中,记忆集的应用其实要复杂的多,它的每个Region都要维护一个独立的记忆集,这些记忆集会记录下别的Region指向自己的指针,并标记这些指针在那些卡页的范围内。
    G1的记忆集在存储结构的本质上是一种哈希表,Key是别的Region的起始地址,Value是一个集合,里面存储的元素是卡表的索引号。这种双向的卡表结构(卡表是“我指向谁”,这种结构还记录了“谁指向我”)比原来的卡表实现起来更加复杂,由于Region数量比传统的分代数量要多,所以G1收集器有比其他收集器更大的内存负担。根据大佬经验,G1收集器会占堆内存的10%~20%的额外内存来维持收集器工作。

  • 在并发标记阶段如何保证收集线程鱼用户线程互不干扰的运行?
    这里首先要解决的是用户线程改变对象引用关系时,必须保证它不能打破原本的对象图结构,导致标记结果出现错误,而解决这个问题的办法也在上篇文章讲到:G1收集器采用了原始快照来解决这个问题。
    另外,垃圾收集对用户线程的影响还提现在回收过程中创建对象的内存分配上,程序如果要继续运行那肯定会产生新对象,G1为每个Region设计了两个名为TAMS(Top at Mark Start) 的指针,把Region中的一部分空间划分出来用于并发过程中的新对象分配,并发回收时的对象地址都必须要在这两个指针位置上。
    G1会默认这个地址上的对象是存活的,如果内存回收跟不上内存分配的速度,那么G1收集器也会被迫冻结用户的线程,导致Full GC而产生长时间 Stop The World

  • 怎么样建立起可靠的停顿预测模型?
    用户通过 -XX:MaxGCPauseMillis 参数指定的停顿时间只意味着垃圾收集的期望值,但是G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿预测模型是以 衰减均值(Decaying Average) 为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region的回收耗时,每个Region记忆集里的脏卡数量和其他可测量的步骤花费的成本,并分析出平均值,标准偏差,置信度等统计信息。强调衰减均值的原因是,越新的统计状态就会越能决定他的回收价值。

而G1收集器大概分为四个步骤:

  • 初始标记
    这步只是最开始的一个步骤,只需要标记出与GC Roots有关联的对象并修改TAMS指针,为保证下次并发标记时用户线程分配新对象,这个步骤需要短暂的暂停线程,是借用Minor GC是同步完成的。
  • 并发标记
    这是收集器开始进行可达性分析,递归所有对象,来找出可回收对象。这个步骤是和用户线程同时进行的,会耗时较长,最后会在执行一次来找出有变动的对象。
  • 最终标记
    对用户线程做一个短暂的暂停,来处理并发阶段结束后遗留的少量对象
  • 筛选回收
    负责更新Region的统计数据,分析哪些Region最有回收价值(根据用户设置的停顿时间),把Region中存活的对象复制到另一个新的Region中,再把旧的Region清空,这个步骤必须要停顿用户线程,由多个收集器线程同步进行。

从上面的描述可以看出,除了并发标记阶段,其余时候收集器都必须完全暂停用户线程才能够执行,它并非纯粹的追求低延迟,官方给它设定的目标是在延迟可控的情况下,尽可能的提高吞吐量。
下面是G1收集器运行示意图:

G1收集器从整体上来看是基于 标记-整理 来实现的,从局部来看又像是基于 标记-复制 实现无论如何,这都表示G1收集器不会产生内存碎片,只要回收对象的速度能够跟上分配对象的速度,那么程序就能完美运行。

当然,G1的缺点也有一些,比如G1收集器需要为每个Region创建一个卡表,所以G1收集器会占20%~30%的额外堆容量来维护卡表。

在执行负载的角度看,G1收集器会使用写后屏障来更新维护卡表外,G1收集器为了实现 SATB 原始快照搜索 算法,也会使用写前屏障来追踪在并发收集时,对象的指针变化。
对比增量更新算法,原始快照可以减少并发标记和重新标记阶段的消耗来避免最终标记执行时间过长,这也的确会为程序运行过程中产生由跟踪引用的变化带来的负担。
所以G1收集器就不得不实现一个类似消息队列的结构,将写前和写后屏障的操作放在一个队列中,进行异步处理。

结尾

以上就是G1收集器某些方面的实现细节,如果有什么需要改进的地方,欢迎大家在评论区内留言!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值