G1基本原理及日志解析

1,184 阅读14分钟

一、基本概念

1. 概述

G1名字的由来

G1会尽可能的搜集垃圾比较多的region。所以叫garbage first。

G1的特点

  • 达10G或者更大的内存

  • 对象分配和晋升效率更高

  • 碎片化问题显著改善

  • 尽可能的满足-XX:MaxGCPauseMillis目标

2. region

G1将内存分配成很多大小相同的region(大约是2048个,每个region的大小在1~32M,region的大小会根据目标耗时动态调整)。这些region就是内存分配和垃圾回收的基本单位。在G1中eden和survivor和其他收集器的功能是一样的,但他们的地址空间不是连续的,虽然其他搜集器的eden和survivor地址空间是连续的。老年代包含“humongous”区域,humongous区域可能横跨多个region。

-XX:G1HeapRegionSize= The set of the heap region size based on initial and maximum heap size. So that heap contains roughly 2048 heap regions. The size of a heap region can vary from 1 to 32 MB, and must be a power of 2.

ergo是ergonomic的缩写。ergonomic是人类工程学的意思。见下图:

可以认为是,比较舒服、比较舒适的方式。在G1参数里面,可以理解为“最佳参数”、“工程参数”的意思。

应用程序总是先在eden区域分配对象。如果是humongous对象的话,直接在Humongous区域分配(Humongous objects are objects larger or equal the size of half a region)。

上图中灰色的是空的内存空间,红色的是eden区,红色中有S的是survivor区,浅蓝色是老年代区(包含连续region的Humongous区)。

3. evacuation

G1 reclaims space mostly by using evacuation: live objects found within selected memory areas to collect are copied into new memory areas, compacting them in the process. After an evacuation has been completed, the space previously occupied by live objects is reused for allocation by the application.

G1将“搜集目标区域”的存活对象拷贝到新的区域,并在拷贝过程中进行压缩。

为什么说G1会减少内存碎片呢?比如相对CMS来讲,CMS在老年代执行的清除算法,而不是拷贝算法。CMS清除的时候,只是将垃圾清除了,这并没有对老年代进行压缩,清除后导致很多“空洞”,造成内存的碎片化。而G1完全是拷贝算法,不会造成内存碎片。

evacuation这个单词是“疏散”的意思。将人从一个地方疏散、移动到另一个地方。

在G1中,可以理解为,将可达对象,从一个region疏散、移动到另一个地方。

本次GC将Eden和survivor evacuation到了新的region中。

4. cset

G1要搜集哪些内存,是靠CSET(Collection set)决定的。G1在搜集时,会将全部的年轻代和垃圾最多的老年代(具体选多少老年代还跟-XX:MaxGCPauseMillis目标有关)作为Cset。

G1是通过控制一次搜集region的个数达到“最大停顿时间”目标的。但是当我们搜集region A、B、C的时候,怎么知道region D、E有没有引用呢?需要进行全堆扫描吗?全堆扫描岂不是很慢?

G1有一个叫做Remembered set(RSet)的概念。就是每个region都有一个RSet用来表示其他region对于本region对象的引用。

需要注意的是,如果在并发标记中,已经认定为垃圾的老年代对象对该对象的引用是无效的。

And, finally, the live objects are moved to survivor regions, creating new if necessary. The now empty regions are freed and can be used for storing objects in again.

5. RSET

G1有很多的region,每个region又被分成了多个card。默认每个card是512B。当regionA的card中的对象引用了其他regionB的对象时,会将该card标记为dirty,并在目标regionB的RSet中记录该card的地址,这样在回收regionB时就知道是不是有其他region在引用该region的对象,减少全堆扫描。

其实,当把region1的card置为dirty的时候,并没有立即在region2的Rset中立即更新指针引用。因为在并发场景下,region2的Rset是全局共享资源,如果立即更新的话,可能加锁会比较重。当把region1的card置为dirty后,会将该card表放入dirty card queue中,有G1的refine(优化)线程去消费这个队列。但是具体消费原理,是通过dirty card queue的着色方式实现的。

