概述
G1 (Garbage-First) 于JDK 6u14版本公布,JDK 7u4版本发行时被正式推出,在JDK9时曾经成了默认的垃圾回收器,算是CMS回收器的代替 计划(CMS在JDK9当前曾经废除)
G1是一款分代的 (generational),增量的 (incremental),并行的 (parallel),移动式(evacuating)的,软实时的垃圾回收器。其最大特点是暂停工夫可配置,咱们能够配置一个最大暂停工夫,G1就会尽可能的在回收的同时保障程序的暂停工夫在容许范畴内,而且在大内存环境下体现更好。
在正式介绍G1 细节之前,先简略说说垃圾回收的一些基本知识:
垃圾回收的一些基础知识
mutator
在垃圾回收的里,mutator指应用程序。至于为什么叫这个奇怪的名字?
mutator 是 Edsger Dijkstra 推敲进去的词,有“扭转某物”的意思。说到要扭转什么,那
就是 GC 对象间的援用关系。不过光这么说可能大家还是不能了解,其实用一句话概括的话, 它的实体就是“应用程序”。这样说就容易了解了吧。GC 就是在这个mutator 外部精神饱满地 工作着。
增量垃圾回收
增量式垃圾回收(Incremental GC)是一种通过逐步推动垃圾回收来管制mutator 最 大暂停工夫的办法。
像一些晚期的年老代垃圾回收器,都是齐全暂停的,比方Serial GC
简略的说,增量垃圾回收,就是让GC程序和Mutator交替运行的方法,交替执行时,实际上垃圾是一点点回收的,所以叫“增量(Incremental)”
G1就属于一款增量垃圾回收器,它通过和mutator交替运行的形式来升高因GC导致的程序暂停工夫
并行GC和并发GC
并行GC(Parallel) 和并发(concurrent)GC是垃圾回收里的一个基本概念。这两个词很含糊,容易弄混同,不过在GC的畛域里,就用GC里的解释吧。
一般来说,以多线程执行的GC被称为并行/并发GC,不过这两个词在GC里意思齐全不同。
并行的GC会先暂停mutator,而后开启多个线程并行的执行GC,如下图所示:
而并发GC是在不暂停Mutator运行的同时,开启GC线程并行的执行GC,如下图所示:
并行GC的目标是晋升GC效率,缩短暂停工夫,而并发GC的目标是彻底干掉暂停工夫
可预测
G1的执行流程
G1 GC中的堆构造和其余回收器的有所不同,在G1中,堆被划分为N个大小的相等的区域(Region),每个区域占用一段间断的地址空间,以区域为单位进行垃圾回收,而且这个区域的大小是可配置的。在调配时,如果抉择的区域曾经满了,会主动寻找下一个闲暇的区域来执行调配。
G1是一个分代的垃圾回收器,同样的它将堆分为年老代(young)和老年代(old),将划分的区域又分为年老代区域和老年代区域。不过和其余垃圾回收器不同,G1中不同代的区域空间并不是间断的。
这里解释一下,为什么G1中不同代应用不间断的区域。因为G1 Heap中初始时只划分了区域,并没有给区域分类,在对象调配时,只须要从闲暇区域集(free-list)中选取一个存储对象即可,这样区域调配更灵便。
当产生GC时,EDEN区被清空,而后会作为一个闲暇的区域,这个区域待会可能会作为老年代,也可能会被作为Survivor区域。不过在G1在调配时还是会查看新生代的区域总大小是否超过新生代大小限度的,如果超出就会进行GC
尽管每个区域的大小是无限的,不过针对一些占用较大的大对象(humongous object),还是会存在跨区域的状况。对于跨区域的对象,会调配多个间断的区域。
G1中的区域,次要分为两种类型:
年老代区域
- Eden区域 - 新调配的对象
- Survivor区域 - 年老代GC后存活但不须要降职的对象
老年代区域
- 降职到老年代的对象
- 间接调配至老年代的大对象,占用多个区域的对象
G1中的堆构造如下图所示:
和其余的垃圾回收形式有所不同,G1的年老代/老年代的回收算法都是统一的,属于挪动/转移式回收算法。比方复制算法,就属于移动式回收算法,长处是没有碎片,存活的越少效率越高
RememberedSet
比方在对某个区域进行回收时,首先从GC ROOT开始遍历可中转这些区域中的对象,可因为降职或者挪动的起因,这些区域中的某些对象挪动到了其余区域,可是挪动之后依然放弃着对原区域对象的援用;那么此时原区域中被援用的对象对GC ROOT来说并不能“中转”,他们被其余对象的区域援用,这个发动援用的其余对象对于GC ROOT可达。这种状况下,如果想正确的标记这种GC ROOT不可中转但被其余区域援用的对象时就须要遍历所有区域了,代价太高。
如下图所示,如果此时堆区域A进行回收,那么须要标记区域A中所有存活的对象,可是A中有两个对象被其余区域援用,这两个灰色的问号对象在区域A中对GC ROOTS来是不可达的,然而实际上这两个对象的援用对象被GC ROOTS援用,所以这两个对象还是存活状态。此时如果不将这两个对象标记,那么就会导致标记的脱漏,可能造成误回收的问题
RememberedSet(简称RS或RSet)就是用来解决这个问题的,RSet会记录这种跨代援用的关系。在进行标记时,除了从GC ROOTS开始遍历,还会从RSet遍历,确保标记该区域所有存活的对象(其实不光是G1,其余的分代回收器里也有,比方CMS)
如下图所示,G1中利用一个RSet来记录这个跨区域援用的关系,每个区域都有一个RSet,用来记录这个跨区援用,这样在进行标记的时候,将RSet也作为ROOTS进行遍历即可
所以在对象降职的时候,将降职对象记录下来,这个存储跨区援用关系的容器称之为RSet,在G1中通过Card Table来实现。
留神,这里说Card Table实现RSet,并不是说CardTable是RSet背地的数据结构,只是RSet中存储的是CardTable数据
Card Table
在G1 堆中,存在一个CardTable的数据,CardTable 是由元素为1B的数组来实现的,数组里的元素称之为卡片/卡页(Page)。这个CardTable会映射到整个堆的空间,每个卡片会对应堆中的512B空间。
如下图所示,在一个大小为1 GB的堆下,那么CardTable的长度为2097151 (1GB / 512B);每个Region 大小为1 MB,每个Region都会对应2048个Card Page。
那么查找一个对象所在的CardPage只须要简略的计算就能够得出:
介绍完了CardTable,上面说说G1中RSet和CardTable如何配合工作。
每个区域中都有一个RSet,通过hash表实现,这个hash表的key是援用本区域的其余区域的地址,value是一个数组,数组的元素是援用方的对象所对应的Card Page在Card Table中的下标。
如下图所示,区域B中的对象b援用了区域A中的对象a,这个援用关系跨了两个区域。b对象所在的CardPage为122,在区域A的RSet中,以区域B的地址作为key,b对象所在CardPage下标为value记录了这个援用关系,这样就实现了这个跨区域援用的记录。
不过这个CardTable的粒度有点粗,毕竟一个CardPage有512B,在一个CardPage内可能会存在多个对象。所以在扫描标记时,须要扫描RSet中关联的整个CardPage。
写入屏障
写入屏障 (Write Barrier) 也是GC里的一个关键技术(不是linux里的membarrier),当产生援用关系的更新时,通过写入屏障来(这里指转移用的写入屏障)记录这个援用关系的变更,只是一系列函数而已,就像这样(伪代码):
def evacuation_write_barrier(obj, field, newobj){ //查看援用和被援用新对象是否在同一个区域 if(!check_cross_ref(obj, newobj)){ return } //不反复增加dirty_card if(is_dirty_card(obj)){ return } to_dirty(obj); //将obj增加到newobj所在region的rs add_to_rs(obj, newobj);}
为了便于了解,下面的伪代码屏蔽了一些细节,理解外围工作内容即可
不过在G1里,不止一种写入屏障,像后面介绍的SATB也是有的写入屏障,这里不做过多介绍
分代回收
G1中有两种回收模式:
- 齐全年老代GC(fully-young collection),也称年老代垃圾回收(Young GC)
- 局部年老代GC(partially-young collection)又称混合垃圾回收(Mixed GC)
齐全年老代GC是只抉择年老代区域(Eden/Survivor)进入回收汇合(Collection Set,简称CSet)进行回收的模式。年老代GC的过程和其余的分代回收器差不多,新创建的对象调配至Eden区域,而后将标记存活的对象挪动至Survivor区,达到降职年龄的就降职到老年代区域,而后清空原区域(不过这里可没有年老代复制算法中两个Survivor的替换过程)。
年老代GC会抉择所有的年老代区域退出回收汇合中,然而为了满足用户进展工夫的配置,在每次GC后会调整这个最大年老代区域的数量,每次回收的区域数量可能是变动的
上面是一个齐全年老代GC过程的简略示意图:将抉择的年老代区域中所有存活的对象,挪动至Survivor区域,而后清空原区域
下面只是一个繁难的回收过程示意,接下来具体介绍年老代的回收过程
年老代垃圾回收(齐全年老代GC)
当JVM无奈将新对象调配到eden区域时,会触发年老代的垃圾回收(年老代垃圾回收是齐全暂停的,尽管局部过程是并行,但暂停和并行并不抵触)。也会称为“evacuation pause”
步骤1. 抉择收集汇合(Choose CSet),G1会在遵循用户设置的GC暂停工夫下限的根底上,抉择一个最大年老带区域数,将这个数量的所有年老代区域作为收集汇合。
如下图所示,此时A/B/C三个年老代区域都曾经作为收集汇合,区域A中的A对象和区域B中的E对象,被ROOTS间接援用(图上为了简略,将RS间接援用到对象,实际上RS援用的是对象所在的CardPage)
步骤2. 根解决(Root Scanning),接下来,须要从GC ROOTS遍历,查找从ROOTS直达到收集汇合的对象,挪动他们到Survivor区域的同时将他们的援用对象退出标记栈
如下图所示,在根解决阶段,被GC ROOTS间接援用的A/E两个对象间接被复制到了Survivor区域M,同时A/E两个对象所援用路线上的所有对象,都被退出了标记栈(Mark Stack),这里包含E->C->F,这个F对象也会被退出标记栈中
步骤3. RSet扫描(Scan RS),将RSet作为ROOTS遍历,查找可直达到收集汇合的对象,挪动他们到Survivor区域的同时将他们的援用对象退出标记栈
在RSet扫描之前,还有一步更新RSet(Update RS)的步骤,因为RSet是先写日志,再通过一个Refine线程进行解决日志来保护RSet数据的,这里的更新RSet就是为了保障RSet日志被解决实现,RSet数据残缺才能够进行扫描
如下图所示,老年代区域C中援用年老代A的这个援用关系,被记录在年老代的RSet中,此时遍历这个RS,将老年代C区域中D对象援用的年老代A中的B对象,增加到标记栈中
步骤4. 挪动(Evacuation/Object Copy),遍历下面的标记栈,将栈内的所有所有的对象挪动至Survivor区域(其实说是挪动,实质上还是复制)
如下图所示,标记栈中记录的C/F/B对象被挪动到Survivor区域中
当对象年龄超过降职的阈值时,对象会间接挪动到老年代区域,而不是Survivor区域。
对象挪动后,须要更新援用的指针,这块具体的做法能够参考我的另一篇文章《垃圾回收算法实现之 - 复制(残缺可运行C语言代码)》
收尾步骤. 剩下的就是一些收尾工作,Redirty(配合上面的并发标记),Clear CT(清理Card Table),Free CSet(清理回收汇合),清空挪动前的区域增加到闲暇区等等,这些操作个别耗时都很短
混合回收(局部年老代GC)
混合回收,也称局部年老代GC,会抉择所有年老代区域(Eden/Survivor)(最大年老代分区数)和局部老年代区域进去回收汇合进行回收的模式。年老代区域对象挪动到Survivor区,老年代区域挪动到老年代区域。因为G1中老年代区域的回收形式和新生代一样是“移动式”,被回收区域在挪动后会全副清空,所以不会像其余应用革除算法的回收器一样(比方CMS)有碎片问题。
上面是一个局部年老代GC过程的简略示意图:
混合回收的执行过程次要蕴含两步:
- 并发标记(concurrent marking) - 增量式并发的标记存活对象,标记过程中Mutator的援用更新也会被标记
- 挪动/转移(evacuation)- 和年老代的挪动过程统一,复用代码,最大的不同是将并发标记的后果也进行解决
并发标记
并发标记的目标是标记存活对象,为挪动过程做筹备。在并发标记的过程中,存活对象的标记和Mutator的运行是并发进行的。所以这个标记过程中援用变动的更新,是并发标记过程中最简单的局部。
G1的并发标记设计,是基于CMS回收器的,所以整体标记过程和CMS中的并发标记很像。并发标记会对区域内所有的存活对象进行标记,那么未标记的对象就是垃圾须要回收(这里“并发”的目标是为了升高Mutator的暂停工夫)
当老年代应用的内存加上本次行将调配的内存占到总内存的45%,就会启动混合回收,进行并发标记。
并发标记并不是间接在对象上进行标记,而是用了一个独立的数据容器 - 标记位图(MarkBitMap),对立的对区域中的所有对象进行标记,在挪动时通过这个位图就能够判断对象是否存活
标记位图
每个区域内都有两个标记位图(Mark Bitmap):next和prev。next是本次标记的标记位图,而prev是上次标记的标记位图,保留上次标记的后果。
标记位图就是通过对象地址,映射到标记位图中的数据位,标记位图中的每一个bit都代表一个对象的标记状态,如下图所示:
每个区域中,还有4个标记地位的指针,别离是bottom,top,nextTAMS,prevTAMS。因为是并发标记,标记的同时Mutator会调配对象、批改援用关系,而且并发标记(这里指的是并发标记子阶段)会被年老代GC所中断,中断后持续就须要基于上次中断的点持续标记,所以这几个指针能够了解为记录变动点,有点游戏里暂存点的意思。
如下图所示 ,某个区域在进行标记前。bottom代表这个区域的底部,top示意区域内存的顶部(即使用量),TAMS(Top-at-Mark-Start,标记开始时的top),prevTAMS和nextTAMS即上/下一次的标记信息
在标记时,如果该区域又调配了一些对象,那么top指针就会挪动,那么此时top-nextTAMS就是标记过程中的新对象
并发标记的过程分为以下几个步骤:
初始标记(Initial Mark)
标记由根间接援用的对象(STW),这个过程是在年老代GC中实现的,不过不是每次年老代GC都会进行初始标记。
并发标记(Concurrent Mark)
以步骤1的标记后果作为root,遍历可达的对象进行标记,和mutator并行,并且可被年老代GC中断,年老代GC实现后可持续进行标记
SATB
SATB (Snapshot At The Beginning,初始快照),是一种将并发标记阶段开始时对象间的援用关系,以逻辑快照的模式进行保留的伎俩
这个解释有点……形象,简略了解就是,在并发标记时,以以后的援用关系作为根底援用数据,不思考Mutator并发运行时对援用关系的批改(Snapshot命名的由来),标记时是存活状态就认为是存活状态,同时利用SATB Write Barrier记录援用变动。具体的SATB解释,能够参考我的另一篇文章《SATB的一些了解》
最终标记 (Remark)
标记脱漏的对象,次要是SATB相干**(STW)
清理(Cleanup)
计算标记区域的流动对象数量,清理没有存活对象的区域(标记后没有存活对象,并不是正经的回收阶段),对区域排序等(局部STW)
混合收集
这里的混合收集,是指混合回收GC下的回收过程。在并发标记实现后,就能够进行混合收集了(mixed),混合收集阶段和年老代GC统一,从并发标记的后果/ROOTS/RSet遍历回收存活对象即可,只是多了老年代区域的回收。
Full GC
当混合回收无奈跟上内存调配的速度,导致老年代也满了,就会进行Full GC对整个堆进行回收。G1中的Full GC也而是单线程串行的,而且是全暂停,应用的是标记-整顿算法,代价十分高。
暂停工夫的管制
G1在挪动过程中尽管也是全暂停,不过G1在抉择回收汇合上是变动的,每次只抉择局部的区域进行回收,通过计算每个区域的预测暂停工夫来保障每次回收所占用的工夫。简略的说就是将一次残缺的GC拆分成屡次短时间的GC从而升高暂停的工夫,尽量保障每次的暂停工夫在用户的配置范畴(-XX:MaxGCPauseMilli)内。
年老代大小的配置
G1为了管制暂停工夫,年老代最大区域数是动静调整的,不过如果手动设置了年老代大小,比方Xmn/MaxNewSize/NewRatio等,并且年老代最大和最小值一样,那么相当于禁用了这个最大区域数调整的性能,可能会导致暂停工夫管制的生效(因为年老代GC是抉择全副区域的,区域过多会导致暂停工夫的减少)。
所以G1中尽量不要设置年老代的大小,让G1主动的进行调整
日志解读
年老代GC日志(齐全年老代)
//[GC pause (G1 Evacuation Pause) (young) 代表齐全年老代回收// 0.0182341 secs 是本次GC的暂停工夫0.184: [GC pause (G1 Evacuation Pause) (young), 0.0182341 secs 是本次GC的暂停工夫]// 并行GC线程,一共有8个 [Parallel Time: 16.7 ms, GC Workers: 8] /*这一行信息阐明的是这8个线程开始的工夫,Min示意最早开始的线程工夫,Avg示意均匀开始工夫,Max示意的是最晚开始工夫,Diff为最早和最晚的时间差。这个值越大阐明线程启动工夫越不平衡。线程启动的工夫依赖于GC进入平安点的状况。对于平安点能够参考后文的介绍。*/ [GC Worker Start (ms): 184.2 184.2 184.2 184.3 184.3 184.4 186.1 186.1 Min: 184.2, Avg: 184.7, Max: 186.1, Diff: 1.9] /*根解决的工夫,这个工夫蕴含了所有强根的工夫,分为Java根,别离为Thread、JNI、CLDG;和JVM根上面的StringTable、Universe、JNI Handles、ObjectSynchronizer、FlatProfiler、Management、SystemDictionary、JVMTI */ [Ext Root Scanning (ms): 0.3 0.2 0.2 0.1 0.1 0.0 0.0 0.0 Min: 0.0, Avg: 0.1, Max: 0.3, Diff: 0.3, Sum: 0.8] /*Java线程解决工夫,次要是线程栈。这个工夫蕴含了根间接援用对象的复制工夫,如果根超级大,这个工夫可能会减少 */ [Thread Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [StringTable Roots (ms): 0.0 0.1 0.1 0.1 0.1 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.4] [Universe Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [JNI Handles Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [ObjectSynchronizer Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [FlatProfiler Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Management Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [SystemDictionary Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [CLDG Roots (ms): 0.3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.3, Diff: 0.3, Sum: 0.3] [JVMTI Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] // CodeCache Roots实际上是在解决Rset的时候的统计值,它蕴含上面的 // UpdateRS,ScanRS和Code Root Scanning [CodeCache Roots (ms): 5.0 3.9 2.2 3.3 2.1 2.2 0.6 2.2 Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.6] [CM RefProcessor Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Wait For Strong CLD (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Weak CLD Roots (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [SATB Filtering (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] // 这个就是GC线程更新RSet的工夫破费,留神这里的工夫和咱们在Refine外面解决RSet // 的工夫没有关系,因为它们是不同的线程解决 [Update RS (ms): 5.0 3.9 2.2 3.3 2.1 2.2 0.6 2.2 Min: 0.6, Avg: 2.7, Max: 5.0, Diff: 4.4, Sum: 21.5] // 这里就是GC线程解决的白区中的dcq个数 [Processed Buffers: 8 8 7 8 8 7 2 4 Min: 2, Avg: 6.5, Max: 8, Diff: 6, Sum: 52] // 扫描RSet找到被援用的对象 [Scan RS (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Code Root Scanning (ms): 0.0 0.0 0.0 0.0 0.0 0.1 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1] // 这个就是所有活着的对象(除了强根间接援用的对象,在Java根解决时会间接复制)复制 // 到新的分区破费的工夫。从这里也能够看出复制基本上是最破费工夫的操作。 [Object Copy (ms): 11.3 12.5 14.2 13.1 14.3 14.2 14.2 12.5 Min: 11.3, Avg: 13.3, Max: 14.3, Diff: 3.0, Sum: 106.3] // GC线程完结的工夫信息。 [Termination (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Termination Attempts: 1 1 1 1 1 1 1 1 Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 8] // 这个是并行处理时其余解决所破费的工夫,通常是因为JVM析构开释资源等 [GC Worker Other (ms): 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] // 并行GC破费的总体工夫 [GC Worker Total (ms): 16.6 16.6 16.6 16.5 16.5 16.4 14.7 14.7 Min: 14.7, Avg: 16.1, Max: 16.6, Diff: 1.9, Sum: 128.7] // GC线程完结的工夫信息 [GC Worker End (ms): 200.8 200.8 200.8 200.8 200.8 200.8 200.8 200.8 Min: 200.8, Avg: 200.8, Max: 200.8, Diff: 0.0] // 上面是其余工作局部。 // 代码扫描属于并行执行局部,蕴含了代码的调整和回收工夫 [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] // 革除卡表的工夫 [Clear CT: 0.1 ms] [Other: 1.5 ms] // 抉择CSet的工夫,YGC通常是0 [Choose CSet: 0.0 ms] // 援用解决的工夫,这个工夫是发现哪些援用对象能够革除,这个是能够并行处理的 [Ref Proc: 1.1 ms] // 援用从新激活 [Ref Enq: 0.2 ms] // 重构RSet破费的工夫 [Redirty Cards: 0.1 ms] [Parallel Redirty: 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [Redirtied Cards: 8118 7583 6892 4496 0 0 0 0 Min: 0, Avg: 3386.1, Max: 8118, Diff: 8118, Sum: 27089] // 这个信息是是能够并行处理的,这里是线程重构RSet的数目 // 大对象解决工夫 [Humongous Register: 0.0 ms] [Humongous Total: 2] // 这里阐明有2个大对象 [Humongous Candidate: 0] // 可回收的大对象0个 // 如果有大对象要回收,回收破费的工夫,回收的个数 [Humongous Reclaim: 0.0 ms] [Humongous Reclaimed: 0] // 开释CSet中的分区破费的工夫,有新生代的信息和老生代的信息。 [Free CSet: 0.0 ms] [Young Free CSet: 0.0 ms] [Non-Young Free CSet: 0.0 ms] // GC完结后Eden从15M变成0,下一次应用的空间为21M,S从2M变成3M,整个堆从 // 23.7M变成20M [Eden: 15.0M(15.0M)->0.0B(21.0M) Survivors: 2048.0K->3072.0K Heap: 23.7M(256.0M)->20.0M(256.0M)]
老年代垃圾回收(局部年老代/混合回收)日志
并发标记日志
并发标记是全局的,和回收过程是两个阶段,所以并发标记能够说是独立的。
//并发标记 - 初始标记阶段,在年老代GC中实现100.070: [GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0751469 secs] [Parallel Time: 74.7 ms, GC Workers: 8] [GC Worker Start (ms): Min: 100070.4, Avg: 100070.5, Max: 100070.6, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2, Sum: 1.6] [Update RS (ms): Min: 0.6, Avg: 1.1, Max: 1.5, Diff: 0.9, Sum: 8.9] [Processed Buffers: Min: 1, Avg: 1.6, Max: 4, Diff: 3, Sum: 13] [Scan RS (ms): Min: 1.0, Avg: 1.4, Max: 1.9, Diff: 0.9, Sum: 10.8] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Object Copy (ms): Min: 71.5, Avg: 71.5, Max: 71.6, Diff: 0.1, Sum: 572.1] [Termination (ms): Min: 0.3, Avg: 0.3, Max: 0.4, Diff: 0.1, Sum: 2.6] [Termination Attempts: Min: 1382, Avg: 1515.5, Max: 1609, Diff: 227, Sum: 12124] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2] [GC Worker Total (ms): Min: 74.5, Avg: 74.5, Max: 74.6, Diff: 0.1, Sum: 596.3] [GC Worker End (ms): Min: 100145.1, Avg: 100145.1, Max: 100145.1, Diff: 0.0] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.1 ms] [Other: 0.4 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.1 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.1 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms] [Eden: 23.0M(23.0M)->0.0B(14.0M) Survivors: 4096.0K->4096.0K Heap: 84.5M (128.0M)->86.5M(128.0M)][Times: user=0.63 sys=0.00, real=0.08 secs]// 把YHR中Survivor分区作为根,开始并发标记根扫描100.146: [GC concurrent-root-region-scan-start]// 并发标记根扫描完结,破费了0.0196297,留神扫描和Mutator是并发进行,同时有多个线程并行100.165: [GC concurrent-root-region-scan-end, 0.0196297 secs]// 开始并发标记子阶段,这里从所有的根援用:包含Survivor和强根如栈等登程,对整个堆进行标记100.165: [GC concurrent-mark-start]// 标记完结,破费0.08848s100.254: [GC concurrent-mark-end, 0.0884800 secs]// 这里是再标记子阶段,包含再标记、援用解决、类卸载解决信息100.254: [GC remark 100.254: [Finalize Marking, 0.0002228 secs] 100.254: [GC ref-proc, 0.0001515 secs] 100.254: [Unloading, 0.0004694 secs], 0.0011610 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]// 革除解决,这里的革除仅仅回收整个分区中的垃圾// 这里还会调整RSet,以加重后续GC中RSet根的解决工夫100.255: [GC cleanup 86M->86M(128M), 0.0005376 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
混合回收日志
// 混合回收Mixed GC其实和YGC的日志相似,能看到GC pause(G1EvacuationPause)(mixed)这样的信息// 日志剖析参考Y年老代GC。 122.132: [GC pause (G1 Evacuation Pause) (mixed), 0.0106092 secs] [Parallel Time: 9.8 ms, GC Workers: 8] [GC Worker Start (ms): Min: 122131.9, Avg: 122132.0, Max: 122132.0, Diff: 0.1] [Ext Root Scanning (ms): Min: 0.1, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.7] [Update RS (ms): Min: 0.5, Avg: 0.7, Max: 0.9, Diff: 0.4, Sum: 5.4] [Processed Buffers: Min: 1, Avg: 1.8, Max: 3, Diff: 2, Sum: 14] [Scan RS (ms): Min: 1.0, Avg: 1.3, Max: 1.5, Diff: 0.5, Sum: 10.4] [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0] [Object Copy (ms): Min: 7.5, Avg: 7.6, Max: 7.7, Diff: 0.2, Sum: 60.9] [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [Termination Attempts: Min: 92, Avg: 105.1, Max: 121, Diff: 29, Sum: 841] [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1] [GC Worker Total (ms): Min: 9.7, Avg: 9.7, Max: 9.8, Diff: 0.1, Sum: 77.6] [GC Worker End (ms): Min: 122141.7, Avg: 122141.7, Max: 122141.7, Diff: 0.0] [Code Root Fixup: 0.0 ms] [Code Root Purge: 0.0 ms] [Clear CT: 0.2 ms] [Other: 0.7 ms] [Choose CSet: 0.0 ms] [Ref Proc: 0.1 ms] [Ref Enq: 0.0 ms] [Redirty Cards: 0.5 ms] [Humongous Register: 0.0 ms] [Humongous Reclaim: 0.0 ms] [Free CSet: 0.0 ms] [Eden: 3072.0K(3072.0K)->0.0B(5120.0K) Survivors: 3072.0K->1024.0K Heap: 105.5M(128.0M)->104.0M(128.0M)][Times: user=0.00 sys=0.00, real=0.01 secs]
罕用参数
这里只列出了一些最根底的参数。大多数状况下的GC调优,调的只是一些内存/比例/工夫,对各种线程数调整的场景很少,默认参数下也足够了。
# 启动G1-XX:+UseG1GC# 最小堆内存-Xms8G# 最大堆内存-Xmx8G# metaspace初始值-XX:MetaspaceSize=256M# 冀望的最大暂停工夫,默认200ms-XX:MaxGCPauseMillis# 简称为IHOP,默认值为45,这个值是启动并发标记的阈值,当老年代应用内存占用堆内存的45%启动并发标记。# 如果该过大,可能会导致mixed gc跟不上内存调配的速度从而导致full gc-XX:InitiatingHeapOccupancyPercent# G1主动调整IHOP的指,JDK9之后可用-XX:+G1UseAdaptiveIHOP# 并发标记时能够卸载Class,这个操作比拟耗时,对Perm/MetaSpace进行清理,默认未开启-XX:+ClassUnloadingWithConcurrentMark# 多个线程并行执行java.lang.Ref.*,对象回收前的援用解决-XX:-ParallelRefProcEnabled
和其余回收器的比照
- 和Parallel GC相比,G1的增量并行回收暂停工夫更短
- 和CMS相比,G1因为采纳移动式算法,所以没有碎片问题,更无效的利用了内存
总结
G1的全称是Garbage First,意思是“垃圾优先”。什么叫垃圾优先呢?
在并发标记时,会依据存活对象的数量/大小,对标记的区域进行降序排序。到了挪动过程时,就会优先选择挪动效率高(垃圾多,存活对象少,须要挪动的就少)的区域作为回收汇合,这就是Garbage First命名的由来
但G1并不属于一个高效率的回收器,对老年代应用移动式的回收算法,尽管没有碎片问题,但效率是较低的。因为老年代对象大多数是存活的,所以每次回收须要挪动的对象很多。而革除算法中是革除死亡的对象,所以从效率上来看,革除算法在老年代中会更好。
然而因为G1这个可管制暂停的增量回收,能够保障每次暂停工夫在容许范畴内,对于大多数利用来说,暂停工夫比吞吐量更重要。再加上G1的各种细节优化,效率曾经很高了。
垃圾回收系列贴(残缺可运行C语言代码)
- 垃圾回收算法实现之 - 标记-革除(残缺可运行C语言代码)
- 垃圾回收算法实现之 - 援用计数(残缺可运行C语言代码)
- 垃圾回收算法实现之 - 复制(残缺可运行C语言代码)
- 垃圾回收算法实现之 - 标记-整顿(残缺可运行C语言代码)
- 垃圾回收算法实现之 - 分代回收(残缺可运行C语言代码)
参考 & 材料
- 《深刻Java虚拟机:JVM G1GC的算法与实现》 - 中村成洋 (作者) 吴炎昌 , 杨文轩 (译者)
- 《垃圾回收的算法与实现》- 中村成洋 , 相川光 , 竹内郁雄 (作者) 丁灵 (译者)
- 《JVM G1源码剖析和调优》- 彭成寒 著
- 《Java Performance Companion》- Charlie Hunt, Monica Beckwith, Poonam Parhar, Bengt Rutisson
- Garbage-First Garbage Collector - Oracle
- Getting Started with the G1 Garbage Collector - Oracle
- Understanding the JDK’s New Superfast Garbage Collectors
- G1: One Garbage Collector To Rule Them All - InfoQ
- GC Algorithms: Implementations - Plumbr
- JVM Garbage Collectors Benchmarks Report 19.12
- https://www.redhat.com/en/blog/part-1-introduction-g1-garbage-collector
- [[HotSpot VM] 求教G1算法的原理 - R大](https://hllvm-group.iteye.com...
- [[HotSpot VM] 对于incremental update与SATB的一点了解 - R大](https://hllvm-group.iteye.com...
- 本人对于VM的帖的目录 - ITEYE - R大
原创不易,转载请在结尾驰名文章起源和作者。如果我的文章对您有帮忙,请点赞珍藏激励反对。原文链接https://segmentfault.com/a/1190000039411521/edit