JVM第五周 G1垃圾回收初步理解

·  阅读 167
JVM第五周 G1垃圾回收初步理解

G1垃圾回收器工作原理

ParNew + CMS垃圾回收器的痛点:Stop The World!对系统的运行有一定的影响!

其实之后对垃圾回收器的优化,都是朝着减少"Stop the World"的目标去做的。

在这个基础之上,G1垃圾回收器就应运而生了,他可以提供比“ParNew+CMS"组合更好的垃圾回收的性能。

G1垃圾回收器

G1垃圾回收器可以同时回收新生代和老年代对象,不需要像CMS+ParNew一样 多个垃圾回收器配合工作。

最大特点

  1. 把Java堆内存分成多个大小相等的Region

    G1其实也有新生代和老年代的概念,但都是逻辑上的概念。

  2. 可以设置垃圾回收停顿的时间(Stop The World time cost)

    比如我们可以指定,希望G1同志在垃圾回收的时候,可以保证,在1小时内由G1垃圾回收导致的"Stop the World”时间,也就是系统停顿的时间,不能超过1分钟。

    现在我们直接可以给G1指定,在一个时间内,垃圾回收导致的系统停顿时间不能超过多久,G1全权给你负责,保证达到这个目标。

如何做到对垃圾回收导致的系统停顿可控?

G1必须要追踪每个Region里的回收价值

回收价值

G1必须搞清楚每个Region里的对象有多少是垃圾,如果对这个Region进行垃圾回收,需要耗费多长时间,可以回收掉多少垃圾。

比如一个Region中的垃圾对象有10MB,回收它们需要花费1秒钟;另一个Region中的垃圾对象有20MB,回收它们需要花费200毫秒。

然后在垃圾回收的时候,G1会发现在最近一个时间段内,比如1小时内,垃圾回收已经导致了几百毫秒的系统停顿了,现在又要执行一次垃圾回收,那么必须是回收那个只需要200ms就能回收掉20MB垃圾的Region啊

于是G1触发一次垃圾回收,虽然可能导致系统停顿了200ms,但是一下子回收了更多的垃圾,就是20MB的垃圾。

深度理解G1

如何设定G1对应的内存大小

G1对应的是一大堆的Region内存区域,每一个Region的大小是一致的。

  • 到底有多少个Region呢?
  • 每个Region的大小是多少呢?

默认情况下是自动计算的。我们一般会使用“-Xms -Xmx”来设置堆内存的大小。

JVM启动时,发现使用的是G1垃圾回收器("-XX:+UseG1GC"),此时会自动将堆大小除以2048;

因为JVM最多可以有2048个Region,然后Region的大小必须是2的倍数,例如 1MB, 2MB,4MB。

比如说堆大小是4G,那么就是4096MB,此时除以2048个Region,每个Region的大小就是2MB。大概就是这样子来决定Region的数量和大小的,大家一般保持默认的计算方式就可以。

如果通过手动方式来指定,则是"-XX:G1HeapRegionSize"

刚开始时,默认新生代对堆内存的比率是5%,也就是占据4096MB*5%约等于200MB,大概对应100个Region

新生代对堆内存的比率 可以调整,-XX:G1NewSizePercent。一般采用默认值 %5即可。

因为在系统不停的运行,JVM其实会不停地给新生代增加更多的Region,但新生代的Region最多不超过60%

-XX:G1MaxNewSizePercent

而且一旦Region进行了垃圾回收,此时新生代的Region数量还会减少(新生代Region数量是动态的)

Eden和Survivor

其实在G1中虽然把内存划分为了很多的Region,但是其实还是有新生代、老年代的区分而且新生代里还是有Eden和Survivor的划分的。

“-XX:SurvivorRatio=8",这里还是可以区分出来属于新生代的Region里哪些属于Eden,哪些属于Survivor。

比如新生代之前说刚开始初始的时候,有100个Region,那么可能80个Region就是Eden,两个Survivor各自占10个Region。

Eden和Survivor区分别占据不同的Region。

只不过随着对象不停的在新生代里分配,属于新生代的Region会不断增加,Eden和Survivor对应的Region也会不断增加。

G1新生代垃圾回收

随着不停地在新生代Eden对应的Region中创建对象,JVM就不停地给新生代加入更多的Region。直到新生代占据堆内存的最大比率为60%。

一旦新生代达到了设定的占据堆内存的最大大小60%,假设目前有1200个Region了,Eden占有1000个,每个Survivor占据100个。而且Eden区占满了对象

此时会触发G1新生代垃圾回收,G1就会采用复制算法,Stop The World。然后Eden区将对应的Region中的存活对象放入S1对应的Region中来,接着回收掉Eden对应的Region中的垃圾对象。

因为G1是可以指定 停顿时间的,也就是G1执行垃圾回收时最多可以让系统停顿多长时间,可以通过-XX:MaxGCPauseMills 来设置,默认是200ms。

因此G1就会对每个Region追踪回收它需要多长时间,可以回收多少垃圾对象来选择回收一部分的Region;来保证G1停顿时间控制在指定范围内,尽可能多的回收掉一些对象。

对象什么时候进入老年代

G1的内存模型下,新生代和老年代都会占据一定的Region。

按照默认的新生代最多只能占据堆内存60%的Region来推算,老年代最多占据40%的Region,大概800个Region。

1. 对象在新生代躲过了多次垃圾回收,达到了一定的年龄

-XX:MaxTenuringThreshold

2. 动态年龄判定规则

如果一旦发现某次新生代GC过后,存活对象超过了Survivor的50%; 此时就会判断一下,比如年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象全部会进入老年代。

3. 对象在Survivor对应的Region中放不下了,就会让对象进入老年代

