前言
本文章是系列文章「Java经典故障案例分享」的第二篇(第一篇:<<# Java故障案例分析第一期:父子任务使用不当线程池死锁>>),本次分享一个完整的排查堆外内存泄漏的过程,你将在本文章中了解到:
- Native Memory Track是什么以及如何发现可能存在的内存泄漏问题
- 如何使用pmap命令查看Java进程的内存映射状态
- 如何结合NMT和pmap排查内存泄漏
本文的主要内容:
- 故障描述
- 故障排查过程
- 提供一个小工具代码帮助快速分析
本文分为上下两篇,上篇主要讲述一般的内存泄漏排查过程,下篇讲述Netty堆外内存泄漏排查:juejin.cn/post/734012…
接下来就让我们开始吧!
故障描述
线上服务机器内存逐渐降低,最终触发内存不足告警,经过初步排查,进程所占用内存(通过ps的RSS查看)和JVM通过Native Memory Track显示的内存相差较大,达到2G以上。
故障排查过程
PS查看结果:
NMT查看结果:
ps结果 - nmt结果 = 2098913 Kbytes(进程总占用 - JVM占用) 约等于 2G,这2G内存到底是什么占用的呢?
简单介绍下我们这个应用:该应用是一个监控系统后端,是基于美团开源的CAT魔改的,它直接使用Netty从网络上读取业务应用上传的埋点数据,经过聚合计算后将结果存储到磁盘和数据库里。所以我们第一时间怀疑是不是Netty的问题,因为CAT是直接使用的Netty没有其他封装。
是不是Netty?
为了排查是不是Netty造成的,我们从Netty暴露的PooledByteBufAllocatorMetric
中抓取使用的直接内存大小并配置监控,发现并没有一直增长:
而且通过应用日志,也没有Netty的泄漏日志。
通过调研,Netty的直接内存使用是在NMT track的一部分,在Internal项里:
1 | ini复制代码... |
既然不是Netty造成的,那还有其他可能吗?
有的,比如:使用Native方法分配内存没有正确释放(即没有使用DirectByteBuffer,但是自己使用JNI等方式分配了不受JVM管理的内存) ,而我们极有这种情况。
那该如何排查呢?
有两种方式:
- 直接点,找谁在分配内存,即找调用分配内存的函数的堆栈
- 从泄漏内存的数据发现一些特点,顺藤摸瓜找到原因
第一种方法可以用perf工具找(如果是较新版本的内核可以用eBPF相关的工具如bcc、bpftrace排查,但我们线上内核版本是3.10)。但内存分配是非常频繁的操作,这种方法很难从大量调用中找到那个存在问题的,而且可能导致性能问题。所以我们使用第二种方法。
下面我们介绍这种情况的排查方式,即如何利用NMT + pmap分析泄漏的内存中的数据。
首先第一步要找到泄漏内存,第二步根据其中的数据找到原因
使用NMT & pmap找泄漏内存
什么是Native Memory Track?
首先介绍下NMT,全称是Native Memory Tracking (NMT) ,它是Hotspot VM用来分析VM内部内存使用情况的一个功能。我们可以利用jcmd
(jdk自带)这个工具来访问NMT的数据。
开启方法:在启动参数中加上:
-XX:NativeMemoryTracking=summary
或者-XX:NativeMemoryTracking=detail
,在我们的排查中需要使用detail
,但是detail
有一定性能损失,切记不要长时间开启。detail
相比summary
包含了虚拟内存映射的信息和造成内存使用的调用栈信息。
查看方式:
jcmd $(pid) VM.native_memory summary 或 jcmd $(pid) VM.native_memory detail
我们使用NMT查看各个JVM内存区域占用的大小和虚拟内存预设情况,大致输出如下:
1 | ini复制代码31748: |
这里列出了JVM内部各个区域如堆、Class、线程等占用的内存。
而在 Virtual memory map:
下方的就是虚拟内存每个地址区域是用来做什么的,比如0x00000001f0000000 - 0x00000007f0000000这24GB的地址空间是用来映射堆内存的,而我们应用的最大堆内存就是24GB。
但是这里均是JVM管理下的内存,我们没办法从这里找出为什么RSS占用的内存大小比NMT中的多的原因,如果我们能够知道Java进程所有的内存映射然后和这里的比较,找出在NMT中没有的就能够知道是什么在占用我们的内存,那么如何知道一个进程所有的内存映射呢?
接下来介绍pmap命令。
pmap命令
PMAP命令用于展示一个或多个进程的内存映射,命令格式如下:
pmap [options
] pid
[
...]
下面是一个例子:
1 | lua复制代码pmap 25693 |
可以看到每个映射的起始地址和大小,而如果和NMT下面的输出一起来看是不是能够分析出哪些映射是在NMT中没有的?
结合pmap和NMT
我们可以按照这个思路手动在pmap的输出中找哪些没有在nmt输出中出现的,或者写一个shell脚本来匹配。
这里我写了一个脚本在github上:github.com/hengyoush/J…,或者直接下载,使用方法:
./memleak.sh show pid
。
发现几个64MB的块很可疑,如:
但是该怎么查看这个地址块的内容呢?
泄漏内存分析
这些地址块可以通过下面的脚本查看:
1 | bash复制代码 cat /proc/$1/maps | grep -Fv ".so" | grep " 0 " | awk '{print $1}' | grep $2 | ( IFS="-" |
将上述脚本保存为dump.sh
, 然后执行:./dump.sh ${pid} ${address}
比如:./dump.sh 31748 7fcbfb622000
可以得到如下的文件:31748_mem_7fcbfb622000.bin
这个文件中包含的就是该地址块的内容,但是是二进制的,可以使用如下命令转为字符串方便查看:strings 31748_mem_7fcbfb622000.bin > 31748_mem_7fcbfb622000.bin.txt
查看转换后的内容:
我们发现这应该是一个接口返回的监控报表数据(我们的这个服务是一个监控服务,会有客户端拉取监控数据),查找了相关代码后终于水落石出了!!原来在使用servlet输出流写响应时没有正确使用try-with-resource,当客户端应用主动关闭连接时(发布或者重启),写入失败,但是流没有关闭,相关代码大致如下:
修复上线之后,内存不再诡异减少了,RSS和NMT输出也不会越来越大了。
总结
我帮你总结了一下排查步骤:
当你怀疑线上出现内存泄漏时,首先确定是堆外内存造成的还是堆内,堆外内存造成的有如下特点:
- JVM本身没有大量FGC等情况
- 机器内存不足
RSS - NMT
统计的内存 差距较大
如果确定是堆外内存造成,按照如下方式排查:
- 确定是否是Netty内存泄漏,排查方式有:应用日志有没有Netty的泄漏日志,Netty的直接内存指标有没有一直上升。
- 如果不是Netty内存泄漏,按照如下流程:
1. 使用`pmap -x {pid} > ./pmap.txt` 获取内存映射信息到文件里。
2. 使用`jcmd ${pid} VM.native_memory detail > nmt.txt`获取NMT统计的JVM内存详细信息。
3. 找nmt中没有但是在pmap中有的内存块地址(tips1:最好找64MB的内存块)
4. 使用上面的`dump.sh:./dump.sh {pid} {addr}` dump对应内存块的内容到磁盘上。
5. 使用 `strings {dump文件名} > dumped.txt`转换为可读的文本,然后根据文本内容进一步寻找问题的原因
- 如果是Netty内存泄漏,可以看我的这一篇文章:juejin.cn/post/734012…
是不是还挺复杂的,没关系,使用我写的一个小工具JavaMemLeak(Github地址)可以帮你自动化上述流程,只要执行:./memleak.sh show pid
列出可能存在泄漏的内存映射地址:
1 | css复制代码00007f2824000000 19396 19396 19396 rw--- [ anon ] |
命令行执行:./memleak.sh dump pid addr
得到内存块里的内容进一步分析,输出如下:
1 | makefile复制代码Dump文件已输出至: ./11983_mem_7f2964000000.bin |
最终会产出一个泄漏内存块的文件,你只需要从第五步开始就可以啦!
如果有帮到你的话欢迎star:github.com/hengyoush/J…
本文转载自: 掘金