Java默认垃圾回收器Parallel Scavenge导致Full GC增加

一、前言

当大家在部署一个新项目或者工程的时候,对于JVM参数,想必很多同学都会只配置几个容量大小相关的参数:xmx、xms、-XX:PermSize  -XX:MaxPermSize等。这样配置之后启动起来,貌似没有什么问题,但是随着程序的运行,问题就会慢慢而来。最终会导致Survivor Space变得很小(1G的新时代,Survivor Space可能只有几十兆),导致老年代不断增减。最终引起Full GC。且此过程会不断的重复,导致周期性的Full GC。从而导致应用不是出现响应慢的情况。接下来,我们就将会和大家一起分析下,为什么会出现这样的问题,以及我们在部署一个全新工程的时候需要注意哪些事项。

二、问题现象复现

1、默认垃圾回收器

其实这一切原因都是JDK1.7和JDK1.8的默认垃圾回收器惹的祸。在JDK1.7和JDK1.8中,如果我们不显示指定垃圾回收器,那么JDK就会让新生代和老年代默认都使用Parallel Scavenge垃圾处理器。即新生代为:Parallel Scavenge,老年代为:Parallel Old。这类垃圾处理器我们简称为PS垃圾处理器。

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

熟悉Java垃圾会搜器的同学都应该知道,PS垃圾回收器是一种侧重于CPU吞吐量的垃圾回收器。即它着重于让更多的CPU资源(CPU时钟)用于业务的运行。因此它不会关心某个时间点GC的时候的STW(Stop The World)的长短。

接着我们一起来看看使用默认的PS垃圾回收器会带来什么问题?

2、Parallel Scavenge问题复现过程

我们首先部署一个应用到JDK1.7的环境中,然后只配置如下JVM参数:

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

然后启动应用,通过jmap -heap查看其堆的分配情况。此时可以看到其使用了默认的PS垃圾回收器。且Eden Space=745.5M,Suvivor Space = 135.5M,此时的Eden :Survivor = 5。此时老年代为2048M。

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

接着我们查看当前晋升到老年的的年龄为多大。通过下图可以看出TT=4

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

当应用运行几个小时候之后(这个时间长短和各自应用的业务流量有一定的关系),我们再观察下,如下是我们的堆分配情况。我们可以看到Eden Space变成了963M,Suvivior减小到了31M,此时的Eden : Suvivor = 31。看着是不是觉得很神奇。

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

接着我们再看下现在对象晋升到老年的年龄为多少。通过下图可以看到TT变成了1。

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

通过下图的GC的历史监控曲线我们可以看出,老年的的大小不断增加,最终导致Full GC。然后老年代又会不断增加,最终引起Full  GC。这个过程会周而复始的进行,具体频率则与业务的请求量和老年的的大小有关。

Java默认垃圾回收器Parallel Scavenge导致Full GC增加

三、根因分析

1、为什么TT( TenuringThreshold)和Suvivor Space都会变小

其实和PS的特性有关。因为PS垃圾回收器是侧重于吞吐量的垃圾处理器。因此它了为了提高CPU的吞吐量,它就会想办法减少新时代的Young GC。让尽量多的对象移动到老年代去。然后找准时机直接一次STW,让后会受到所有垃圾。这样就可以让更少的CPU资源用于GC回收。让更多的CPU资源用于业务计算。当然它会导致在Full GC的时候应用的响应会变慢。那么我们要怎样才能够减少YounGC的时间,让对象都移动到老年代去给统一集中回收呢?熟悉垃圾回收流程的同学都应该能够想到,主要有两个办法:1、减少Suvivor Space的空间,让对象无法待在Survivor Space直接进入老年代。2、减少TT(移动到老年代的年龄),这样对象就能够在Suvivor之间复制1到2次就会被移动到老年代。

其实TT的变化在PS中是通过如下公式来决策的:

young_gc_time>full_gc_time*1.1,则threshold降低。即YoungGC的时间太多,就降低 TenuringThreshold的值,让更多的对象进入老年代。
full_gc_time>young_gc_time*1.1,则threshold提高。即Full GC的时间太多,则增加 TenuringThreshold的值,让更少的对象进入老年代。

2、怎么解决

上述就是为什么随着程序的运行,默认的PS垃圾回收器的Suvivor Space会减少到几十兆,TT会变成1的原因。那么是不是我们使用PS垃圾回收器,就一定会这样?有没有其他办法处理呢?当然是有的。在PS垃圾回收器中有个参数叫做UseAdaptiveSizePolicy。它代表每次Young GC后是否需要动态调整堆的大小(Eden Space、Suvivor Space和TT)。这个参数默认就是开启的,因此我们上面的例子中会出现Suvivor Space和TT变小的问题。所以如果我们不想使用让堆空间动态变化,则只需要显示的通过-XX:-UseAdaptiveSizePolicy关闭该功能即可。

虽然我们关闭了UseAdaptiveSizePolicy之后,堆空间不会动态变化,但是随着时间的推移老年代的总是会有被占满的时候,最后仍然会导致Full GC。且这个过程会随着时间的推移周而复始的进行下去。这样咱们的应用就会是不是的出现响应慢的情况。

四、垃圾回收器使用建议

通过上面的分析我们知道默认的PS可以关闭自适应Size策略,但是其最后还是会因为老年代满了导致Full GC。我们在互联网应用中几乎都重视程序响应时长,因此是无法接受周期性的Full GC。因此不建议使用默认的PS垃圾回收器。尽量手动配置CMS或者G1。

五、惯例

如果你对本文有任何疑问或者高见,欢迎添加公众号共同交流探讨(添加公众号可以获得”Java高级架构“上10G的视频和图文资料哦)。

Java默认垃圾回收器Parallel Scavenge导致Full GC增加