jdk11-zgc-gc时间不断增长

1.现象

为了降低gc时间,我们打算对一批服务安装jdk11,使用zgc。在对zgc进行测试期间,发现随着程序的运行,gc时间越来越长。如下图所示:

每分钟gc总耗时

同时进程的gc次数并没有发生太大变化,如下图所示:

每分钟gc总次数

zgc的 gc.time 只和 GC Roots 相关,关于zgc可以参考:https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html

既然gc次数没有变多,所以应该是GC Roots 数量增长

2.原因分析

我们去查看近几天的gc日志,会发现 Subphase: Pause Roots ClassLoaderDataGraph  这个阶段的耗时会不断增加

gc日志

这时候开始怀疑GC Roots有问题

此时查看metaspace元空间监控,发现metaspace不断增长,metaspace里的对象是被认为GC Roots。所以因为metaspace不断增长,导致GC Roots越来越多,最终导致gc时间越来越长

元空间内存占用情况

为什么metasapce空间不断增长呢?

metaspace空间主要保存对象类信息,我们查看关于class相关监控信息

加载class信息监控

可以看到我们程序不断load class,但是没有unload class,所以导致meta内存一直增长

原因大概也就清楚了,但是此时产生疑问,为什么程序原先使用jdk8 cms gc算法,没有这个问题呢。

我们查看一下之前使用cms算法相关监控


cms机器监控信息

可以看到这台 cms 机器,也不断load class,但是他也会 unload class,所以metaspace一直维持平稳

因为我们在使用cms时,设置了一个jvm参数:CMSClassUnloadingEnabled 。该参数表示在进行cms,进行class卸载

3. zgc

为什么zgc不会收 meta:使用的jdk11不支持回收,jdk12才支持

在JDK11,zgc垃圾回收器目前还只是实验性的功能,只支持Linux/x64平台。后续优化接改进,短时间内无法更新到JDK11中,所以可能会遇到一些不稳定因素。例如: 1. JDK12支持并发类卸载功能。2. JDK13将可回收内存从4TB支持到16TB。3. JDK14提升稳定性的同时,提高性能。4. JDK15从实验特性转变为可生产特性 。所以对于一些大量使用反射、动态代理、CGLIB和Javasist等频繁自定义类加载器的场景中,ZGC难以处理Metaspace的巨大内存压力。

4.为什么metaspace一直升高

我们对运行的进程进行class统计

jcmd {pid} GC.class_stats

加载的class类信息

发现存在大量的jdk.internal.reflect.GeneratedConstructorAccessor class类

我们对上述的内容进行前缀统计,命令如下:

 jcmd {pid} GC.class_stats |awk '{print$13}'|sed  's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1

class前缀统计

发现 jdk.internal.reflect 包名下的类文件,占用了很大一部分,并且随着程序运行,jdk.internal.reflect这个类文件越来越多。

这个就是我们在使用jdk反射时,会自动生成的类字节码,被jvm加载进metaspace。随着程序运行,加载类字节码越来越多,但是没有释放,导致meta越来越大。

5.解决办法

-Dsun.reflect.inflationThreshold 可以控制通过反射生成字节码。(该值表示 反射调用多少次 才开始生成字节码)

当把该参数这是int 最大值时,说明永不生成字节码。

从stackoverflows摘抄一段话

When using Java reflection, the JVM has two methods of accessing the information on the class being reflected. It can use a JNI accessor, or a Java bytecode accessor. If it uses a Java bytecode accessor, then it needs to have its own Java class and classloader (sun/reflect/GeneratedMethodAccessor class and sun/reflect/DelegatingClassLoader). Theses classes and classloaders use native memory. The accessor bytecode can also get JIT compiled, which will increase the native memory use even more. If Java reflection is used frequently, this can add up to a significant amount of native memory use. The JVM will use the JNI accessor first, then after some number of accesses on the same class, will change to use the Java bytecode accessor. This is called inflation, when the JVM changes from the JNI accessor to the bytecode accessor. Fortunately, we can control this with a Java property. The sun.reflect.inflationThreshold property tells the JVM what number of times to use the JNI accessor. If it is set to 0, then the JNI accessors are always used. Since the bytecode accessors use more native memory than the JNI ones, if we are seeing a lot of Java reflection, we will want to use the JNI accessors. To do this, we just need to set the inflationThreshold property to zero.

If you are on a Oracle JVM then you would only need to set:  -Dsun.reflect.inflationThreshold=2147483647

If you are on IBM JVM, then you would need to set:   -Dsun.reflect.inflationThreshold=0

大概意思说:使用java反射,内部实现有2种方式,一种是jni,另一种是生成字节码方式。生成字节码方式会占用更多内存,但是性能会好一点。sun.reflect.inflationThreshold 代表,反射执行多少次后,开始使用字节码方式,当我们把这个参数设置为int最大值,代表永不使用字节码方式,也就没有内存问题了。

具体原理参考:

https://www.moregeek.xyz/i/880000804235

https://stackoverflow.com/questions/16130292/java-lang-outofmemoryerror-permgen-space-java-reflection

重新加上 -Dsun.reflect.inflationThreshold=2147483647 这个参数,后来gc.time 次数就一直稳定下来了,并且metaspace空间也不增长了

你可能感兴趣的:(jdk11-zgc-gc时间不断增长)