JAVA之G1垃圾回收器
阅读原文时间:2022年03月19日阅读:1

概述

G1 GC,全称Garbage-First Garbage Collector,通过-XX:+UseG1GC参数来启用,作为体验版随着JDK 6u14版本面世,在JDK 7u4版本发行时被正式推出,相信熟悉JVM的同学们都不会对它感到陌生。在JDK 9中,G1被提议设置为默认垃圾收集器(JEP 248)。在官网中,是这样描述G1的: > The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that: > * Can operate concurrently with applications threads like the CMS collector. > * Compact free space without lengthy GC induced pause times. > * Need more predictable GC pause durations. > * Do not want to sacrifice a lot of throughput performance. > * Do not require a much larger Java heap.

从官网的描述中,我们知道G1是一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能的满足垃圾收集暂停时间的要求。它是专门针对以下应用场景设计的: * 像CMS收集器一样,能与应用程序线程并发执行。 * 整理空闲空间更快。 * 需要GC停顿时间更好预测。 * 不希望牺牲大量的吞吐性能。 * 不需要更大的Java Heap。

G1收集器的设计目标是取代CMS收集器,它同CMS相比,在以下方面表现的更出色: * G1是一个有整理内存过程的垃圾收集器,不会产生很多内存碎片。 * G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

G1基础知识

初衷

在G1提出之前,经典的垃圾收集器主要有三种类型:串行收集器、并行收集器和并发标记清除收集器,这三种收集器分别可以是满足Java应用三种不同的需求:内存占用及并发开销最小化、应用吞吐量最大化和应用GC暂停时间最小化,但是,上述三种垃圾收集器都有几个共同的问题:(1)所有针对老年代的操作必须扫描整个老年代空间;(2)年轻地和老年代是独立的连续的内存块,必须先决定年轻代和老年代在虚拟地址空间的位置。

设计目标

G1是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

使用场景

G1适用于以下几种应用:

  • 可以像CMS收集器一样,允许垃圾收集线程和应用线程并行执行,即需要额外的CPU资源;

  • 压缩空闲空间不会延长GC的暂停时间;

  • 需要更易预测的GC暂停时间;

  • 不需要实现很高的吞吐量

G1对象分配策略

说起对象的分配,我们不得不谈谈对象的分配策略。它分为4个阶段:

  1. 栈上分配

  2. TLAB(Thread Local Allocation Buffer)线程本地分配缓冲区

  3. 共享Eden区中分配

  4. Humongous区分配

对象在分配之前会做逃逸分析,如果该对象只会被本线程使用,那么就将该对象在栈上分配。这样对象可以在函数调用后销毁,减轻堆的压力,避免不必要的gc。 如果对象在栈是上分配不成功,就会使用TLAB来分配。TLAB为线程本地分配缓冲区,它的目的为了使对象尽可能快的分配出来。如果对象在一个共享的空间中分配,我们需要采用一些同步机制来管理这些空间内的空闲空间指针。在Eden空间中,每一个线程都有一个固定的分区用于分配对象,即一个TLAB。分配对象时,线程之间不再需要进行任何的同步。

对TLAB空间中无法分配的对象,JVM会尝试在共享Eden空间中进行分配。如果是大对象,则直接在Humongous区分配

G1重要概念

在G1的实现过程中,引入了一些新的概念,对于实现高吞吐、没有内存碎片、收集时间可控等功能起到了关键作用。下面我们就一起看一下G1中的这几个重要概念。

传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址,下同)是连续的。如下图所示:

G1采取了不同的策略来解决并行、串行和CMS收集器的碎片、暂停时间不可控制等问题——G1将整个堆分成相同大小的分区(Region),如下图所示:

每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。 年轻代、幸存区、老年代这些概念还存在,成为逻辑上的概念,这样方便复用之前分代框架的逻辑。在物理上不需要连续,则带来了额外的好处——有的分区内垃圾对象特别多,有的分区内垃圾对象很少,G1会优先回收垃圾对象特别多的分区,这样可以花费较少的时间来回收这些分区的垃圾,这也就是G1名字的由来,即首先收集垃圾最多的分区。

新生代其实并不是适用于这种算法的,依然是在新生代满了的时候,对整个新生代进行回收—— 整个新生代中的对象,要么被回收、要么晋升,至于新生代也采取分区机制的原因,则是因为这样跟老年代的策略统一,方便调整代的大小。

G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但是都是2的冥次方,如果G1HeapRegionSize为默认值,则在堆初始化时计算Region的实践大小,具体实现如下。

一组可被回收的分区的集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间、或者老年代。CSet会占用不到整个堆空间的1%大小。

为了解决赋值器开销这个问题,在G1 中又引入了另外一个概念,卡表(Card Table)。一个Card Table将一个分区在逻辑上划分为固定大小的连续区域,每个区域称之为卡。卡通常较小,介于128到512字节之间。CardTable通常为字节数组,由Card的索引(即数组下标)来标识每个分区的空间地址。默认情况下,每个卡都未引用。当一个地址空间有引用时,这个地址空间对应的数组索引的值被标记为"0",即标记为脏引用,此外RSet也将这个数组下标记录下来。一般情况下,这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

全称是Snapshot-At-The-Beginning,由字面理解,是GC开始时活着的对象的一个快照。它是通过Root Tracing得到的,作用是维持并发GC的正确性。 那么它是怎么维持并发GC的正确性的呢?根据三色标记算法,我们知道对象存在三种状态: * 白:对象没有被标记到,标记阶段结束后,会被当做垃圾回收掉。 * 灰:对象被标记了,但是它的field还没有被标记或标记完。 * 黑:对象被标记了,且它的所有field也被标记完了。