G1中的大对象

G1提供了专门的Region来存放大对象。

大对象判定规则

当一个大对象超过了一个Region的50%,假设每个Region大小为2MB,那么一个对象超过了1MB,那就会放进大对象专门的Region。

如果一个对象足够大,大的超过了一个Region的大小,比如10M,那么就可能会横跨多个Region来存放。

堆内存中哪些Region来存放大对象呢?

在G1里,新生代和老年代的Region是不停的变化的。比如新生代现在占据了1200个Region,但是一次垃圾回收之后,就让里面1000个Region都空了,此时那1000个Region就可以不属于新生代了,里面很多Region可以用来存放大对象。

大对象所在的Region既不属于新生代,也不属于老年代,那么什么时候会触发垃圾回收呢?

其实新生代、老年代在回收的时候,会顺带带着大对象Region一起回收,所以这就是在G1内存模型下对大对象的分配和回收的策略。

思考:从新生代的GC来看,G1垃圾回收器相对于ParNew最大的进步在哪

可以做到对垃圾回收导致的停顿可控。

什么时候触发新生代+老年代的混合回收(Mixed GC)

G1有一个参数,是"-XX:InitiatingHeapOccupancyPercent”,默认值是45%。

如果老年代占据了堆内存的45%的Region的时候,此时就会尝试触发一个新生代+老年代一起回收的混合回收阶段。

堆内存有2048个Region,如果老年代占据了45%,也就是接近1000个Region时,就会开始触发混合回收。

混合回收过程

1. 初始标记

首先会触发一个“初始标记”的操作,这个过程是需要进入“Stop the World"的,仅仅只是标记一下GC Roots直接能引用的对象,这个过程速度是很快的。

先停止系统程序的运行,然后对各个线程栈内存中的局部变量代表的GCRoots,以及方法区中的类静态变量代表的GC Roots,进行扫描,标记出来他们直接引用的那些对象。

  • 方法局部变量
  • 类的静态变量
2. 并发标记

这个阶段会允许系统程序的运行,同时进行GC Roots追踪,从GCRoots开始追踪所有的存活对象。

耗时较长,但不会停止应用程序的运行。

而且JVM会对并发标记阶段对对象做出的一些修改记录起来,比如说哪个对象被新建了,哪个对象失去了引用。

3. 最终标记

这个阶段会进入“Stop the World",系统程序是禁止运行的,但是会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对象。

4. 混合回收

这个阶段会计算老年代中每个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。 接着会停止系统程序,然后全力以赴尽快进行垃圾回收,此时会选择部分Region进行回收因为必须让垃圾回收的停顿时间控制在我们指定的范围内。

比如说老年代此时有1000个Region都满了,但是因为根据预定目标,本次垃圾回收可能只能停顿200毫秒,那么通过之前的计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region,把GC导致的停顿时间控制在我们指定的范围内。

此时垃圾回收不仅仅是回收老年代,还会回收新生代,还会回收大对象。

此时就不仅仅回收老年代对应的Region了,就要看情况了,因为我们设定了对GC停顿时间的目标,所以说他会从新生代、老年代、大对象里各自挑选一些Region,保证用指定的时间(比如200ms)回收尽可能多的垃圾,这就是所谓的混合回收。

G1的一些参数

一般在老年代的Region占据了堆内存的Region的45%之后,会触发一个混合回收的过程,也就是Mixed GC。

-XX:G1MixedGCCountTarget

混合回收阶段,会Stop The World,停止所有程序运行,而G1是允许执行多次混合回收的

比如先停止工作,执行一次混合回收回收掉一些Region,接着恢复系统运行,然后再次停止系统运行,再执行一次混合回收回收掉一些Region。(反正在前三个阶段,已经标记好了垃圾对象)

该参数可以设置在一次混合回收(Mixed GC)中,在最后一个阶段执行几次(混合回收),默认为8次。

假设一次混合回收预期要回收掉一共有160个Region,那么此时第一次混合回收,会回收掉一些Region,比如就是20个Region。

接着恢复系统运行一会儿,然后再执行一次“混合回收”,再次回收掉20个Region。

如此反复执行8次混合回收阶段之后,不就把预订的160个Region都回收掉了?而且还把系统停顿时间控制在指定范围内。

Q: 为什么要回收多次呢?

因为你停止系统一会儿,回收掉一些Region,再让系统运行一会儿,然后再次停止系统一会儿,再次回收掉一些Region,这样可以尽可能让系统不要停顿时间过长,可以在多次回收的间隙,也运行一下。

-XX:G1HeapWastePercent

默认值为5%

在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉。

这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了

而且从这里也能看出来G1整体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不需要像CMS那样标记-清理之后,再进行内存碎片的整理。

-XX:G1MixedGCLiveThresholdPercent

他的默认值是85%,意思就是确定要回收的Region的时候,必须是存活对象低于85%的Region才可以进行回收

否则要是一个Region的存活对象多余85%,你还回收他干什么?这个时候要把85%的对象都拷贝到别的Region,这个成本是很高的。

回收失败时的Full GC

如果在进行Mixed回收的时候,无论是年轻代还是老年代都基于复制算法进行回收,都要把各个Region的存活对象拷贝到别的Region里去。

此时万一出现拷贝的过程中发现没有空闲Region可以承载自己的存活对象了,就会触发一次失败

一旦失败,立马就会切换为停止系统程序,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

思考:G1垃圾回收相关问题

  1. G1垃圾回收使用时,可以优化的点在哪
  2. 什么时候可能会导致G1频繁的触发Mixed混合垃圾回收?
  3. 如何尽量减少Mixed GC的频率
分类:
后端
标签: