最近生产服务器从JDK 7升级到了JDK 8,运行一段时间后发现一台服务器出现了java.lang.OutOfMemoryError: Metaspace
错误。这个角色的服务器在使用JDK 7时设置的MaxPermSize是256M,实际使用在100M左右,因此虽然JDK 8的Metaspace可以不设置最大值,但为了防止类似classloader leak的问题导致系统内存耗尽,我们还是设置了512M的限制。由于代码没有变更过,因此怀疑是JDK 8的Metaspace回收机制有变化导致了OOM。
故障分析
分析GC日志
出现OOM异常时,系统一直在进行Full GC (Metadata GC Threshold)
,但Metaspace空间始终无法回收
MAT(Eclipse Memory Analyzer)分析Heap Dump
MAT分析Metaspace的手段比较有限,从Class Loader Explorer看起来classloader数量,defined class数量都在合理范围内
分析运行时数据
jcmd提供了多种分析Metaspace的手段,包括
VM.native_memory
,通过-XX:NativeMemoryTracking=summary
启用,分类汇总Native Memory的使用量GC.class_stats
,通过-XX:+UnlockDiagnosticVMOptions
启用,打印所有class信息
修改JVM启动参数运行一段时间后,通过VM.native_memory发现,大部分内存占用都在Class上
通过GC.class_stats可以看到class数量在不停增长,简单统计后,发现是同一个类com.foo.Bar$JaxbAccessorF_baz
的数量在不停增长,从class_stats里可以看出这个类的父类是com.sun.xml.bind.v2.runtime.reflect.Accessor
,怀疑是JAXB的bug,尝试google下,发现的确有相关的jira,JAXB-564 Reflection Injector for Optimisation Loosing Previously Defined Classes),StackOverflow上也有人反馈类似问题Old JaxB and JDK8 Metaspace OutOfMemory Issue
JAXB Bug分析
JAXB为了避免每次通过反射去访问对象属性或者方法,会为属性和方法动态生成Accessor类,并通过Injector对象进行管理
|
|
Injector是通过WeakHashMap进行缓存的,GC以后,Injector就被回收了,因此后续访问时就无法获取到生成过的Accessor类,于是JAXB会重新创建一个Injector再次动态生成Accessor类,然而classloader中这个对应的Accessor类已经存在,因此会报LinkageError
|
|
修复方式也比较简单,即生成Accessor类前先去classloader里查找下是否已经生成过,具体可以看一下Patch代码
故障重现
为了验证是否是重复定义类造成的OOM,我们尝试下重现故障,重复加载当前package下Foo
这个类,使用默认的并发收集器,并限制Permgen/Metaspace为20MB,测试代码如下:
|
|
JDK 7 运行结果
|
|
JDK 8 运行结果
|
|
|
|
|
|
结论
从重现过程中可以看出,虽然类定义失败了,但过程中产生的数据似乎并没有被清理。在JDK 7中,对Permgen中对象的回收会进行存活性检查,因此重复定义时产生的数据会在GC时被清理。然而在JDK 8中,Metaspace的回收只依赖classloader的存活,当classloader还活着时,它所产生的对象无论存活与否都不会被回收,由此引发了OOM。