由于并发阶段的存在,Mutator和Garbage Collector线程同时对对象进行修改,就会出现白对象漏标的情况,这种情况发生的前提是: * Mutator赋予一个黑对象该白对象的引用。 * Mutator删除了所有从灰对象到该白对象的直接或者间接引用。

对于第一个条件,在并发标记阶段,如果该白对象是new出来的,并没有被灰对象持有,那么它会不会被漏标呢?Region中有两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象是新分配的,这是一种隐式的标记。对于在GC时已经存在的白对象,如果它是活着的,它必然会被另一个对象引用,即条件二中的灰对象。如果灰对象到白对象的直接引用或者间接引用被替换了,或者删除了,白对象就会被漏标,从而导致被回收掉,这是非常严重的错误,所以SATB破坏了第二个条件。也就是说,一个对象的引用被替换时,可以通过write barrier 将旧引用记录下来。

SATB也是有副作用的,如果被替换的白对象就是要被收集的垃圾,这次的标记会让它躲过GC,这就是float garbage。因为SATB的做法精度比较低,所以造成的float garbage也会比较多。

SATB是维持并发GC的正确性的一个手段,G1GC的并发理论基础就是SATB,SATB是由Taiichi Yuasa为增量式标记清除垃圾收集器设计的一个标记算法。Yuasa的SATAB的标记优化主要针对标记-清除垃圾收集器的并发标记阶段。按照R大的说法:CMS的incremental update设计使得它在remark阶段必须重新扫描所有线程栈和整个young gen作为root;G1的SATB设计在remark阶段则只需要扫描剩下的satbmarkqueue。

SATB算法创建了一个对象图,它是堆的一个逻辑“快照”。标记数据结构包括了两个位图:previous位图和next位图。previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉。 下面我们以几个图例来描述SATB算法的过程。

1,在并发周期开始之前,NTAMS字段被设置到每个分区当前的顶部,并发周期启动后分配的对象会被放在TAMS之前(图里下边的部分),同时被明确定义为隐式存活对象,而TAMS之后(图里上边的部分)的对象则需要被明确地标记。

2,并发标记过程中的堆分区:

3,位于堆分区的Bottom和PTAMS之间的对象都会被标记并记录在previous位图中;

4,位于堆分区的Top和PATMS之间的对象均为隐式存活对象,同时也记录在previous位图中:

5,在重新标记阶段的最后,所有NTAMS之前的对象都会被标记。

6,在并发标记阶段分配的对象会被分配到NTAMS之后的空间,它们会作为隐式存活对象被记录在next位图中。一次并发标记周期完成后,这个next位图会覆盖previous位图,然后将next位图清空。

SATB是一个快照标记算法,在并发标记进行的过程中,垃圾收集器(Collecotr)和应用程序(Mutator)都在活动,如果一个对象还没被mark到,这时候Mutator就修改了它的引用,那么这时候拿到的快照就是不完整的了,如何解决这个问题呢?G1 GC使用了SATB write barrier来解决这个问题——在并发标记过程中,将该对象的旧的引用记录在一个SATB日志对列或缓冲区中。去翻G1的代码,却发现实际代码如下——只该对象入队列,并没有将整个修改过程放在写屏障之间完成。

enqueue的真正代码在 hotspot/src/share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp中,这里使用 JavaThread::satb_mark_queue_set().is_active()判断是否处于并发标记周期。

stabmarkqueue.enqueue方法首先尝试将以前的值记录在一个缓冲区中,如果这个缓冲区已经满了,就会将当期这个SATB缓冲区“退休”并放入全局列表中,然后再给线程分配一个新的SATB缓冲区。并发标记线程会定期检查和处理那些“被填满”的缓冲区。

1 // share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.hpp
2 // This notes that we don't need to access any BarrierSet data
3 // structures, so this can be called from a static context.
4 template static void write_ref_field_pre_static(T* field, oop newVal) {
5 T heap_oop = oopDesc::load_heap_oop(field);
6 if (!oopDesc::is_null(heap_oop)) {
7 enqueue(oopDesc::decode_heap_oop(heap_oop));
8 }
9 }
10 // share/vm/gc_implementation/g1/g1SATBCardTableModRefBS.cpp
11 void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
12 // Nulls should have been already filtered.
13 assert(pre_val->is_oop(true), "Error");
14 if (!JavaThread::satb_mark_queue_set().is_active()) return;
15 Thread* thr = Thread::current();
16 if (thr->is_Java_thread()) {
17 JavaThread* jt = (JavaThread*)thr;
18 jt->satb_mark_queue().enqueue(pre_val);
19 } else {
20 MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
21 JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
22 }
23 }

全称是Remembered Set,是辅助GC过程的一种结构,典型的空间换时间工具,和Card Table有些类似。还有一种数据结构也是辅助GC的:Collection Set(CSet),它记录了GC要收集的Region集合,集合里的Region可以是任意年代的。在GC的时候,对于old->young和old->old的跨代对象引用,只要扫描对应的CSet中的RSet即可。 逻辑上说每个Region都有一个RSet,RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。而Card Table则是一种points-out(我引用了谁的对象)的结构,每个Card 覆盖一定范围的Heap(一般为512Bytes)。G1的RSet是在Card Table的基础上实现的:每个Region会记录下别的Region有指向自己的指针,并标记这些指针分别在哪些Card的范围内。 这个RSet其实是一个Hash Table,Key是别的Region的起始地址,Value是一个集合,里面的元素是Card Table的Index。

RSet记录了其他Region中的对象引用本Region中对象的关系,属于points-into结构(谁引用了我的对象)。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可。

如下图所示,Region1和Region3中的对象都引用了Region2中的对象,因此在Region2的RSet中记录了这两个引用。

下图表示了RSet、Card和Region的关系。