Another option to increase throughput is to try to decrease the amount of concurrent work in particular, concurrent remembered set updates often require a lot of CPU resources. Increasing -XX:G1RSetUpdatingPauseTimePercent moves work from concurrent operation into the garbage collection pause. In the worst case, concurrent remembered set updates can be disabled by setting -XX:-G1UseAdaptiveConcRefinement -XX:G1ConcRefinementGreenZone=2G -XX:G1ConcRefinementThreads=0. This mostly disables this mechanism and moves all remembered set update work into the next garbage collection pause.

增加吞吐量的一个方式就是尽可能的降低并发工作的数量。而并发的更新RSet经常需要很多的CPU资源。可以增加参数-XX:G1RSetUpdatingPauseTimePercent的数值,使得这种RSet的更新尽可能的在GC停顿的过程中完成。最坏的情况,可以关闭并发更新Rset,这样可以将所有的RSet更新工作转移到下一次的GC停顿中完成。

引用上面一段是为了说明:RSET的更新有可能是与用户线程并发的,但是GC STW时也会更新RSET(在GC日志中有体现)。

To maintain the remembered sets, during the runtime of the application, a Post-Write Barrier is issued whenever a write to a field is performed. If the resulting reference is cross-region, i.e. pointing from one region to another, a corresponding entry will appear in the Remembered Set of the target region. To reduce the overhead that the Write Barrier introduces, the process of putting the cards into the Remembered Set is asynchronous and features quite a number of optimizations. But basically it boils down to the Write Barrier putting the dirty card information into a local buffer, and a specialized GC thread picking it up and propagating the information to the remembered set of the referred region.

boil down to词组意思是:归结为。

propagate的本意应该是繁殖的意思,可以延伸为 “传播”的意思。

6. 三色标记法

G1的并发标记阶段是怎么进行的?注意,GC都是标记的存活对象,只是将存活对象进行了拷贝,垃圾是还在原来的地方的,可能根本没有进行清空,因为会用新数据去覆盖这些垃圾对象。

将被标记的对象用三个颜色区分:

  • 黑色:根对象,或,对象本身及其子对象已经被标记完。

  • 灰色:对象本身被标记,但是子对象还没有被标记或者没有被标记完。

  • 白色:对象没有被标记,将会被作为垃圾存在。

三色标记的过程:

经过上面这一步可以发现:原来灰色的对象变成了黑色,原来灰色引用的白色对象变成了灰色。

经过上面一步可以看到,上面的白色对象,就是最后的垃圾对象了。

需要注意的是,G1的标记过程是与应用程序并发执行的,那么这个过程可能存在Lost Object Problem(丢失对象问题,即不是垃圾的对象没有被标记上,误认为是垃圾,被误回收),也有可能出现浮动垃圾问题(本来应该是垃圾的对象,被误标记了)。

下面这一步,GC线程被挂起,应用线程执行。

上面这一步,GC线程被调度执行。但是对象A已经是黑色的了,表示其自身及其子对象已经被标记完了,而对象B的子对象指针已经置空了,使得C对象成为了白色对象。但是对象C对应用线程而言,却实实在在被对象A引用着。但是按照这种标记算法,对象C将会被回收掉。显然是不行的。

7. SATB

G1 marking uses an algorithm called Snapshot-At-The-Beginning (SATB) . It takes a virtual snapshot of the heap at the time of the Initial Mark pause, when all objects that were live at the start of marking are considered live for the remainder of marking. This means that objects that become dead (unreachable) during marking are still considered live for the purpose of space-reclamation (with some exceptions). This may cause some additional memory wrongly retained compared to other collectors. However, SATB potentially provides better latency during the Remark pause. The too conservatively considered live objects during that marking will be reclaimed during the next marking.

G1标记使用SATB算法。G1在初始标记暂停阶段,做一个堆内存的备份,如果标记之初这些对象是存活的,那么标记的剩余阶段这些对象也被认为保持存活阶段(即使后面某些对象已经不可达了,这次也回收不掉)。这导致一些内存被错误地保留了下来。但是这种算法使得在remark阶段获得更好的吞吐。这些被错误标记的存活对象,会在下次标记的时候被回收。

