记一次JVMFullGC(MetadataGCThreshold)调优经历

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

记⼀次JVMFullGC(MetadataGCThreshold)调优经历
记⼀次JVM Full GC (Metadata GC Threshold)调优经历
⼀、背景:
线上服务器内存使⽤超过90%,分析上⾯部署的各个服务的GC⽇志,发现有⼀个服务的JVM内存分配过⼤,使⽤率较低,有调优的空间,可以在不迁移服务或者不升级服务器配置的情况下,降低服务器内存占⽤。

JVM推荐配置原则:
应⽤程序运⾏时,计算⽼年代存活对象的占⽤空间⼤⼩X。

程序整个堆⼤⼩(Xmx和Xms)设置为X的3 ~ 4倍;永久代PermSize和MaxPermSize设置为X的1.2 ~ 1.5倍。

年轻代Xmn的设置为X的1 ~ 1.5倍。

⽼年代内存⼤⼩设置为X的2 ~ 3倍。

JDK官⽅建议年轻代占整个堆⼤⼩空间的3/8左右。

完成⼀次Full GC后,应该释放出70%的堆空间(30%的空间仍然占⽤)。

观察线上
发现2G的堆内存,Full GC之后的活跃对象才占⽤60M。

按照推荐设置JVM内存只需要给⼏百M就好了。

所以决定改成1G,既能够降低服务器内存占⽤,也预留了⾜够的业务增长空间。

在这个过程中,发现如下⼏个问题:
GC⽇志没有时间显⽰,看起来很不⽅便
GC⽇志没有滚动,时间久了,⽇志⽂件较⼤
GC⽇志中存在⼤量Full GC (Metadata GC Threshold)
显然第三个问题最为严重。

我们知道,元数据区主要是⽤来存储类的元数据的。

⼀般来讲,类加载完成之后,⼤⼩应该是⽐较稳定的,不会有太⼤变动。

所以可以判断,这么频繁的Full GC (Metadata GC Threshold),肯定是哪⾥出问题了。

但是我们⼀步⼀步来解决问题,⽽且GC⽇志不够详细也影响我们定位问题。

⼆、优化GC⽇志打印
⾸先复习⼀下JVM的GC⽇志打印的启动参数。

详见
-verbose:gc
同-XX:+PrintGC
-XX:+PrintGC
最简单的 GC 参数会打印 GC 前后堆空间使⽤情况以及 GC 花费的时间
-XX:+PrintGCDetails
打印GC的详细信息,会打印 youngGC FullGC前后堆【新⽣代,⽼年代,永久区】的使⽤情况以及 GC 时⽤户态 CPU 耗时及系统CPU 耗时及 GC 实际经历的时间
-XX:+PrintGCTimeStamps
打印CG发⽣的时间戳,从应⽤启动开始累计的时间戳
-XX:+PrintGCDateStamps
打印GC发⽣的时刻,所处⽇期时间信息
-Xloggc:gc.log
指定GC log的位置,以⽂件输出
-XX:+PrintHeapAtGC
每⼀次GC前和GC后,都打印堆信息。

所以通过增加-XX:+PrintGCDateStamps解决我的第⼀个问题。

其次,JVM滚动记录GC⽇志参数如下:
-XX:UseGCLogFileRotation
打开或关闭GC⽇志滚动记录功能,要求必须设置 -Xloggc参数。

-XX:NumberOfGCLogFiles
设置滚动⽇志⽂件的个数,必须⼤于等于1。

⽇志⽂件命名策略是,.0, .1, ..., .n-1,其中n是该参数的值。

-XX:GCLogFileSize
设置滚动⽇志⽂件的⼤⼩,必须⼤于8k。

当前写⽇志⽂件⼤⼩超过该参数值时,⽇志将写⼊下⼀个⽂件。

不过需要注意的是,如果设置滚动,最好保留⾜够多的滚动⽇志,以免异常时的GC⽇志被冲掉。

三、Full GC排查
3.1 现象
最后,我们来解决Full GC (Metadata GC Threshold)的问题。

以下是系统重新启动之后,从GC⽇志中grep出的Full GC⽇志。

可以观察到如下⼏个现象:
前⼏次Full GC, 元数据区的空间并没有释放。

元数据区Full GC前的占⽤空间会逐渐增⼤⾄趋于⼀个⽐较稳定的值
Full GC过于频繁,⼏乎没30秒⼀次Full GC,这太夸张。

3.2 相关知识及分析
抱着这些疑问,我们学习⼀下metadata的启动参数配置项:
-XX:MetaspaceSize
这个参数是初始化的Metaspace⼤⼩,该值越⼤触发Metaspace GC的时机就越晚。

随着GC的到来,虚拟机会根据实际情况调控Metaspace的⼤⼩,可能增加上线也可能降低。

在默认情况下,这个值⼤⼩根据不同的平台在12M到20M浮动。

使⽤java -
XX:+PrintFlagsInitial命令查看本机的初始化参数,-XX:Metaspacesize为21810376B(⼤约20.8M)。

-XX:MaxMetaspaceSize
这个参数⽤于限制Metaspace增长的上限,防⽌因为某些情况导致Metaspace⽆限的使⽤本地内存,影响到其他程序。

在本机上该参数的默认值为4294967295B(⼤约4096MB)。