上图中有三个Region,每个Region被分成了多个Card,在不同Region中的Card会相互引用,Region1中的Card中的对象引用了Region2中的Card中的对象,蓝色实线表示的就是points-out的关系,而在Region2的RSet中,记录了Region1的Card,即红色虚线表示的关系,这就是points-into。 而维系RSet中的引用关系靠post-write barrier和Concurrent refinement threads来维护,操作伪代码如下:

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

post-write barrier记录了跨Region的引用更新,更新日志缓冲区则记录了那些包含更新引用的Cards。一旦缓冲区满了,Post-write barrier就停止服务了,会由Concurrent refinement threads处理这些缓冲区日志。 RSet究竟是怎么辅助GC的呢?在做YGC的时候,只需要选定young generation region的RSet作为根集,这些RSet记录了old->young的跨代引用,避免了扫描整个old generation。 而mixed gc的时候,old generation中记录了old->old的RSet,young->old的引用由扫描全部young generation region得到,这样也不用扫描全部old generation region。所以RSet的引入大大减少了GC的工作量。

摘一段R大的解释:G1 GC则是在points-out的card table之上再加了一层结构来构成points-into RSet:每个region会记录下到底哪些别的region有指向自己的指针,而这些指针分别在哪些card的范围内。 这个RSet其实是一个hash table,key是别的region的起始地址,value是一个集合,里面的元素是card table的index。 举例来说,如果region A的RSet里有一项的key是region B,value里有index为1234的card,它的意思就是region B的一个card里有引用指向region A。所以对region A来说,该RSet记录的是points-into的关系;而card table仍然记录了points-out的关系。

Pause Prediction Model 即停顿预测模型。它在G1中的作用是: >G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.

G1 GC是一个响应时间优先的GC算法,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。 停顿预测模型是以衰减标准偏差为理论基础实现的。

1 // share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
2 double get_new_prediction(TruncatedSeq* seq) {
3 return MAX2(seq->davg() + sigma() * seq->dsd(),
4 seq->davg() * confidence_factor(seq->num()));
5 }

在这个预测计算公式中:davg表示衰减均值,sigma()返回一个系数,表示信赖度,dsd表示衰减标准偏差,confidence_factor表示可信度相关系数。而方法的参数TruncateSeq,顾名思义,是一个截断的序列,它只跟踪了序列中的最新的n个元素。

在G1 GC过程中,每个可测量的步骤花费的时间都会记录到TruncateSeq(继承了AbsSeq)中,用来计算衰减均值、衰减变量,衰减标准偏差等。

1 // src/share/vm/utilities/numberSeq.cpp
2
3 void AbsSeq::add(double val) {
4 if (_num == 0) {
5 // if the sequence is empty, the davg is the same as the value
6 _davg = val;
7 // and the variance is 0
8 _dvariance = 0.0;
9 } else {
10 // otherwise, calculate both
11 _davg = (1.0 - _alpha) * val + _alpha * _davg;
12 double diff = val - _davg;
13 _dvariance = (1.0 - _alpha) * diff * diff + _alpha * _dvariance;
14 }
15 }

比如要预测一次GC过程中,RSet的更新时间,这个操作主要是将Dirty Card加入到RSet中,具体原理参考前面的RSet。每个Dirty Card的时间花费通过_cost_per_card_ms_seq来记录,具体预测代码如下:

1 // share/vm/gc_implementation/g1/g1CollectorPolicy.hpp
2
3 double predict_rs_update_time_ms(size_t pending_cards) {
4 return (double) pending_cards * predict_cost_per_card_ms();
5 }
6 double predict_cost_per_card_ms() {
7 return get_new_prediction(_cost_per_card_ms_seq);
8 }

get_new_prediction就是我们开头说的方法,现在大家应该基本明白停顿预测模型的实现原理了。

GC模式

G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。

发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。

当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。

那么mixed gc什么时候被触发?

先回顾一下cms的触发机制,如果添加了以下参数:

-XX:CMSInitiatingOccupancyFraction=80
-XX:+UseCMSInitiatingOccupancyOnly

当老年代的使用率达到80%时,就会触发一次cms gc。相对的,mixed gc中也有一个阈值参数 -XX:InitiatingHeapOccupancyPercent,当老年代大小占整个堆大小百分比达到该阈值时,会触发一次mixed gc.

mixed gc的执行过程有点类似cms,主要分为以下几个步骤:

如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc。

G1的过程

Mix GC不仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的老年代分区。

它的GC步骤分2步:

  1. 全局并发标记(global concurrent marking)

  2. 拷贝存活对象(evacuation)

在进行Mix GC之前,会先进行global concurrent marking(全局并发标记)。 global concurrent marking的执行过程是怎样的呢?

在G1 GC中,它主要是为Mixed GC提供标记服务的,并不是一次GC过程的一个必须环节。global concurrent marking的执行过程分为五个步骤:

  • 初始标记(initial mark,STW)
        在此阶段,G1 GC 对根进行标记。该阶段与常规的     (STW) 年轻代垃圾回收密切相关。

  • 根区域扫描(root region scan)
        G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。

  • 并发标记(Concurrent Marking)
        G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断

  • 最终标记(Remark,STW)
        该阶段是 STW 回收,帮助完成标记周期。G1 GC     清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。

  • 清除垃圾(Cleanup,STW)
        在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。

提到并发标记,我们不得不了解并发标记的三色标记算法。它是描述追踪式回收器的一种有用的方法,利用它可以推演回收器的正确性。首先,我们将对象分成三种类型的。

  • 黑色:根对象,或者该对象与它的子对象都被扫描

  • 灰色:对象本身被扫描,但还没扫描完该对象中的子对象

  • 白色:未被扫描对象,扫描完成所有对象之后,最终为白色的为不可达对象,即垃圾对象

当GC开始扫描对象时,按照如下图步骤进行对象的扫描:,

1,根对象被置为黑色,子对象被置为灰色

2,继续由灰色遍历,将已扫描了子对象的对象置为黑色。

3,遍历了所有可达的对象后,所有可达的对象都变成了黑色。不可达的对象即为白色,需要被清理。

这个过程看起来很美好,但是如果在标记过程中,应用程序也在运行,那么对象的指针就有可能改变。这样的话,我们就会遇到一个问题:对象丢失问题

我们看下面一种情况,当垃圾收集器扫描到下面情况时:

这时候应用程序执行了以下操作:

A.c=C

这样,对象的状态图变成如下情形:

很显然,此时C是白色,被认为是垃圾需要清理掉,显然这是不合理的。那么我们如何保证应用程序在运行的时候,GC标记的对象不丢失呢?有如下2中可行的方式:

  1. 在插入的时候记录对象

  2. 在删除的时候记录对象

刚好这对应CMS和G1的2种不同实现方式:

在CMS采用的是增量更新(Incremental update),只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,删除的时候记录所有的对象,它有3个步骤:

1,在开始标记的时候生成一个快照图标记存活对象

2,在并发标记的时候所有被改变的对象入队(在write barrier里把所有旧的引用所指向的对象都变成非白的)

3,可能存在游离的垃圾,将在下次被收集

这样,G1到现在可以知道哪些老的分区可回收垃圾最多。 当全局并发标记完成后,在某个时刻,就开始了Mix GC。这些垃圾回收被称作“混合式”是因为他们不仅仅进行正常的新生代垃圾收集,同时也回收部分后台扫描线程标记的分区。

G1收集器的收集活动主要有四种操作:

  • 新生代垃圾收集

  • 后台收集、并发周期

  • 混合式垃圾收集

  • 必要时候的Full GC

第一、新生代垃圾收集的图例如下:

  • Eden区耗尽的时候就会触发新生代收集,新生代垃圾收集会对整个新生代进行回收

  • 新生代垃圾收集期间,整个应用STW

  • 新生代垃圾收集是由多线程并发执行的

  • 新生代收集结束后依然存活的对象,会被拷贝到一个新的Survivor分区,或者是老年代。

G1设计了一个标记阈值,它描述的是总体Java堆大小的百分比,默认值是45,这个值可以通过命令 -XX:InitiatingHeapOccupancyPercent(IHOP)来调整,一旦达到这个阈值就回触发一次并发收集周期。注意:这里的百分比是针对整个堆大小的百分比,而CMS中的 CMSInitiatingOccupancyFraction命令选型是针对老年代的百分比。并发收集周期的图例如下:

在上图中有几个情况需要注意:

  • 新生代的空间占用情况发生了变化——在并发收集周期中,至少有一次(很可能是多次)新生代垃圾收集;

  • 注意到一些分区被标记为X,这些分区属于老年代,它们就是标记周期找出的包含最多垃圾的分区(注意:它们内部仍然保留着数据);

  • 老年代的空间占用在标记周期结束后变得更多,这是因为在标记周期期间,新生代的垃圾收集会晋升对象到老年代,而且标记周期中并不会是否老年代的任何对象

第二、G1的并发标记周期包括多个阶段: 并发标记周期采用的算法是我们前文提到的SATB标记算法,产出是找出一些垃圾对象最多的老年代分区:

初始标记(initial-mark),在这个阶段,应用会经历STW,通常初始标记阶段会跟一次新生代收集一起进行,换句话说——既然这两个阶段都需要暂停应用,G1 GC就重用了新生代收集来完成初始标记的工作。在新生代垃圾收集中进行初始标记的工作,会让停顿时间稍微长一点,并且会增加CPU的开销。初始标记做的工作是设置两个TAMS变量(NTAMS和PTAMS)的值,所有在TAMS之上的对象在这个并发周期内会被识别为隐式存活对象;

根分区扫描(root-region-scan),这个过程不需要暂停应用,在初始标记或新生代收集中被拷贝到survivor分区的对象,都需要被看做是根,这个阶段G1开始扫描survivor分区,所有被survivor分区所引用的对象都会被扫描到并将被标记。survivor分区就是根分区,正因为这个,该阶段不能发生新生代收集,如果扫描根分区时,新生代的空间恰好用尽,新生代垃圾收集必须等待根分区扫描结束才能完成。如果在日志中发现根分区扫描和新生代收集的日志交替出现,就说明当前应用需要调优。

并发标记阶段(concurrent-mark),并发标记阶段是多线程的,我们可以通过 -XX:ConcGCThreads来设置并发线程数,默认情况下,G1垃圾收集器会将这个线程总数设置为并行垃圾线程数( -XX:ParallelGCThreads)的四分之一;并发标记会利用trace算法找到所有活着的对象,并记录在一个bitmap中,因为在TAMS之上的对象都被视为隐式存活,因此我们只需要遍历那些在TAMS之下的;记录在标记的时候发生的引用改变,SATB的思路是在开始的时候设置一个快照,然后假定这个快照不改变,根据这个快照去进行trace,这时候如果某个对象的引用发生变化,就需要通过pre-write barrier logs将该对象的旧的值记录在一个SATB缓冲区中,如果这个缓冲区满了,就把它加到一个全局的列表中——G1会有并发标记的线程定期去处理这个全局列表。

重新标记阶段(remarking),重新标记阶段是最后一个标记阶段,需要暂停整个应用,G1垃圾收集器会处理掉剩下的SATB日志缓冲区和所有更新的引用,同时G1垃圾收集器还会找出所有未被标记的存活对象。这个阶段还会负责引用处理等工作。

