FullGC排查,居然是它!

前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!(抖音、公众号同号)

🔈 今天这篇文章一定值得你看 转载前公司CTO发的文章:FullGC排查,居然是它

项目上有 Full GC 告警,每个节点平均每天有一到两次Full GC 告警。出现Full GC时,应用是处于 Stop The World 状态,对于高可用要求的应用来说,这是一个很大的稳定性风险。就在告警出现的这几天,上游业务方也联系到我,说他们近期调用我们的接口超时,通过监控来看,调用超时的时间正好也就是Full GC出现的时间。虽然我其实并没有排查过Full GC问题,不过我还是想试一下。

问题分析排查

通过查看Full GC日志,发现Full GC是由于达到Metaspace Threadhold 触发,GC耗时在2-4S。

js 复制代码
2025-09-27T14:33:02.210+0800: 312596.342:  
[FullGC(MetadataGCThreshold) 3169M->553M(6144M), 3.5247678secs]  
[Eden: 1258.0M(3594.0M)->0.0B(3686.0M) Survivors: 22528.0K->0.0B Heap: 3169.3M(6144.0M)->554.0M(6144.0M)], [Metaspace: 752742K->303689K(1511424K)]  
[Times: user=4.20 sys=0.43, real=3.52 secs]

既然是元空间,那我们通过应用监控查看当时的元空间情况,当Metaspace 达到700多MB时,就会触发Full GC, GC后元空间降到300多MB,释放了400M内存。

元空间主要是存放 class 信息,看一下类数量的监控,在FullGC时,类数量达到15万,GC后降低到4万,释放了11万个class。

正常情况下 class 是相对稳定的,通常是由于动态代理、序列化、脚本引擎这类场景会动态生成类。

通过类监控的增长曲线发现,工作时间的类增长曲线明显比非工作时间的类增长曲线更陡峭,说明大概率是工作时间调用的某个接口的内部逻辑导致。

通过接口调用数量监控,判断可能和其中三个接口有关。于是从接口调用入口出发捋代码逻辑,找到一段调用 Aviator 框架的代码。

java 复制代码
AviatorEvaluatorInstance instance = ExpressContext.getInstance();  
ExpressionLexer lexer = new ExpressionLexer(instance, expression);  
CodeGenerator codeGenerator = new OptimizeCodeGenerator(instance, null, CLASS_LOADER, null);
ExpressionParser parser = new ExpressionParser(instance, lexer, codeGenerator);
parser.parse();

这段代码看着就可疑,又是CodeGenerator,又用到ClassLoader,这些类名仿佛就是在和我说,别找了,就是我。

于是写了一段单元测试代码,循环调用,jvm启动参数上加上

ruby 复制代码
-XX:+TraceClassLoading -XX:+TraceClassUnloading

运行后发现有大量类的生成和卸载,完全满足触发 Metapsace Threshold 的条件。此时此刻,我已经有80%的把握就是它的问题了。不过熟悉我的人都知道,我不仅是一个解决问题能力还不错的人,同时我还是一个保守的人, 没有十足的把握我是不会下结论,我可不想在新团队砸了自己招牌。

现在一切猜想都很符合逻辑,现在就差最后的证明,那就是类统计信息。在这不像我以前的时候,各种环境我都是超管权限,想怎么弄就怎么弄。我无法登录到容器内部用jcmd或者 jmap histo命令去统计类,容器平台上提供了堆dump的功能,但是堆dump是一个非常重量级的操作,为了不影响业务,我只能等。

在国庆期间,大家都休假了,我做了一个决定,我偷偷的登录到云平台,关闭其中一个节点的流量,导出堆内存,导入MAT进行分析。此时我心情是平静的,我知道我对了,我只是要让MAT告诉我。我本将心向明月,然明月何曾是两乡,MAT没有辜负我,它小声的和我说:你对了。

解决方案

这些类就是Aviator框架生成的,类名后缀是顺序递增,总共生成过100多万个class。解决方案通过增加表达式的LRU缓存解决这个问题,上线后类数量非常平稳,Full GC也消失了。加LRU缓存也只是临时方案,最好是Aviator官方能支持我这种场景,给他们提了issue,不过大概率他们是不会同意的,毕竟它也只是一个个人项目,靠爱发电不太靠得住。你看我,想啥时候发就啥时候发。

github.com/killme2008/...

相关推荐
Jagger_3 小时前
SOLID原则中的依赖倒置原则(DIP):构建可维护软件架构的关键
后端
老K的Java兵器库4 小时前
集合性能基准测试报告:ArrayList vs LinkedList、HashMap vs TreeMap、并发 Map 四兄弟
java·开发语言
Knight_AL4 小时前
如何解决 Jacob 与 Tomcat 类加载问题:深入分析 Tomcat 类加载机制与 JVM 双亲委派机制
java·jvm·tomcat
Penge6664 小时前
为什么 Go 中值类型有时无法实现接口?—— 从指针接收器说起
后端
用户90555842148054 小时前
Milvus源码分析:向量查询(Search)
后端
间彧4 小时前
Java HashMap:链表工作原理与红黑树转换
后端
哲学七4 小时前
Springboot3.5.x版本引入javaCv相关库版本问题以及精简引入包
java·ffmpeg
亚雷4 小时前
深入浅出达梦共享存储集群数据同步
数据库·后端·程序员