24、JVM 调优实战 - G1分代回收原理深度图解:为什么回收性能比传统GC更好?

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

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

 

问题:到底有多少个Region呢?每个Region的大小是多大呢

Region的数量和大小,在默认情况下是自动计算和设置的。也可以给整个堆内存设置一个大小,比如说用 “-Xms” 和 “-Xmx” 来设置堆内存的大小。

然后JVM启动的时候一旦发现你使用的是G1垃圾回收器,可使用 “-XX:+UseG1GC” 来指定使用 G1垃圾回收器,此时会自动用堆大小除以2048。

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

比如堆内存大小是4G,就是4096MB,除以2048个Region,每个Region的大小就是 2MB。这是默认的计算方式。

通过手动方式来指定,则是 “-XX:G1HeapRegionSize” ,如下图:

 

刚开始,默认新生代对堆内存的占比是 5%,也就是占据200MB的内存,对应大概100个Region;可以通过 “-XX:G1NewSizePercent” 来设置新生代初始占比。

在系统运行中,JVM会不停地给新生代增加更多的 Region,最多新生代的占比不超过 60%,可以通过 “-XX:G1MaxNewSizePercent” 设置。

一旦Region进行垃圾回收,新生代的 Region 数量就会减少。

3. 新生代还有Eden和 Survivor的概念吗?

在G1中也是有新生代、老年代的区分的。而新生代里还是有Eden和Survivor的划分的。

根据新生代的参数 “-XX:SurvivorRatio=8” ,是可以区分出来属于新生代的 Region 里哪些属于 Eden,哪些属于 Survivor。

比如新生代初始的时候,有 100个 Region,那么其中80个Region就是 Eden,两个 Survivor 各占10个 Region。如下图:

 

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

4. G1的新生代垃圾回收

G1的新生代中有 Eden 和 Survivor ,其触发垃圾回收的机制都是类似的。

G1中新生代所占用的 Region 占据堆大小的最大比例 60%,一旦超过该值,就会触发新生代的 GC, G1 会用复制算法来进行垃圾回收,进入 STW 状态。

然后把Eden 对应的 Region中的存活对象放入 S1对应的 Region中,接着回收掉 Eden对应的 Region中的垃圾对象。

在上述过程中,G1是可以设定目标GC停顿时间的,也就是 G1 执行 GC的时候最多可以让系统停顿多长时间,可以通过“-XX:MaxGCPauseMills” 参数来设定,默认值是 200ms。

然后G1 会通过对每个 Region 追踪回收它需要多少时间,可以回收多少对象来选择回收一部分的 Region,保证GC停顿时间停顿在指定范围内,尽可能多的回收掉一些对象。

5. 对象什么时候进入老年代?

在G1的内存模型中,老年代也有自己的 Region。按照新生代占据堆内存 60%的 Region计算,老年代最多可以占据剩余的 40%的Region,也就是800个Region。

对象想要从新生代进入老年代需要满足以下几个条件

1、 对象在新生代躲过了很多次的垃圾回收,达到了一定的年龄, “-XX:MaxTenuringThreshold” 参数可以设置这个年龄,它就会进入老年代

2、 动态年龄判定规则,一旦发现某次新生代 GC 过后,存活对象超过了 Survivor 的50%

综上所述,经过一段时间的新生代使用和垃圾回收之后,总有一些对象会进入老年代中。

 

6. 大对象 Region

G1提供了专门的 Region 来存放大对象,而不是让大对象进入老年代的 Region中。

在G1中,大对象的判定规则就是一个大对象超过了一个 Region大小的 50%,就会被放入大对象专门的 Region 中。

如果一个大对象太大,可能会横跨多个 Region 来存放。

哪些Region用来存放大对象?

答:当新生代触发一次垃圾回收后,空下来的 Region 就会被用来存放大对象。

大对象既然不属于新生代和老年代,那什么时候会触发垃圾回收

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