清理阶段(cleanup),清理阶段真正回收的内存很小,截止到这个阶段,G1垃圾收集器主要是标记处哪些老年代分区可以回收,将老年代按照它们的存活度(liveness)从小到大排列。这个过程还会做几个事情:识别出所有空闲的分区、RSet梳理、将不用的类从metaspace中卸载、回收巨型对象等等。识别出每个分区里存活的对象有个好处是在遇到一个完全空闲的分区时,它的RSet可以立即被清理,同时这个分区可以立刻被回收并释放到空闲队列中,而不需要再放入CSet等待混合收集阶段回收;梳理RSet有助于发现无用的引用。

第三、混合收集只会回收一部分老年代分区,下图是第一次混合收集前后的堆情况对比。

混合收集会执行多次,一直运行到(几乎)所有标记点老年代分区都被回收,在这之后就会恢复到常规的新生代垃圾收集周期。当整个堆的使用率超过指定的百分比时,G1 GC会启动新一轮的并发标记周期。在混合收集周期中,对于要回收的分区,会将该分区中存活的数据拷贝到另一个分区,这也是为什么G1收集器最终出现碎片化的频率比CMS收集器小得多的原因——以这种方式回收对象,实际上伴随着针对当前分区的压缩。

GC日志

我们可以看到这段GC日志有层级关系,笔者现在将其抽取成如下样式:

由这段GC日志我们可知,整个YGC由多个子任务以及嵌套子任务组成,且一些核心任务为:Root Scanning,Update/Scan RS,Object Copy,CleanCT,Choose CSet,Ref Proc,Humongous Reclaim,Free CSet。

G1收集器的日志与其他收集器有很大不同,源于G1独立的体系架构和数据结构,下面这两段日志来源于美团点评的CRM系统线上生产环境。

Young GC日志

我们先来看看Young GC的日志:

1 {Heap before GC invocations=12 (full 1):
2 garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
3 region size 1024K, 172 young (176128K), 13 survivors (13312K)
4 Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
5 class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
6 2014-11-14T17:57:23.654+0800: 27.884: [GC pause (G1 Evacuation Pause) (young)
7 Desired survivor size 11534336 bytes, new threshold 15 (max 15)
8 - age 1: 5011600 bytes, 5011600 total
9 27.884: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms]
10 27.884: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms]
11 27.884: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms]
12 , 0.0158389 secs]
13 [Parallel Time: 8.1 ms, GC Workers: 4]
14 [GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1]
15 [Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1]
16 [Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4]
17 [Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11]
18 [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
19 [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6]
20 [Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4]
21 [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
22 [GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4]
23 [GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2]
24 [GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3]
25 [Code Root Fixup: 0.5 ms]
26 [Code Root Migration: 1.3 ms]
27 [Code Root Purge: 0.0 ms]
28 [Clear CT: 0.2 ms]
29 [Other: 5.8 ms]
30 [Choose CSet: 0.0 ms]
31 [Ref Proc: 5.0 ms]
32 [Ref Enq: 0.1 ms]
33 [Redirty Cards: 0.0 ms]
34 [Free CSet: 0.2 ms]
35 [Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)]
36 Heap after GC invocations=13 (full 1):
37 garbage-first heap total 3145728K, used 171269K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
38 region size 1024K, 11 young (11264K), 11 survivors (11264K)
39 Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
40 class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
41 }
42 [Times: user=0.05 sys=0.01, real=0.02 secs]

每个过程的作用如下:

* garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000) 这行表示使用了G1垃圾收集器,total heap 3145728K,使用了336645K。

* region size 1024K, 172 young (176128K), 13 survivors (13312K) Region大小为1M,青年代占用了172个(共176128K),幸存区占用了13个(共13312K)。

* Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K java 8的新特性,去掉永久区,添加了元数据区,这块不是本文重点,不再赘述。需要注意的是,之所以有committed和reserved,是因为没有设置MetaspaceSize=MaxMetaspaceSize。

* [GC pause (G1 Evacuation Pause) (young) GC原因,新生代minor GC。

* [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms] 发生minor GC和full GC时,所有相关region都是要回收的。而发生并发GC时,会根据目标停顿时间动态选择部分垃圾对并多的Region回收,这一步就是选择Region。_pending_cards是关于RSet的Card Table。predicted base time是预测的扫描card table时间。

* [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms] 这一步是添加Region到collection set,新生代一共159个Region,13个幸存区Region,这也和之前的(172 young (176128K), 13 survivors (13312K))吻合。预计收集时间是44.09 ms。

* [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms] 这一步是对上面两步的总结。预计总收集时间79.34ms。

* [Parallel Time: 8.1 ms, GC Workers: 4] 由于收集过程是多线程并行(并发)进行,这里是4个线程,总共耗时8.1ms(wall clock time)

* [GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1] 收集线程开始的时间,使用的是相对时间,Min是最早开始时间,Avg是平均开始时间,Max是最晚开始时间,Diff是Max-Min(此处的0.1貌似有问题)

* [Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1] 扫描Roots花费的时间,Sum表示total cpu time,下同。

* [Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4] [Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11] Update RS (ms)是每个线程花费在更新Remembered Set上的时间。

* [Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3] 扫描CS中的region对应的RSet,因为RSet是points-into,所以这样实现避免了扫描old generadion region,但是会产生float garbage。

* [Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6] 扫描code root耗时。code root指的是经过JIT编译后的代码里,引用了heap中的对象。引用关系保存在RSet中。 * [Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4] 拷贝活的对象到新region的耗时。

* [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] 线程结束,在结束前,它会检查其他线程是否还有未扫描完的引用,如果有,则”偷”过来,完成后再申请结束,这个时间是线程之前互相同步所花费的时间。

* [GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4] 花费在其他工作上(未列出)的时间。

* [GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2] 每个线程花费的时间和。

* [GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3] 每个线程结束的时间。

* [Code Root Fixup: 0.5 ms] 用来将code root修正到正确的evacuate之后的对象位置所花费的时间。

* [Code Root Migration: 1.3 ms] 更新code root 引用的耗时,code root中的引用因为对象的evacuation而需要更新。

* [Code Root Purge: 0.0 ms] 清除code root的耗时,code root中的引用已经失效,不再指向Region中的对象,所以需要被清除。

* [Clear CT: 0.2 ms] 清除card table的耗时。

* [Other: 5.8 ms] [Choose CSet: 0.0 ms] [Ref Proc: 5.0 ms] [Ref Enq: 0.1 ms] [Redirty Cards: 0.0 ms] [Free CSet: 0.2 ms] 其他事项共耗时5.8ms,其他事项包括选择CSet,处理已用对象,引用入ReferenceQueues,释放CSet中的region到free list。

* [Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)] 新生代清空了,下次扩容到301MB。