SATB(Snapshot-At-The-Beginning)就是指,在标记之前,先进行一次Snapshot(快照),也就是GC的三色标记过程都是在这个snapshot中进行的。有了这个snapshot,那么上面三色标记案例中被遗漏的对象C就不会被漏标记了,因为在snapshot中,总是有对象B到对象C的引用。

但是问题来了,由于标记是与应用线程同时进行的,那么,如果应用线程对snapshot的白对象并发的进行了引用,怎么办?标记线程怎么感知到?还记得在讲RSET的时候说的write barrier吗?这里也是用到了write barrier,当进行了新的复制操作,会被记录下来,在remark阶段被重新标记上。

还有个问题,如果应用线程创建了新对象,这个新对象也没有在snapshot中,怎么办?每个region记录着两个top-at-mark-start(TAMS)指针,分别为prevTAMS和nextTAMS。在TAMS以上的对象就是新分配的,因而被视为隐式marked。

8. write barrier

在CSET和SATB小节中都说到了write barrier。都是在对象赋值时,用到了write barrier。其实,write barrier就是JVM在进行赋值语句时,额外加的一小段代码,用来记录一些额外的信息,比如在CSET中,A.b = B,需要在B所在的region的CSET中记录到A对象所在region的card的引用。

二、GC日志分析

1. Evacuation Pause: Fully Young

当年轻代满的时候,会进行一次YGC,将eden区的内容拷贝到survivor区。但是年轻代大小是多少,eden和survivor之间的比例是多少?官方给出解释,不要配置这些。年轻代的大小是G1实现“最大停顿时间”目标的关键。

If you prefer high throughput, then relax the pause-time goal by using -

XX:MaxGCPauseMillis or provide a larger heap. 要吞吐的话,就把XX:MaxGCPauseMillis参数调大,或提供大的堆内存。

If latency is the main requirement, then modify the pause-time target. Avoid limiting the young generation size to particular values by using options like -Xmn, -XX:NewRatio and others because the young generation size is the main means for G1 to allow it to meet the pause-time. Setting the young generation size to a single value overrides and practically disables pausetime control. 要延迟的话,就修改“最大停顿时间”。不要使用-Xmn, -XX:NewRatio配置。

年轻代与老年代的比例关系还是有一个参数可以配置的。

年轻代的大小会在这两个值之间动态变化。

  • user – Total CPU time that was consumed by Garbage Collector threads during this collection

  • sys – Time spent in OS calls or waiting for system event

  • real – Clock time for which your application was stopped. With the parallelizable activities during GC this number is ideally close to (user time + system time) divided by the number of threads used by Garbage Collector. In this particular case 8 threads were used. Note that due to some activities not being parallelizable, it always exceeds the ratio by a certain amount.

  • 用户时间:G1的线程在搜集期间使用的时间。这个时间是G1每个线程使用时间的总和。

  • 系统时间:GC过程中在内核态花费的时间或者等待系统事件的时间。

  • 墙上时间:这个是实实在在花费的时间,约等于(user + sys)/ gcThreadsCount。一般会比这个公式计算出的时间略长,因为不是所有的操作都是并行执行的。

上面GC日志描述的就是并行搜集年轻代的过程。比较关键的点:

  • 并行搜集的时间(这个是墙上时间,是实实在在的时间消耗)和并行搜集的线程数(这个也很关键,不要太大)。

  • GC worker start time:这个显示了GC并行搜集的开始时间(是JVM启动到现在的时间流逝,基本与这次GC的开始时间一致,也就是0.134: [GC pause (G1 Evacuation Pause) (young), 0.0144119 secs] 中的0.134)。这里面描述了各个G1 worker线程的启动时间差异,如果min和max相差很大的话,需要引起注意:有可能是GC worker线程数太大,有可能是机器负载太高,机器上的其他进程在偷该进程GC的CPU时间。

  • GC worker end time:跟 GC worker start time同样道理。

2. G1并发标记

当老年代的空间使用率超过阈值 InitiatingHeapOccupancyPercent(IPOP)时,会进行并发标记。

