你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:
- 了解大厂经验
- 拥有和大厂相匹配的技术等 希望看什么,评论或者私信告诉我!
文章目录
- 一、前言
- 二、 metaspace
- 2.1 metaspace 是什么
- 2.2 metaspace 如何设置
- 三、结合 GC 日志分析问题
- 3.1 问题分析
- 源码分析
- 3.3 源码解析
- 四、问题解决
- 五、总结
一、前言
上一篇文章 我们通过 增大 metaspace 的内存,让程序跑了起来,不影响线上使用。今天呢,就彻底解决 metaspace 不断增长的问题
二、 metaspace
2.1 metaspace 是什么
这是 java 的内存模型,从 1.8 开始metaspace 替代方法区,用于存储类定义、方法数据、字段数据等元数据,并且呢它是从 是从本机内存中分配的,大小不固定。
也就是说如果不断的创建类等,metaspace 总会被撑爆的
2.2 metaspace 如何设置
对于 java 来说
- `-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为)。
对于 Flink 来说
taskmanager.memory.jvm-metaspace.size=1024mb
三、结合 GC 日志分析问题
3.1 问题分析
2024-05-08T11:11:35.075+0800: 22.379: [GC (Metadata GC Threshold) [PSYoungGen: 428583K->21165K(2160128K)] 451157K->43747K(7097344K), 0.0344272 secs] [Times: user=0.08 sys=0.02, real=0.04 secs]
2024-05-08T11:11:35.109+0800: 22.413: [Full GC (Metadata GC Threshold) [PSYoungGen: 21165K->0K(2160128K)] [ParOldGen: 22581K->35384K(4937216K)] 43747K->35384K(7097344K), [Metaspace: 34235K->34235K(1079296K)], 0.2253439 secs] [Times: user=0.54 sys=0.03, real=0.22 secs]
发现 Full GC 前后 Metaspace 的内存大小并没有变化,很是奇怪,
为了弄清楚为啥,我增加了如下两个JVM启动参数来观察类的加载、卸载信息:
-XX:TraceClassLoading -XX:TraceClassUnloading
加了这两个参数后,系统跑了一段时间,从GC日志中发现大量如下的日志:
另外在 aviatorscript 也发现了对应的 issues
源码分析
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)
问题
五、总结
本文介绍了Java中Metaspace的相关知识和参数设置,同时提供了解决Metaspace内存泄漏的方案。同时,通过分析GC日志,发现了使用Aviator表达式引擎时可能导致Metaspace内存泄漏的问题,并提供了解决方案。