背景
最近线上服务平均两周就会出现重启的现象,同时SRE的同事发现重启前服务占用内存非常高,重启后内存下降,可见该服务是逐步耗尽系统内存。给力的SRE同事最后发现是服务所用的框架导致的内存被撑爆了。本文在本地模拟在这次线上问题,并记录这次问题的排查过程
本地模拟
首先由于线上服务间隔两周才出现重启的情况,为了在本地模拟线上Metaspace OOM的情况, 采用JMeter对本地服务进行压测。本地的操作系统是Mac OS。使用top
命令查看压测之前的程序的内存占用情况,如下图所示
可以看到程序整体占用2495M。然后用JMeter对服务进行压测。在压测的过程中在终端使用反复使用jcmd [PID] GC.heap_info
命令, 重点观察Metaspace 区域数据变化
可以看到Metaspace中committed在飙升
排查与解决
使用jmap -dump:format=b,file=[filename].bin [PID]
命令分别在压测刚开始和结束之前打印堆快照。然后使用MAT工具对快照进行分析。下面第一张图片一开始的快照, 第二张是压测之后的快照。
对比上面两张图可以明显看到,压测一段时间后Class的数量与Class loader的数量有了显著的上升。我们知道Java 8使用Metaspace来存贮类的元数据以及类加载器,Metaspace占用的是本地堆内存(native heap),所以Metaspace的增长导致了本地内存“撑爆”了。
可以通过-XX:MaxMetaspaceSize
来设置触发Metaspace回收的阈值, 保证Metaspace不会太大,超过阈值就FullGC。但是这治标不治本,为什么会有这么多类加载器呢?
TranletClassLoader并不是业务层自定义的ClassLoader, 深入到框架层面的代码发现有如下代码。
每次请求都会调用template.newTransformer()
, 该方法每次都会生成一个TransletClassLoader
去加载类TemplatesImpl
,慢慢导致Metaspace不断增长。询问同事知道,这个API是框架层面暴露给监控部门调用,获取服务的健康状态, 请求量不是很大,所以才会有两个礼拜内存才满的现象。
找到问题的原因之后,目前先给线上服务加上-XX:MaxMetaspaceSize
参数,框架部门的同事开始着手处理代码问题。
总结
Java 8开始彻底移除PermGen
, 引入Metaspace来解决java.lang.OutOfMemoryError: PermGen
。但是不代表对Metaspace可以不管不问了,使用不当Metaspace会导致本地内存溢出的情况。也有JVM大神就觉得Metaspace取代perm的好处不是那么明显,有点费力不讨好的感觉。