乐趣区

关于jvm:记一次堆外内存泄漏排查过程

本文波及以下内容

  • 开启 NMT 查看 JVM 内存应用状况
  • 通过 pmap 命令查看过程物理内存应用状况
  • smaps 查看过程内存地址
  • gdb 命令 dump 内存块

背景

最近收到运维反馈,说有我的项目的一个节点的 RSS 曾经是 Xmx 的两倍多了,因为是 ECS 机器所以我的项目能够始终运行,幸好机器内存短缺,不然就可能影响到其余利用了。

排查问题

通过跳板机登录到指标机器,执行 top 命令,再按 c,看到对应的过程所占用的 RES 有 8 个多 G(这里过后遗记截图了),然而实际上咱们配置的 Xmx 只有 3G,而且程序还是失常运行的,所以不会是堆占用了这么多,于是就把问题方向指向了非堆的内存。

首先想到通过 Arthas 查看内存散布状况,执行 dashboard 命令,查看内存散布状况

发现堆和非堆的内存加起来也就 2 个 G 不到,然而这里看到的非堆只蕴含了 code_cache 和 metaspace 的局部,那有没有可能是其余非堆的局部有问题呢?

NMT

NMT 是 Native Memory Tracking 的缩写,是 Java7U40 引入的 HotSpot 新个性,开启后能够通过 jcmd 命令来对 JVM 内存应用状况进行跟踪。留神,依据 Java 官网文档,开启 NMT 会有 5%-10% 的性能损耗。

-XX:NativeMemoryTracking=[off | summary | detail]  
# off: 默认敞开 
# summary: 只统计各个分类的内存应用状况.
# detail: Collect memory usage by individual call sites.

增加 -XX:NativeMemoryTracking=detail 命令到启动参数中,而后重启我的项目。

跑了一段时间后 top 看了下过程的 RES,发现曾经 5 个多 G 了

执行jcmd <pid> VM.native_memory summary scale=MB

从图中能够看到堆和非堆的总应用内存(committed)也就 2G 多,那还有 3 个 G 的内存去哪里了呢。

pmap

pmap 命令是 Linux 上用来开过程地址空间的

执行 pmap -x <pid> | sort -n -k3 > pmap-sorted.txt 命令能够依据理论内存排序

查看 pmap-sorted.txt 文件,发现有大量的 64M 内存块

难道是 linux glibc 中经典的 64M 内存问题?之前看挖坑的张师傅写过一篇文章(一次 Java 过程 OOM 的排查剖析(glibc 篇))讲过这个问题,于是筹备参考一下排查思路

尝试设置环境变量 MALLOC_ARENA_MAX=1,重启我的项目,跑了一段时间当前,再次执行 pmap 命令查看内存状况,发现并没有什么变动,看来并不是这个起因,文章前面的步骤就没有什么参考意义了。

smaps + gdb

既然能够看到有这么多异样的内存块,那有没有方法晓得外面存的是什么内容呢,答案是必定的。通过一番材料查阅,发现能够通过 gdb 的命令把指定地址范畴的内存块 dump 进去。

要执行 gdb 的 dump 须要先晓得一个地址范畴,通过 smaps 能够输入过程应用的内存块详细信息,包含地址范畴和起源

cat /proc/<pid>/smaps > smaps.txt

查看 smaps.txt,找到有问题的内存块地址,比方下图中的 7fb9b0000000-7fb9b3ffe000

启动 gdb

gdb attach <pid>

dump 指定内存地址到指定的目录下,参数的地址须要在 smaps 拿到地址前加上 0x

dump memory /tmp/0x7fb9b0000000-0x7fb9b3ffe000.dump 0x7fb9b0000000 0x7fb9b3ffe000

显示长度超过 10 字符的字符串

strings -10 /tmp/0x7fb9b0000000-0x7fb9b3ffe000.dump

发现外面有大量的图中红框中的内容,这个内容是后端给前端 websocket 推送的内容,怎么会驻留在堆外内存外面呢?查看了我的项目代码发现,后端的 websocket 是应用的 netty-socketio 实现的,maven 依赖为

<dependency>
 <groupId>com.corundumstudio.socketio</groupId>
 <artifactId>netty-socketio</artifactId>
 <version>1.7.12</version>
</dependency>

这是一个开源的 socket.io 的一个 java 实现框架,具体能够看
https://github.com/mrniko/netty-socketio

看了下最近的版本公布日志,发现这个框架的最新版本曾经是 1.7.18,而且两头公布的几个版本屡次修复了内存透露相干的问题

于是把依赖版本升级到最新版,从新公布后,第二天在看,发现 RES 还是变得很高

两头又认真看了应用框架的相干代码,发现断开连接的时候没有调用 leaveRoom 办法,而是调用了 joinRoom,导致群发信息的时候还会往断开的连贯外面发送数据(实践上会报错,然而没有看到过相干日志),修复代码后又从新公布了一版,跑了一天在看,仍然没有成果。

论断

到这一步能够确认的是内存透露的问题必定跟这个 websocket 及相干性能无关的(因为两头降级到 1.7.18 发现 websocket 连不上了,起初降到了 1.7.17,期间有较长的工夫是没有裸露 websocket 服务的,而正好这段时间的 RES 是十分稳固的,齐全没有上涨的趋势,因为这个性能点比拟小,用户没有什么感知),最初,通过跟产品方面沟通,决定把这里的 websocet 去掉,改为前端间接申请接口获取数据,因为这里的性能就是为了实时推送一个未读信息数量,而这个信息其实很少有用户关怀,所以,就改成刷新页面的时候查问一下就行了,就这样,问题算是变相解决了????。

总结

尽管进过不懈的致力,最终只是证实了【没有需要就没有 BUG】这句话,然而两头还是有挺多的播种的,很多命令也是第一次应用,其实两头还有一些波折,就没有一一写进去了,筛选了比拟有价值的写了这篇文章总结,心愿能够分享给有须要的人,当前遇到相似问题,能够做个教训参考。

感激

这次排查问题从网上查找学习了很多材料,非常感谢以下文章作者分享的教训

  • 一次 Java 过程 OOM 的排查剖析(glibc 篇)
  • JAVA 堆外内存排查小结
  • Linux 中应用 gdb dump 内存
退出移动版