生产环境上个别服务出现内存占用过高的情况。
运行环境:jdk1.8.0_171、Linux 64bit
其中一个服务的最大堆内存设置为1g的情况下,但rss达到3.4g。该服务的具体vm参数如下:
-server -Xms1g -Xmx1g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:ErrorFile=$AFA_HOME/log/grp_err.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$AFA_HOME/log/heap_dump.hprof -Djava.security.egd=file:/dev/./urandom -Xloggc:"${AFA_HOME}"/log/grp_gc"${CLASS_ARGS}".log
针对此情况对该服务进行了检查。
1. 生产环境服务检查
1.1 堆内存(heap)检查
执行jmap -heap $pid命令,可以看到MaxHeapSize的值是1024M,且当前的使用率不足50%,且afa平台日志中并没有出现“java.lang.OutOfMemoryError”,故可排除堆内存原因。
1.2 直接内存检查(direct memory)
未指定直接内存的最大值(-XX:MaxDirectMemorySize)时,直接内存的默认最大值等于Xmx,即1024M,且afa平台日志中并没有出现“java.lang.OutOfMemoryError: Direct buffer memory”,故可以断定直接内存是正常的。
1.3 元空间(metaspace)检查
根据服务的gc日志,可以看到metaspace占用的内存空间稳定在68M左右。故可以判断metaspace是正常的。
1.4 线程数检查
使用jstack $pid|grep Thread.State | wc -l命令统计服务进程的线程数,可以看到当前线程数是140,未指定线程栈大小(-XX:ThreadStackSize)的情况下,Linux 64bit操作系统的每个线程栈的最大值为1M,140条线程即占用140M,故可判断内存问题并非线程数造成的。
2. NMT分析
由上面的生产服务检查可知,该服务的堆内存、直接内存、元空间、线程数均正常,且当前该服务已经无法通过java监控命令分析其它内存域的使用情况,需要开启NMT(native memory tracking)才能进一步地分析其它部分的内存使用情况。
2.1 NMT介绍
NMT是Java HotSpot VM在JDK1.8之后新增的一项功能,可跟踪Java HotSpot VM的内部内存使用情况。
2.2 使用NMT检查内存泄漏
- 在JVM启动参数中加入-XX:NativeMemoryTracking=summary或者-XX:NativeMemoryTracking=detail。注意,该参数最好紧跟java命令的后面(例如,java -XX:NativeMemoryTracking=detail xxx xxx),和-D参数混在一起时会开启NMT失败。
进程启动成功后,使用jcmd <pid> VM.native_memory summary即可观察该进程的各种内存占用情况,如下图所示:
上图中的各项参数的描述如下:
reserved:操作系统预留的内存大小,即可划分的内存大小
committed:已划分的内存大小
Java Heap:堆内存,需要注意的时,此处的committed并不是指堆内存区域内部(jmap -heap <pid>)的使用情况,而是堆内存域在系统中占用的内存大小。
Class:元空间
Thread:线程栈内存
Code:生成的代码
GC:GC使用的数据,例如卡表等等
Compiler:生成代码时编译器使用的内存
Symbol:Symbols
Native Memory Tracking:NMT使用的内存
Internal:命令行工具、JVMTI等使用的内部内存
Arena Chunk:Memory used by chunks in the arena chunk pool
2.3 测试环境进行NMT分析
为尝试重现生产环境上出现的内存问题,在测试环境中做了以下操作:
- 开启NMT
- 对服务进行压力测试
- 使用jcmd <pid> VM.native_memory summary命令进行内存分析
但经过4个小时的压力测试,服务进程的内存没有很明显增长,内存稳定在1g。
2.4 生产环境开启NMT
由于测试环境没能重现,所以决定在生产环境开启NMT,等待服务运行一段时间后,通过jcmd <pid> VM.native_memory summary命令观察其内存变化。
另外,生产环境中的服务,之前使用了crontab跑了一个会导致服务Full GC的脚本(脚本中含有两条jmap -histo:live <pid>命令,该命令会使得java进程进行Full GC),每5秒会执行1次该脚本,所以决定把此不合理的脚本停止掉。
3. 新发现
3.1 猜测
测试环境中并没有定时地去跑导致服务不断Full GC的脚本,所以猜测服务的内存问题是否与Full GC有关系。
3.2 验证猜测
1. 使用以下参数启动服务,该参数指定使用G1垃圾收集器:
-server -Xms1g -Xmx1g -XX:+UseG1GC |
2. 服务启动后,使用jcmd <pid> VM.native_memory summary命令查看服务的内存分布,以及使用ps -p <pid> -o rss命令查看该服务占用的物理内存,并保存下来,以便和后面作对比。
3. 执行以下命令,使得服务每隔3秒执行一次Full GC:
# 根据实际将此处的pid修改为服务的进程号 pid=9484 while true do jmap -histo:live $pid sleep 3s done |
- 待服务持续Full GC一段时间后,再使用jcmd <pid> VM.native_memory summary命令查看服务的内存分布,及使用ps -p <pid> -o rss命令查看该服务占用的物理内存。
- 对比Full GC前后的内存变化,可以发现整个服务占用的物理内存越来越大,其中在不断增大的部分是在GC部分。
- 另外,启动服务时使用以下参数不会出现内存问题,以下的参数没有指定垃圾收集器,JDK1.8默认会使用平行收集器。
-server -Xms1g -Xmx1g |
3.3 结论
jdk1.8.0_171中,JVM使用G1垃圾收集器(-XX:+UseG1GC),Full GC时GC占用的内存会越来越大,且占用的内存不会释放(测试时达到了GC部分最高占用了2g,继续Full GC可能会更大);使用默认的平行回收器时,Full GC时GC占用的内存一直稳定,长时间Full GC也不会出现增长。