Linux:Java应用随着持续运行一段时间后,内存可用率逐渐减少的乌龙事件排查过程

环境:centos
运行程序: springMVC的web服务
容器:jetty

一、背景

我们有一个应用,在上线之后,监控到内存可用率随着运行时间逐步下降,从上线之初的50%,运行一段时间后下降到20%左右。机器上有其他进程也占内存,我想确定下是否是内存泄漏导致的,查清楚后也能对线上的应用运行情况有更好掌握,如果有内存泄漏查出原因进行解决,避免隐患的发生。

二、 排查过程

整体的排查步骤如下:

  1. Java的堆内存和metaspace等内存排查。使用的工具是:Jconsole、JVisualVM、arthas、strace。
  2. Java堆外内存分析排查。使用的命令:jcmd
  3. 进程的原生内存排查。使用的工具:google的gperftools、gdb、pmap、smaps、jcmd、jstack。

其中还有一些linux的命令:free、top等。

2.1 Java的堆内存和metaspace等内存排查

jetty服务器开启jmx监控(不会的可以查一下怎么开启监控)后,利用jdk自带的jconsole和jvisualvm工具开始对应用进行监控。

jconsole能够监控的项有:

  • 内存:能够堆的各项指标变化,包括新生代、老年代、伊甸园区和survivor区的变化,同时也能看到非堆区的变化,包括metaspace、code_cache、direct、compressed_class_space和mapped等。
  • 线程:能够看到各个线程的情况。还能够检测到是否有死锁。
  • 类:能够看到加载类和卸载类的各种情况。
  • VM概要:能够看到启动进程的环境变量,以及jvm大的reserved和conmmited的大小等。
  • MBean:能够看到内存的各个对象指标。

jvisualvm能够监控的项有:

  • CPU 使用情况和垃圾回收情况
  • 堆和metaspace:能够看到堆的整体使用情况,以及metaspace的变化。
  • 类:能够看到加载类和卸载类的各种情况。
  • 线程:能够看到各种线程的状态和使用情况,活动线程和守护线程等。
  • 监控采样:cpu和内存堆。这个功能很强大,能够观测随着应用的运行,产生了哪些对象,特别是增量对比功能,对于内存泄漏的排查有很大的用处,一般持续增多的对象,一直不释放就有可呢鞥是内存泄漏的源头。

阿里的arthas工具具有的监控项有:

  • dashboard:能够实时显示当前进程的heap和noheap的使用情况。
  • trace 能够追踪到具体的方法是谁调用的。
  • 火焰图:能够看到进程的内存热点图。

通过以上工具,观察到进程的GC频率、堆的使用一直处于良好的状态,堆内的GC能够及时的回收无用的对象。可以排除堆内存泄漏的原因。
通过以上工具也能排除metaspace、code_cache、direct、compressed_class_space和mapped

2.2 Java堆外内存分析排查

接着开始排查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堆外内存大的泄漏。

2.3 进程的原生内存排查

已经排除了java堆和非堆、java的堆外内存相关因素,现在只能从系统层面排查问题,为了排查系统层面的内存,恶补了linux操作系统内存方面的相关知识,根据查询到的相关资料,结合/proc/meminfo文件是对整个操作系统内存管理的文件,自己画了一个内存分布图,如下图所示是对/proc/meminfo文件中的内存各个指标分布的介绍:
Linux:Java应用随着持续运行一段时间后,内存可用率逐渐减少的乌龙事件排查过程_第1张图片
充分了解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容量的增长一探究竟。

  1. 先排查下在java进程运行过程中 anon的块数有没有数量上的变化。
    capture.sh脚本中在meminfo采集命令后面增加了如下的采集信息:
  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
变化趋势 不变 持续上升 持续上升 不变 持续上升 持续上升 不变 上升后稳定 上升后稳定

从这个图(后面补上)结果数据中可以得到如下的结论:

  • total RSS 持续上升,total RSS = jvm-RSS + other-RSS
  • RSS和Dirty的大小基本一致。
  • 能够看到total RSS上升的趋势是和jvm-RSS的趋势一致, jvm-RSS贡献了大部分的RSS。

同时通过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还是会持续增长的。

--------完结,待补充相关的数据和图标-----

你可能感兴趣的:(Linux,Java)