默认的,G1会开始自适应的IPOP开关(-XX:+G1UseAdaptiveIHOP),以通过观察标记过程的耗时和老年代的分配情况,自适应的计算最佳的IPOP值。可以看出IPOP的计算是以“标记”过程作为依据的。如果此时还没有标记过,总不能一直都不标记吧?第一次IPOP的值是:当前老年代占用的最大值减去-XX:G1HeapReservePercent。

当自适应的IPOP开关是关闭的时候(-XX:+G1UseAdaptiveIHOP),总是使用-XX:InitiatingHeapOccupancyPercent参数作为阈值(默认是45%)。

并发标记的整个过程:

  • 初识标记:标记从GC root可达的所有存活对象。其实G1的初识标记阶段就是YGC的一个阶段。

再讲一个单词:piggy-backed,可以理解为在...的后背、基础上,沾光,搭便车的意思。

  • 扫描root region。

  • 并发标记阶段

  • remark。stop the world阶段。

  • clean up。有可能会有stop the world阶段。这个阶段会并发的回收全free的region,以及并发地计算存活对象的比例,以对这些region进行排序,为garbage first做好准备。

3. Evacuation Pause: Mixed

在并发标记阶段的clean up时,如果释放了全部的老年代,那么就不用mixed阶段了。mixed阶段并不是在并发标记阶段之后立即执行。通常,在并发标记和mixed之间又多次的YGC。mixed阶段不仅会搜集年轻代还会搜集老年代。

通过前面的并发标记,GC计算出了各个old region中垃圾的比例,选择垃圾最多的old region放入CSET中。在CSET中的老年代region,这些old region中存活对象的比例都在-XX:G1MixedGCLiveThresholdPercent=85(Old generation regions with higher live object occupancy than this percentage aren't collected in this space-reclamation phase.)之下。

一次mix GC虽然根据垃圾占比选择old region,但是一次mix搜集多少个old region,可以通过参数-XX:G1OldCSetRegionThresholdPercent=10(Sets an upper limit on the number of old regions to be collected during a mixed garbage collection cycle. The default is 10 percent of the Java heap. )进行配置。

由于mix gc执行的拷贝算法,也就是将存活的对象拷贝到新的region中,如果old region中存活的对象比例很高,这种拷贝是很费时的,可以忽略掉。可以通过参数-XX:G1HeapWastePercent=10(Sets the percentage of heap that you are willing to waste. The Java HotSpot VM does not initiate the mixed garbage collection cycle when the reclaimable percentage is less than the heap waste percentage. The default is 10 percent. )配置。也就是,当堆中有10%的垃圾时,不进行mix gc。

当有跨region引用的情况时,需要将引用写入到被应用对象所在region的RSet中,这个过程是异步的。

三、实战

在一次线上中,GC日志中出现了多次to-space exhausted。

这个是因为一次GC之后要进行拷贝的时候发现to空间不足以存放下来,需要进行一次FGC。reserve就是用来预留空间的。这样不会进行FGC。

-XX:G1ReservePercent=10

Sets the percentage of reserve memory to keep free so as to reduce the risk of to-space overflows. The default is 10 percent. When you increase or decrease the percentage, make sure to adjust the total Java heap by the same amount.

原始参数
-Xmx4096m -Xms4096m -verbose:gc -Xloggc:/home/logs/gc-19164.log 
-XX:+AlwaysPreTouch -XX:+UnlockExperimentalVMOptions 
-XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/logs  
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=1 
-XX:G1ReservePercent=10 
-XX:-OmitStackTraceInFastThrow 
-XX:+UseCGroupMemoryLimitForHeap

优化后的参数
-Xmx6144m -Xms6144m -verbose:gc -Xloggc:/home/logs/gc-19164.log 
-XX:+AlwaysPreTouch -XX:+UnlockExperimentalVMOptions 
-XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps 
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/logs 
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 
-XX:G1ReservePercent=20 
-XX:-OmitStackTraceInFastThrow 
-XX:+UseCGroupMemoryLimitForHeap 
-XX:InitiatingHeapOccupancyPercent=40 
-XX:MaxGCPauseMillis=40

8月5号的数据为优化后的数据,7月31号的数据为优化前的数据。

从"gc总耗时"和“gc最大耗时”可以看到,优化前后还是有很大的效果的。

参考:

硬广告

欢迎关注公众号:double6