告警显示tp-audit的多个应用间隔性发生GC:ConcurrentMarkSweepCount(OldGC)引起了我们的注意。
现象:
观看我们公司(点评)cat监控平台
这是同一个小时内的图像,由图可知oldgc次数一分钟保持在6次左右,然而老年代内存依然坚挺使用高达900多M,没有下降趋势。
正常应用发生oldgc,老年代内存理论上应该被释放掉绝大部分。
由此我们可以判断出程序肯定有某处发生了内存泄漏。
用了ha456.jar、MAT、VisualVM、JProfiler等工具和请教各路大神分析得知
AppClassLoader里主要ConcurrentHashMap占用内存最大,而ConcurrentHashMap里主要存放的几乎都是Groovy动态生成的类名
这个ConcurrentHashMap存放了近600万个Entry
原因:
AppClassLoader是java内置类加载器,用来加载用户应用程序的类。里面有一个parallelLockMap,
主要用来存储类锁,避免JVM加载同名的类,和提高类加载的并发度。
GroovyClassLoader如果加载的是无类名的Script,最终会生成一个随机的类名,每次都不一样。
导致parallelLockMap不断膨胀,幸运的是parallelLockMap只存储一个name和Object,并不会占用过多空间。
所以现象就是上线半个多月都没有问题,一个月后才会内存吃紧。8G的LVS甚至几个月都不会有问题。
这应该是一个JDK的BUG,只要加载过多的不同类,parallelLockMap就会不断的膨胀,导致memory leak,
最终机器就会宕机。这个这个BUG早已经提给官方,但是JDK7、JDK8都未修复,而且明确指出不修复。
结论:
理论上是不合理的,因为这个Map只进不出。既然官方指出不修复,那我们只能规范使用流程了。
无论是Groovy还是通过别的方式动态加载类,尽量使用固定类名。如果类名是随机的,就要控制加载数量了。
如果你是4G的LVS,可以放心的创建500万个不同名的类。毛估500万个不同类名,占用大约800M内存。
如果程序仅加载变更后的类,相信500万次变更是肯定够用的。
如果还不够,那只能通过加内存来解决了。
使用Groovy动态加载类就算跳过了自带的无法卸载的类的坑,还是会踩进JDK自带的坑。
此次案例详细bug记录请预览https://issues.apache.org/jira/browse/GROOVY-6655
与此次案例无关的BUG预警:
Groovy推荐版本2.3.7,新版本有无法卸载类的坑,很容易导致OOM:PermGen space。
详细bug记录请预览https://issues.apache.org/jira/browse/GROOVY-7913
JDK8内存模型更新,默认已经没有PermGen了,也就是说不会发生OOM:PermGen space了。
但是使用的是LVS自带的内存,所以最好还是指定这个参数-XX:MaxMetaspaceSize=128m控制下大小
不然Groovy也会饰无忌惮的吃光LVS内存。
Groovy动态加载类使用方式推荐
|
因为
GroovyClassLoader是static的,所以想卸载无引用的Class,要执行classLoader.clearCache();
如果GroovyClassLoader每次都是new出来的,可以忽略执行classLoader.clearCache();