你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:
- 了解大厂经验
- 拥有和大厂相匹配的技术等 希望看什么,评论或者私信告诉我!
一、前言
上一篇文章 我们通过 增大 metaspace 的内存,让程序跑了起来,不影响线上使用。今天呢,就彻底解决 metaspace 不断增长的问题
二、 metaspace
2.1 metaspace 是什么
这是 java 的内存模型,从 1.8 开始metaspace 替代方法区,用于存储类定义、方法数据、字段数据等元数据,并且呢它是从 是从本机内存中分配的,大小不固定。
也就是说如果不断的创建类等,metaspace 总会被撑爆的
2.2 metaspace 如何设置
对于 java 来说
text
- `-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 来说
txt
taskmanager.memory.jvm-metaspace.size=1024mb
三、结合 GC 日志分析问题
3.1 问题分析
text
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启动参数来观察类的加载、卸载信息:
ruby
-XX:TraceClassLoading -XX:TraceClassUnloading
加了这两个参数后,系统跑了一段时间,从GC日志中发现大量如下的日志:
另外在 aviatorscript 也发现了对应的issues
源码分析
3.3 源码解析
查看代码我们可以看到Aviator提供了两个调用接口:
typescript
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);
}
深入源码:
kotlin
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
参数最底层的使用:
typescript
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内存泄漏的问题,并提供了解决方案。