环境:centos
运行程序: springMVC的web服务
容器:jetty
我们有一个应用,在上线之后,监控到内存可用率随着运行时间逐步下降,从上线之初的50%,运行一段时间后下降到20%左右。机器上有其他进程也占内存,我想确定下是否是内存泄漏导致的,查清楚后也能对线上的应用运行情况有更好掌握,如果有内存泄漏查出原因进行解决,避免隐患的发生。
整体的排查步骤如下:
其中还有一些linux的命令:free、top等。
jetty服务器开启jmx监控(不会的可以查一下怎么开启监控)后,利用jdk自带的jconsole和jvisualvm工具开始对应用进行监控。
jconsole能够监控的项有:
jvisualvm能够监控的项有:
阿里的arthas工具具有的监控项有:
通过以上工具,观察到进程的GC频率、堆的使用一直处于良好的状态,堆内的GC能够及时的回收无用的对象。可以排除堆内存泄漏的原因。
通过以上工具也能排除metaspace、code_cache、direct、compressed_class_space和mapped
接着开始排查java进程中是不是有第三方库使用了DirectByteBuffer操作堆外内存导致的泄露。因为需要监控JVM堆外内存,所以需在重启的JVM启动参数中加入
-XX:NativeMemoryTracking=detail
同时编写了一个定时采集进程的JVM内存的小工具(bash脚本:capture.sh),主要内容如下:
for (( i=1; i <= 100000;i++))
do
echo "-----------------------本次执行到第" $i "次采集"
if [ ! -f "/home/test/stopAutoCapture" ];then
echo "手动执行停止采集jcmd,目前执行到第" $i "次采集"
break
else
pid=11234 #进程号
captureTime=$pid`date +"%Y%m%d-%H%M%S"`
jcmd $pid VM.native_memory detail > /tmp/capture/jcmd$captureTime
sleep 3600
fi
注意:请先创建/tmp/capture目录,/home/test/stopAutoCapture是为了手动停止后台采集任务创建的空文件,可以使用命令:touch stopAutoCapture进行创建,当想取消采集脚本的执行时,可以执行:rm stopAutoCapture 进行停止。
编写完成后,使用如下命令进行后台启动:
nohup sh capture.sh &
执行后会在当前目录下生成nohup.log 日志,可以通过如下命令进行查看数据采集的进度:
tail -f nohup.log
采集一段时间的数据后,编写小工具(暂时给它命名为MTDAT:MemTrackingDataAnalyseTool)对采集的数据进行分析(小工具后期我会放到github上,在这儿会给个链接):
jcmd 主要统计以下方面的指标的reserved和committed的大小:
Java Heap | Class | Thread | Code | GC | Compiler | Internal | Symbol | Native Memory Tracking | Arena Chunk | Unknown |
---|---|---|---|---|---|---|---|---|---|---|
平稳 | 平稳 | 平稳 | 平稳 | 平稳 | 平稳 | 平稳 | 平稳 | 平稳 | 平稳 | 平稳 |
用工具对各个指标的reserved和committed生成曲线图后,分析的结果可知运行平稳后,这些指标基本没有发生大幅度增加或者减少的变化。可以排除Java堆外内存大的泄漏。
已经排除了java堆和非堆、java的堆外内存相关因素,现在只能从系统层面排查问题,为了排查系统层面的内存,恶补了linux操作系统内存方面的相关知识,根据查询到的相关资料,结合/proc/meminfo文件是对整个操作系统内存管理的文件,自己画了一个内存分布图,如下图所示是对/proc/meminfo文件中的内存各个指标分布的介绍:
充分了解linux内存分布情况后,我决定定时(还是每小时一次)采集/proc/meminfo的信息,capture.sh脚本中在jcmd采集命令后面增加了如下的采集信息:
cp /proc/meminfo /tmp/capture/meminfo$captureTime
采集了3天的数据后,使用MTDAT工具对meminfo进行分析,可以得到各个指标的容量曲线(使用MTDAT工具结合excel绘图功能,能够得到很直观的曲线图,这个后期补上),
指标名称 | 变化情况 | 指标名称 | 变化情况 | 指标名称 | 变化情况 | 指标名称 | 变化情况 |
---|---|---|---|---|---|---|---|
MemTotal | 不变 | SwapFree | 基本平稳 | Committed_AS | 基本平稳 | SwapTotal | 不变 |
MemFree | 持续下降 | Dirty | 基本平稳 | VmallocTotal | 不变 | CommitLimit | 不变 |
MemAvailable | 持续下降 | Writeback | 不变 | VmallocUsed | 不变 | Mlocked | 不变 |
Buffers | 不变 | AnonPages | 持续上升 | VmallocChunk | 不变 | WritebackTmp | 不变 |
Cached | 基本平稳 | Mapped | 基本平稳 | HardwareCorrupted | 不变 | Unevictable | 不变 |
SwapCached | 基本平稳 | Shmem | 基本平稳 | AnonHugePages | 持续上升后平稳 | Bounce | 不变 |
Active | 持续升高 | Slab | 基本平稳 | HugePages_Tota | 不变 | DirectMap2M | 不变 |
Inactive | 持续下降后平稳 | SReclaimable | 基本平稳 | HugePages_Free | 不变 | nactive(file) | 基本平稳 |
Active(anon) | 持续升高 | SUnreclaim | 基本平稳 | HugePages_Rsvd | 不变 | INFS_Unstable | 不变 |
Inactive(anon) | 缓慢增长后平稳 | KernelStack | 基本平稳 | HugePages_Surp | 不变 | DirectMap4k | 不变 |
Active(file) | 基本平稳 | PageTables | 基本平稳 | Hugepagesize | 不变 |
从上表可知,MemAvailable和MemFree在持续下降,AnonPages、AnonHugePages和Active(anon)处于上升阶段,又从上图中linux中内存的分布情况,可知AnonPages是包含AnonHugePages和Active(anon)的,所以可以得知:java进程运行的过程中,anon(匿名页)的容量是持续上升。
现在要对anon容量的增长一探究竟。
pmap -x $pid > /tmp/capture/pmap$captureTime
jstack $pid > /tmp/capture/jstack$captureTime
cp /proc/$pid/smaps > /tmp/capture/smaps$captureTime
pmap文件的结构如下所示:
Address Kbytes RSS Dirty Mode Mapping
00007f756c000000 1636 1540 1540 rw--- [ anon ]
00007f756c199000 63900 0 0 ----- [ anon ]
00007f7574000000 2552 1720 1720 rw--- [ anon ]
00007f757427e000 62984 0 0 ----- [ anon ]
00007f7578000000 2024 1900 1900 rw--- [ anon ]
00007f75781fa000 63512 0 0 ----- [ anon ]
00007f757c000000 14468 14436 14436 rw--- [ anon ]
00007f757ce21000 51068 0 0 ----- [ anon ]
00007f7580000000 4452 4420 4420 rw--- [ anon ]
00007f7580459000 61084 0 0 ----- [ anon ]
.......... 省略很多
00007f758045f000 61084 0 0 ----- servlet-api-3.0.jar
00007f7584000000 22908 22852 22852 rw--- [ anon ]
00007f758565f000 42628 0 0 ----- [ anon ]
00007f758e45f000 61084 0 0 ----- libpthread-2.17.so
00007f7588000000 51776 51584 51584 rw--- [ anon ]
00007f758b290000 13760 0 0 ----- [ anon ]
00007f758c000000 54680 54620 54620 rw--- [ anon ]
00007f758f566000 10856 0 0 ----- [ anon ]
00007f7590000000 44616 44544 44544 rw--- [ anon ]
00007f7592b92000 20920 0 0 ----- [ anon ]
00007f7594000000 65512 65484 65484 rw--- [ anon ]
————————————————
total KB XXX YYY ZZZ
在采集的开始过程中,pmap文件出现了很多一些64K、1K、2K大小的内存块,数量有增多的趋势,我从另外一篇博客中看到jdk8 的Inflate有内存泄漏的风险,所以在smap文件中找到这块内存地址,使用如下命令查看内存块的内容:
gdb -p pid
和 dump memory mem.bin 0xXXXXXXX 0xYYYYYYYY
里面的内容都是一些间断的信息,实在是看不懂,不过也可以排除是jdk8解压jar包带来的bug内存泄漏问题。
采集够3天的数据后,
我用MTDAT工具对pmap和jstack文件分析,对线程总数体、守护线程和非守护线程的个数以及线程持有的内存大小进行了分析,得到的图(后期补上),数据结论如下:
类型 | 线程总体 | 守护线程 | 非守护线程 |
---|---|---|---|
线程个数 | 平稳 | 平稳 | 平稳 |
线程占用内存大小 | 平稳 | 平稳 | 平稳 |
从以上图可以得到结论: 内存可用率下降与线程个数和线程占用的内存大小无关。
然后使用MTDAT工具对pmap进行分析,可以得到anon和非anon区域的个数在java进程运行过程中的变化,得到一个曲线图(后面会补上),得到结论:anon和非anon区域的个数在运行过程中基本平稳,无明显数量变化。
既然anon和非anon区域的个数在运行的过程中没有变化,那anon的容量的增长又从哪儿来的呢,现在把目光盯向了pmap文件的各个指标容量的变化,因为pmap中的所有内存块一部分是进程使用的,一部分是进程使用的原生内存,好吧,到这儿我再分析下jvm和非jvm(other)使用的pmap中的各项指标。得到如下的数据(图后补上):
指标名称 | total Kbytes | total RSS | total Dirty | jvm-Kbytes | jvm-RSS | jvm-Dirty | other-Kbytes | other-RSS | other- Dirty |
---|---|---|---|---|---|---|---|---|---|
变化趋势 | 不变 | 持续上升 | 持续上升 | 不变 | 持续上升 | 持续上升 | 不变 | 上升后稳定 | 上升后稳定 |
从这个图(后面补上)结果数据中可以得到如下的结论:
同时通过meminfo的MemTotal-MemAvailable 约= total RSS 约=top中的RSS
现在可以知道原来top命令中的RSS 是随着pmap的total RSS一致增涨的。到这儿终于找到top的RSS增长的地方了。
现在开始探究:为什么pmap的total RSS一直增涨(即jvm-RSS和Dirty一直增涨),到底是什么原因导致的。linux为什么不回收呢。
问题抛出来就要去解决,MTDAT工具上场了,分析pmap中堆和非堆的rss增涨趋势,得到如下图(图后补上),数据结论如下:
纬度名称 | heap-RSS | noheap-RSS |
---|---|---|
变化趋势 | 持续增长 | 增长一段事件后平稳 |
到这儿基本上结论出来了:top中的RSS持续增涨,heap的RSS增长贡献值很大。即heap的RSS增涨导致了top中的RSS增涨。
为什么heap的RSS会持续增涨呢,这个jvm管理自己的内存机制有关,jvm自己划了一块内存自己使用自己回收,比如其中堆中无用的对象需要通过GC定期GC进行清除,可是这种清除是逻辑清除(jvm-RSS和jvm-Dirty数据基本一致,说明同一区域的内存块一直写入成为linux操作系统认为的脏数据),对象还在那一块区域上,linux无法感知到这种逻辑删除,linux认为这块区域是有内容的,所以RSS需要加上这块内存,周而复始,堆中使用过的内存区域越多,RSS就会越来越大。等堆中所有的内存区域都被使用过,heap的RSS也就不再增长了,top中的RSS也就居于平稳了。
当然如果有内存泄漏的话,top的RSS还是会持续增长的。
--------完结,待补充相关的数据和图标-----