-XX:MinMetaspaceFreeRatio
当进⾏过Metaspace GC之后,会计算当前Metaspace的空闲空间⽐,如果空闲⽐⼩于这个参数,那么虚拟机将增长Metaspace的⼤⼩。

在本机该参数的默认值为40,也就是40%。

设置该参数可以控制Metaspace的增长的速度,太⼩的值会导致Metaspace增长的缓慢,Metaspace的使⽤逐渐趋于饱和,可能会影响之后类的加载。

⽽太⼤的值会导致Metaspace增长的过快,浪费内存。

-XX:MaxMetasaceFreeRatio
当进⾏过Metaspace GC之后,会计算当前Metaspace的空闲空间⽐,如果空闲⽐⼤于这个参数,那么虚拟机会释放Metaspace的部分空间。

在本机该参数的默认值为70,也就是70%。

-XX:MaxMetaspaceExpansion
Metaspace增长时的最⼤幅度。

在本机上该参数的默认值为5452592B(⼤约为5MB)。

-XX:MinMetaspaceExpansion
Metaspace增长时的最⼩幅度。

在本机上该参数的默认值为340784B(⼤约330KB为)。

那么我们可以得到结论:
前⼏次Full GC,是因为默认的-XX:MetaspaceSize 设置的太⼩⽽导致的。

我们可以根据⾃⼰的应⽤情况适当调⼤。

后⾯逐渐增⼤并动态稳定的原因是-XX:MinMetaspaceFreeRatio和-XX:MaxMetasaceFreeRatio这两个参数在起作⽤。

空闲⽐ = GC释放内存/GC前的⼤⼩,GC释放得太多,就缩⼩MetaSpaceSize,释放得太少,就扩⼤MetaSpaceSize。

同时,复习⼀下元数据区GC的相关知识:
Metaspace中的类需要满⾜什么条件才能够被当成垃圾被卸载回收?
条件还是⽐较严苛的,需同时满⾜如下三个条件的类才会被卸载:
该类所有的实例都已经被回收;
加载该类的ClassLoader已经被回收;
该类对应的ng.Class对象没有任何地⽅被引⽤。

从GC⽇志我们可以看到,Metaspace已使⽤内存在Full GC后明显变⼩(219105K -> 92087K),说明Metaspace经过Full GC 后,确实卸载了很多类。

从这点来看,我们有理由怀疑系统可能在频繁地⽣成⼤量”⼀次性“的类,导致Metaspace所占⽤空间不断增长,增长到GC阈值后触发Full GC。

那么这些被回收的类是什么呢?为了弄清楚这点,我增加了如下两个JVM启动参数来观察类的加载、卸载信息:
-XX:TraceClassLoading -XX:TraceClassUnloading
加了这两个参数后,系统跑了⼀段时间,从GC⽇志中发现⼤量如下的⽇志:
将Aviator与Full GC做⼀个关联搜索,果然遇到这个问题的,不⽌我⼀个。

同时也在作者的github上找到相关的.
3.3 源码解析
查看代码我们可以看到Aviator提供了两个调⽤接⼝:
public static Object execute(String expression, Map<String, Object> env, boolean cached) {
return getInstance().execute(expression, env, cached);
}
public static Object execute(String expression, Map<String, Object> env) {
return execute(expression, env, false);
}
深⼊源码:
public Expression compile(final String expression, final boolean cached) {
if (expression != null && expression.trim().length() != 0) {
if (cached) {
FutureTask<Expression> task = (FutureTask)this.cacheExpressions.get(expression);
if (task != null) {
return this.getCompiledExpression(expression, task);
} else {
task = new FutureTask(new Callable<Expression>() {
public Expression call() throws Exception {
return AviatorEvaluatorInstance.this.innerCompile(expression, cached);
}
});
FutureTask<Expression> existedTask = (FutureTask)this.cacheExpressions.putIfAbsent(expression, task);
if (existedTask == null) {
existedTask = task;
task.run();
}
return this.getCompiledExpression(expression, existedTask);
}
} else {
return this.innerCompile(expression, cached);
}
} else {
throw new CompileExpressionErrorException("Blank expression");
}
}
可以发现核⼼⽅法是innerCompile⽅法。

继续深⼊找到cached参数最底层的使⽤:
public AviatorClassLoader getAviatorClassLoader(boolean cached) {
return cached ? this.aviatorClassLoader : new AviatorClassLoader(Thread.currentThread().getContextClassLoader()); }
综上,我们发现
cached参数为true时,会优先从缓存中获取编译好的表达式对象。

同时使⽤编译表达式使⽤的类加载器也是同⼀个。

⽽cached为false时,每次执⾏表达式都会去编译表达式,且每次编译使⽤的是⼀个全新的类加载器。

这是导致元数据区加载太多"⼀次性"类的元凶。

四、问题解决
最终,在调⽤参数中,将cached设置为true,成功解决Full GC (Metadata GC Threshold)问题。

成功解决该问题,内存使⽤率由原先的90%多下降到80%左右这样⼀个⽐较健康的⽔平,能够给业务增长留有空间。

观察GC⽇志,也没有Full GC (Metadata GC Threshold)的⽇志了。

通过这次调优过程,学习巩固了⼀些 JVM GC相关的启动参数,并对Metadata元数据区的内存管理有了更加详细的认识。

相关文档
最新文档