Spring Boot引起的堆外内存泄露排查及经验总结
背景
为了更好地实现对项目的管理,
JVM的配置:”-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128M -XX:InitialCodeCacheSize=128M,-Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M"。
排查过程
-
使用Java层面的工具定位内存区域(堆内存、code缓存区或者使用unsafe.allocateMemory申请的堆外内存)
笔者在项目中添加“-XX:NativeMemeoryTracking=detail” JVM参数后重启项目,使用命令
jcmd pid VM.native_memory detail
查看到内存分布如下:
发现命令显示的commited的内存小于物理内存,因为jcmd的命令显示的内存包含了堆内存、code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包括其他Native Code申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。
为了防止误判,使用pmap查看内存分布,发现大量的64M的地址;而这些地址空间不再jcmd命令所给出的地址空间里面,基本上判定就是这些64M的内存所导致的。
命令如下:pmap -x pid |sort -k 3 -n -r
- 使用系统层面的工具定位堆外内存
因为我们已经基本能确认是Native Code引起的问题,而java层面的工具不便于排查此类问题,只能使用系统层面的工具定位问题。
- 首先,使用 gperftools去定位问题
gperftools的使用方法可以参考 gperftools, gperftools的监控如下:
从上图可以看出:使用malloc申请的内存最高到3G之后就释放了,之后时钟维持在700M-800M。难道Native Code 中没有使用malloc申请,直接使用mmap/brk申请的?
gperftools的原理就是使用东岱链接的方式替换操作系统默认的内存分配器(glibc)
- 然后,使用strace去追踪系统调用
因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪想OS申请内存请求,但是并没有发现有可疑内存申请。strace监控如下所示:
- 接着,使用GDB去dump可疑内存
因为使用strace没有追踪到可疑内存申请,于是,想着看看内存中的情况。直接使用命令:gdp-pid pid
进入GDB之后,然后使用命令:dump memory mem.bin startAddress endAddress
dump内存,其中startAddress和endAddress可疑从/proc/pid/smaps中查找。然后使用strings mem.bin
查看dump的内容,如下:
从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。
- 再次,项目启动时使用strace去追踪系统调用-
项目启动使用strace追踪系统调用,发现确实申请了很多64M的内存空间,截图如下:
使用该mmap申请的地址空间在pmap对应如下:
- 最后,使用jstack去查看对应的线程
因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid
去查看线程栈,找到对应的线程栈(注意10进制和16进制转换)如下: