背景
生产环境运行一段时间后变得卡顿,排查后发现GC日志中在频繁FullGC
内存分析
好家伙,String对象数量上,checkUser 相关内容就占了 317万多个,还剩下36万多个其它String对象;
关于这个 parallelLockMap
java.lang.ClassLoader#parallelLockMap
loadClass的过程中,会根据要加载的类的类名去获取一把锁,并保存到这个parallelLockMap中
初步结论
类加载器:
org.springframework.boot.loader.LaunchedURLClassLoader
加载了很多类名为 checkUser 的临时类,导致 classLoader里面的parallelLockMap数据增多,且无法及时回收
代码排查
找到业务代码中checkUserOrg与checkUserType相关的代码,得知是使用了QLExpress,脚本中定义的function方法名;
关于QlExpress:https://github.com/alibaba/QLExpress
问题复现
已提交issues:https://github.com/alibaba/QLExpress/issues/281
版本:jdk1.8 + QLExpress 3.2.4
pom.xml
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.2.4</version>
</dependency>
问题复现代码:
public static void main(String[] args) throws Exception {
ExpressRunner RUNNER = new ExpressRunner();
DefaultContext<String, Object> defaultContext = new DefaultContext<String, Object>();
defaultContext.put("org", new Object());
Object result = RUNNER.execute("if (method_1650070704185950208()) {\n return 0;\n } \n function method_1650070704185950208(){return false;}",
defaultContext, null, true, false);
System.out.println(result);
result = RUNNER.execute("if (method_1650070704185950208()) {\n return 0;\n } \n function method_1650070704185950208(){return true;}",
defaultContext, null, true, false);
System.out.println(result);
result = RUNNER.execute("if (method_1650070704185950208()) {\n return 1;\n } \n function method_1650070704185950208(){return true;}",
defaultContext, null, true, false);
System.out.println(result);
}
debug观察:
((ExtClassLoader)DemoTempApplication.class.getClassLoader().parent).parallelLockMap
会发现 “method_1650070704185950208” 作为类名被尝试加载过
查看qlExpress源码
确实是会根据function方法名加载一个类,从com.ql.util.express.ExpressRunner#execute 跟代码到:
com.ql.util.express.parse.ExpressPackage#getClassInner
结论
1、业务代码中这个function方法名带有序列号时,执行次数越多,classLoader里面的parallelLockMap数据就越多,且无法回收;
2、后面排查代码的过程中还发现了一出容易出现内存泄漏的地方,就是QL脚本编译后的缓存也会无限增加,因为他是将QL脚本作为key,将解析后的指令集作为value存入一个Map中;
总的来说,还是属于QL脚本使用不够规范,用了唯一序列号拼接function方法名,导致方法名不同,脚本也就不同,也导致classLoader与QL指令集缓存会爆满;
修复
与QLExpress项目开发人员取得了联系,并沟通后得知:
1、他这里面的loadClass,只是编译时候的一个尝试过程,比如你写一个Map,他就会尝试给你加载 java.util.Map,看看这个类是否已存在;
2、动态脚本,应该是可以枚举的,脚本模板最好能固定住,变化的是参数就行,否则就因为function方法名不一样,每次都要重复编译脚本,也会导致性能差;
然后与自己公司的相关开发人员沟通:
只需要保证当前脚本内方法名不重复即可,不同脚本之间的方法名不需要特意区分;
所以调整了业务代码,脚本内有多个方法定义时,改用循环内使用索引下标值进行拼接;