global concurrent marking 日志

对于global concurrent marking过程,它的日志如下所示:

1 66955.252: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 1449132032 bytes, allocation request: 579608 bytes, threshold: 1449
2 551430 bytes (45.00 %), source: concurrent humongous allocation]
3 2014-12-10T11:13:09.532+0800: 66955.252: Application time: 2.5750418 seconds
4 66955.259: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: requested by GC cause, GC cause: G1 Humongous Allocation]
5 {Heap before GC invocations=1874 (full 4):
6 garbage-first heap total 3145728K, used 1281786K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
7 region size 1024K, 171 young (175104K), 27 survivors (27648K)
8 Metaspace used 116681K, capacity 137645K, committed 137984K, reserved 1171456K
9 class space used 13082K, capacity 16290K, committed 16384K, reserved 1048576K
10 66955.259: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested]
11 2014-12-10T11:13:09.539+0800: 66955.259: [GC pause (G1 Humongous Allocation) (young) (initial-mark)
12 …….
13 2014-12-10T11:13:09.597+0800: 66955.317: [GC concurrent-root-region-scan-start]
14 2014-12-10T11:13:09.597+0800: 66955.318: Total time for which application threads were stopped: 0.0655753 seconds
15 2014-12-10T11:13:09.610+0800: 66955.330: Application time: 0.0127071 seconds
16 2014-12-10T11:13:09.614+0800: 66955.335: Total time for which application threads were stopped: 0.0043882 seconds
17 2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-root-region-scan-end, 0.0281351 secs]
18 2014-12-10T11:13:09.625+0800: 66955.346: [GC concurrent-mark-start]
19 2014-12-10T11:13:09.645+0800: 66955.365: Application time: 0.0306801 seconds
20 2014-12-10T11:13:09.651+0800: 66955.371: Total time for which application threads were stopped: 0.0061326 seconds
21 2014-12-10T11:13:10.212+0800: 66955.933: [GC concurrent-mark-end, 0.5871129 secs]
22 2014-12-10T11:13:10.212+0800: 66955.933: Application time: 0.5613792 seconds
23 2014-12-10T11:13:10.215+0800: 66955.935: [GC remark 66955.936: [GC ref-proc, 0.0235275 secs], 0.0320865 secs]
24 [Times: user=0.05 sys=0.00, real=0.03 secs]
25 2014-12-10T11:13:10.247+0800: 66955.968: Total time for which application threads were stopped: 0.0350098 seconds
26 2014-12-10T11:13:10.248+0800: 66955.968: Application time: 0.0001691 seconds
27 2014-12-10T11:13:10.250+0800: 66955.970: [GC cleanup 1178M->632M(3072M), 0.0060632 secs]
28 [Times: user=0.02 sys=0.00, real=0.01 secs]
29 2014-12-10T11:13:10.256+0800: 66955.977: Total time for which application threads were stopped: 0.0088462 seconds
30 2014-12-10T11:13:10.257+0800: 66955.977: [GC concurrent-cleanup-start]
31 2014-12-10T11:13:10.259+0800: 66955.979: [GC concurrent-cleanup-end, 0.0024743 secs

这次发生global concurrent marking的原因是:humongous allocation,上面提过在巨大对象分配之前,会检测到old generation 使用占比是否超过了 initiating heap occupancy percent(45%),因为 1449132032(used)+ 579608(allocation request:) > 1449551430(threshold),所以触发了本次global concurrent marking。对于具体执行过程,上面的表格已经详细讲解了。值得注意的是上文中所说的initial mark往往伴随着一次YGC,在日志中也有体现:GC pause (G1 Humongous Allocation) (young) (initial-mark)。

G1执行过程中的异常情况

巨型对象:在G1中,如果一个对象的大小超过分区大小的一半,该对象就被定义为巨型对象(Humongous Object)。巨型对象时直接分配到老年代分区,如果一个对象的大小超过一个分区的大小,那么会直接在老年代分配两个连续的分区来存放该巨型对象。巨型分区一定是连续的,分配之后也不会被移动——没啥益处。

由于巨型对象的存在,G1的堆中的分区就分成了三种类型:新生代分区、老年代分区和巨型分区,如下图所示:

如果一个巨型对象跨越两个分区,开始的那个分区被称为“开始巨型”,后面的分区被称为“连续巨型”,这样最后一个分区的一部分空间是被浪费掉的,如果有很多巨型对象都刚好比分区大小多一点,就会造成很多空间的浪费,从而导致堆的碎片化。如果你发现有很多由于巨型对象分配引起的连续的并发周期,并且堆已经碎片化(明明空间够,但是触发了FULL GC),可以考虑调整 -XX:G1HeapRegionSize参数,减少或消除巨型对象的分配。

关于巨型对象的回收:在JDK8u40之前,巨型对象的回收只能在并发收集周期的清除阶段或FULL GC过程中过程中被回收,在JDK8u40(包括这个版本)之后,一旦没有任何其他对象引用巨型对象,那么巨型对象也可以在年轻代收集中被回收。

G1启动了标记周期,但是在并发标记完成之前,就发生了Full GC,日志常常如下所示:

GC concurrent-mark-start开始之后就发生了FULL GC,这说明针对老年代分区的回收速度比较慢,或者说对象过快得从新生代晋升到老年代,或者说是有很多大对象直接在老年代分配。针对上述原因,我们可能需要做的调整有:调大整个堆的大小、更快得触发并发回收周期、让更多的回收线程参与到垃圾收集的动作中。

在GC日志中观察到,在一次混合收集之后跟着一条FULL GC,这意味着混合收集的速度太慢,在老年代释放出足够多的分区之前,应用程序就来请求比当前剩余可分配空间大的内存。针对这种情况我们可以做的调整:增加每次混合收集收集掉的老年代分区个数;增加并发标记的线程数;提高混合收集发生的频率。

在新生代垃圾收集快结束时,找不到可用的分区接收存活下来的对象,常见如下的日志:

这意味着整个堆的碎片化已经非常严重了,我们可以从以下几个方面调整:(1)增加整个堆的大小——通过增加 -XX:G1ReservePercent选项的值(并相应增加总的堆大小),为“目标空间”增加预留内存量;(2)通过减少 -XX:InitiatingHeapOccupancyPercent提前启动标记周期;(3) 你也可以通过增加 -XX:ConcGCThreads选项的值来增加并发标记线程的数目。

G1的调优

G1的调优目标主要是在避免FULL GC和疏散失败的前提下,尽量实现较短的停顿时间和较高的吞吐量。关于G1 GC的调优,需要记住以下几点:

1,不要自己显式设置新生代的大小(用 Xmn-XX:NewRatio参数),如果显式设置新生代的大小,会导致目标时间这个参数失效。

2,由于G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,即调整 -XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单,这里有个取舍:如果减小这个参数的值,就意味着会调小新生代的大小,也会导致新生代GC发生得更频繁,同时,还会导致混合收集周期中回收的老年代分区减少,从而增加FULL GC的风险。这个时间设置得越短,应用的吞吐量也会受到影响。

3,针对混合垃圾收集的调优。如果调整这期望的最大暂停时间这个参数还是无法解决问题,即在日志中仍然可以看到FULL GC的现象,那么就需要自己手动做一些调整,可以做的调整包括以下三种。

第一调整G1垃圾收集的后台线程数,通过设置 -XX:ConcGCThreads=n这个参数,可以增加后台标记线程的数量,帮G1赢得这场你追我赶的游戏;

第二调整G1垃圾收集器并发周期的频率,如果让G1更早得启动垃圾收集,也可以帮助G1赢得这场比赛,那么可以通过设置 -XX:InitiatingHeapOccupancyPercent这个参数来实现这个目标,如果将这个参数调小,G1就会更早得触发并发垃圾收集周期。这个值需要谨慎设置:如果这个参数设置得太高,会导致FULL GC出现得频繁;如果这个值设置得过小,又会导致G1频繁得进行并发收集,白白浪费CPU资源。通过GC日志可以通过一个点来判断GC是否正常——在一轮并发周期结束后,需要确保堆剩下的空间小于InitiatingHeapOccupancyPercent的值;

第三调整G1垃圾收集器的混合收集的工作量,即在一次混合垃圾收集中尽量多处理一些分区,可以从另外一方面提高混合垃圾收集的频率。在一次混合收集中可以回收多少分区,取决于三个因素:(1)有多少个分区被认定为垃圾分区, -XX:G1MixedGCLiveThresholdPercent=n这个参数表示如果一个分区中的存活对象比例超过n,就不会被挑选为垃圾分区,因此可以通过这个参数控制每次混合收集的分区个数,这个参数的值越大,某个分区越容易被当做是垃圾分区;(2)G1在一个并发周期中,最多经历几次混合收集周期,这个可以通过 -XX:G1MixedGCCountTarget=n设置,默认是8,如果减小这个值,可以增加每次混合收集收集的分区数,但是可能会导致停顿时间过长;(3)期望的GC停顿的最大值,由 MaxGCPauseMillis参数确定,默认值是200ms,在混合收集周期内的停顿时间是向上规整的,如果实际运行时间比这个参数小,那么G1就能收集更多的分区;

G1的最佳实践

关键参数:

  • -XX:+UseG1GC,告诉JVM使用G1垃圾收集器

  • -XX:MaxGCPauseMillis=200,设置GC暂停时间的目标最大值,这是个柔性的目标,JVM会尽力达到这个目标

  • -XX:INitiatingHeapOccupancyPercent=45,如果整个堆的使用率超过这个值,G1会触发一次并发周期。记住这里针对的是整个堆空间的比例,而不是某个分代的比例

还有不要根据平均响应时间(ART)来设置 -XX:MaxGCPauseMillis=n这个参数,应该设置希望90%的GC都可以达到的暂停时间。这意味着90%的用户请求不会超过这个响应时间,记住,这个值是一个目标,但是G1并不保证100%的GC暂停时间都可以达到这个目标;

参数选项:

常见问题:

Young GC、Mixed GC和Full GC的区别?

答:Young GC的CSet中只包括年轻代的分区,Mixed GC的CSet中除了包括年轻代分区,还包括老年代分区;Full GC会暂停整个引用,同时对新生代和老年代进行收集和压缩。

ParallelGCThreads和ConcGCThreads的区别?

答:ParallelGCThreads指得是在STW阶段,并行执行垃圾收集动作的线程数,ParallelGCThreads的值一般等于逻辑CPU核数,如果CPU核数大于8,则设置为 5/8*cpus,在SPARC等大型机上这个系数是5/16。;ConcGCThreads指的是在并发标记阶段,并发执行标记的线程数,一般设置为ParallelGCThreads的四分之一。

write barrier在GC中的作用?如何理解G1 GC中write barrier的作用? 写屏障是一种内存管理机制,用在这样的场景——当代码尝试修改一个对象的引用时,在前面放上写屏障就意味着将这个对象放在了写屏障后面。write barrier在GC中的作用有点复杂,我们这里以trace GC算法为例讲下:trace GC有些算法是并发的,例如CMS和G1,即用户线程和垃圾收集线程可以同时运行,即mutator一边跑,collector一边收集。这里有一个限制是:黑色的对象不应该指向任何白色的对象。如果mutator视图让一个黑色的对象指向一个白色的对象,这个限制就会被打破,然后GC就会失败。针对这个问题有两种解决思路:(1)通过添加read barriers阻止mutator看到白色的对象;(2)通过write barrier阻止mutator修改一个黑色的对象,让它指向一个白色的对象。write barrier的解决方法就是讲黑色的对象放到写write barrier后面。如果真得发生了white-on-black这种写需求,一般也有多种修正方法:增量得将白色的对象变灰,将黑色的对象重新置灰等等。我理解,增量的变灰就是CMS和G1里并发标记的过程,将黑色的对象重新变灰就是利用卡表或SATB的缓冲区将黑色的对象重新置灰的过程,当然会在重新标记中将所有灰色的对象处理掉。关于G1中write barrier的作用,可以参考R大的这个帖子里提到的。

G1里在并发标记的时候,如果有对象的引用修改,要将旧的值写到一个缓冲区中,这个动作前后会有一个write barrier,这段可否细说下? 答:这块涉及到SATB标记算法的原理,SATB是指start at the beginning,即在并发收集周期的第一个阶段(初始标记)是STW的,会给所有的分区做个快照,后面的扫描都是按照这个快照进行;在并发标记周期的第二个阶段,并发标记,这是收集线程和应用线程同时进行的,这时候应用线程就可能修改了某些引用的值,导致上面那个快照不是完整的,因此G1就想了个办法,我把在这个期间对对象引用的修改都记录动作都记录下来,有点像mysql的操作日志。

GC算法中的三色标记算法怎么理解? trace GC将对象分为三类:白色(垃圾收集器未探测到的对象)、灰色(活着的对象,但是依然没有被垃圾收集器扫描过)、黑色(活着的对象,并且已经被垃圾收集器扫描过)。垃圾收集器的工作过程,就是通过灰色对象的指针扫描它指向的白色对象,如果找到一个白色对象,就将它设置为灰色,如果某个灰色对象的可达对象已经全部找完,就将它设置为黑色对象。当在当前集合中找不到灰色的对象时,就说明该集合的回收动作完成,然后所有白色的对象的都会被回收。

总结

感谢网络大神提供这么多的资料供我们整理学习:

https://mp.weixin.qq.com/s/9-NFMt4I9Hw2nP0fjR8JCg

https://tech.meituan.com/2016/09/23/g1.html

https://www.jianshu.com/p/0f1f5adffdc1

https://hllvm-group.iteye.com/group/topic/44381#post-272188

https://mp.weixin.qq.com/s/ZwlT89vsvD2e0qEuxZto3Q

https://mp.weixin.qq.com/s?__biz=MzI1NDc5MzIxMw==&mid=2247485724&idx=1&sn=b73649b3d0f99a884e9d4ba4fa40775b&chksm=ea3e8d8edd490498b485942de59a56a0a84ed266f062c49f3e81e66feeb553827deb37944034&mpshare=1&scene=1&srcid=0212Fdz9Dc3clyCANRnIyYoi&sharer_sharetime=1581514736340&sharer_shareid=d40e8d2bb00008844e69867bcfc0d895#rd

https://mp.weixin.qq.com/s?__biz=MzU5ODUwNzY1Nw==&mid=2247484394&idx=1&sn=f6b3b9fbe7c956577de4d5d13bd59796&chksm=fe426a0cc935e31af283623c62e9eb519a94f0ac9a5e3ccf3e196210c500b072dd365a273b6d&scene=21#wechat_redirect

https://mp.weixin.qq.com/s?__biz=MzU5ODUwNzY1Nw==&mid=2247484428&idx=1&sn=8834ecdc082c64bd9614959fe96a3b9e&chksm=fe426deac935e4fc0222fca1820741517223d015a8c71e889c6316886abb08f76d6bb8d023db&mpshare=1&scene=1&srcid=0212dcUWw5hHNtReoprKUFoG&sharer_sharetime=1581514688834&sharer_shareid=d40e8d2bb00008844e69867bcfc0d895#rd

https://mp.weixin.qq.com/s?__biz=MzI5MTU1MzM3MQ==&mid=2247483927&idx=1&sn=0237dc5a32e1f9b80a87f7a276ec08cb&chksm=ec0fab23db782235bb372f3de27462d80b12ac8fe72cbe9d393d8d812c7ef0db0f3de9c63b11&mpshare=1&scene=1&srcid=&sharer_sharetime=1581291274013&sharer_shareid=d40e8d2bb00008844e69867bcfc0d895#rd

https://mp.weixin.qq.com/s?__biz=MzI5ODI5NDkxMw==&mid=2247492322&idx=2&sn=50cd8621e3a3a157413a74f7b172644a&chksm=ecaaa90cdbdd201a3e1f75afbeb1c246dc72bae52c390533fe3a8cd9f9538266ae5bc1caf792&mpshare=1&scene=1&srcid=&sharer_sharetime=1581204399771&sharer_shareid=d40e8d2bb00008844e69867bcfc0d895#rd

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章