问题描述
项目使用SpringCloud进行开发。其中A服务共有四个实例,突然四个实例在注册中心全部下线,查询日志发现内存溢出。
解决流程
1.导出堆转储文件
遇到内存溢出问题,首先应该导出堆转储文件,对当前溢出的服务的内存分配情况进行分析,查看是哪个或哪些对象占用较多,然后去代码中寻找占用内存过多的对象不被垃圾回收的原因。
导出堆转储文件的命令如下:
jmap -F -dump:file=dump_file_name.hprof [pid]
-dump:file= 后边的路径是导出的文件全路径,可自行设置。
注意导出 hprof 文件会很漫长,笔者导出了 将近 8 个小时。导出的文件大小也不尽相同,笔者导出了 4.1 个 G。
如果在Linux 环境中,执行此命令 建议使用 nohup jmap -F -dump:file=dump_file_name.hprof [pid] & 方式,防止会话结束后命令进程停止,导致 导出的文件不完整。
2.使用 MAT 分析内存
MAT 是 eclipse 的 一款插件,可以用于分析 hprof 文件并提供可视化对象分配图形等功能,辅助分析内存溢出问题。找到内存溢出的对象。
打开 MAT ,选择 File -> Open Heap Dump ,打开第一步中导出的 hprof 文件。
等待 MAT 成功打开 hprof 文件后,会进入 Overview 页面,如下图所示,点击 Leak Suspects 按钮。
进入 Leak Suspects 页面,此页面可帮助我们分析可能出现内存泄漏的对象。如下图所示,分析出两个可能有问题的对象,其中问题 a 更是占据了 1.9 个 G内存。
根据问题 a 的分析,可以看出 出问题的应该是 com.netflic.servo.monitor.BasicTimer 这个类对应的实体对象。点击 Details 可以查看详细引用。
其中 DefaultMonitorRegistry 对象名是 INSTANCE 全大写,难道是静态的?此时查看源码。
果然是静态的,根据 对象引用图,我们可以看到 INSTANCE 的实现应该是 BasicMonitorRegistry。再继续查看源码。
这里有一个 Set 集合 存放着 Monior,而BasicTimer 实际上 就是 Monitor 的一个子类。静态的引用 JVM 不会进行垃圾回收,需要手动进行回收。排查到现在,需要找到 什么时候 向这个 monitors 中 添加 BasicTimer 对象,而又是什么时候 从中移除对象。
经过排查,发现在 使用 restTemplete 发送请求时 , 部分调用方式 会(getForObject/postForObject 等) 走 org.springframework.cloud.netflix.metrics.MetricsClientHttpRequestInterceptor 这个 类。
我们进入 getTimer 方法,可以看到 在这里 创建新的 BasicTimer 并 添加到 monitors 中。
到目前为止已经找到了添加方法。但是经过查询,虽然看到了 有移除的方法,却没地方调用。所以问题原因已经找到:
3.结论
使用 restTemplete 执行 getForObject等方法时,会创建 BasicTimer,但一直不进行移除,且因为静态 JVM 不尽兴垃圾回收,导致对象一直添加,最终内存溢出。
后续
经过在 github 上查询,最后发现,果然是框架有问题,作者也提供了一种解决方法。
升级版本或者关闭监控 spring.metrics.servo.enabled=false 即可。