背景

为了更好地实现对项目的管理,

JVM的配置:”-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+AlwaysPreTouch -XX:ReservedCodeCacheSize=128M -XX:InitialCodeCacheSize=128M,-Xss512k -Xmx4g -Xms4g,-XX:+UseG1GC -XX:G1HeapRegionSize=4M"。

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

排查过程

  1. 使用Java层面的工具定位内存区域(堆内存、code缓存区或者使用unsafe.allocateMemory申请的堆外内存)

    笔者在项目中添加“-XX:NativeMemeoryTracking=detail” JVM参数后重启项目,使用命令jcmd pid VM.native_memory detail查看到内存分布如下:

    《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

发现命令显示的commited的内存小于物理内存,因为jcmd的命令显示的内存包含了堆内存、code区域、通过unsafe.allocateMemory和DirectByteBuffer申请的内存,但是不包括其他Native Code申请的堆外内存。所以猜测是使用Native Code申请内存所导致的问题。

为了防止误判,使用pmap查看内存分布,发现大量的64M的地址;而这些地址空间不再jcmd命令所给出的地址空间里面,基本上判定就是这些64M的内存所导致的。

命令如下:pmap -x pid |sort -k 3 -n -r

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

  1. 使用系统层面的工具定位堆外内存

因为我们已经基本能确认是Native Code引起的问题,而java层面的工具不便于排查此类问题,只能使用系统层面的工具定位问题。

  • 首先,使用 gperftools去定位问题

gperftools的使用方法可以参考 gperftools, gperftools的监控如下:

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

从上图可以看出:使用malloc申请的内存最高到3G之后就释放了,之后时钟维持在700M-800M。难道Native Code 中没有使用malloc申请,直接使用mmap/brk申请的?

gperftools的原理就是使用东岱链接的方式替换操作系统默认的内存分配器(glibc)

  • 然后,使用strace去追踪系统调用

因为使用gperftools没有追踪到这些内存,于是直接使用命令“strace -f -e"brk,mmap,munmap" -p pid”追踪想OS申请内存请求,但是并没有发现有可疑内存申请。strace监控如下所示:

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

  • 接着,使用GDB去dump可疑内存

因为使用strace没有追踪到可疑内存申请,于是,想着看看内存中的情况。直接使用命令:gdp-pid pid进入GDB之后,然后使用命令:dump memory mem.bin startAddress endAddressdump内存,其中startAddress和endAddress可疑从/proc/pid/smaps中查找。然后使用strings mem.bin查看dump的内容,如下:

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

从内容上来看,像是解压后的JAR包信息。读取JAR包信息应该是在项目启动的时候,那么在项目启动之后使用strace作用就不是很大了。所以应该在项目启动的时候使用strace,而不是启动完成之后。

  • 再次,项目启动时使用strace去追踪系统调用-

项目启动使用strace追踪系统调用,发现确实申请了很多64M的内存空间,截图如下:

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

使用该mmap申请的地址空间在pmap对应如下:

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》

  • 最后,使用jstack去查看对应的线程

因为strace命令中已经显示申请内存的线程ID。直接使用命令jstack pid去查看线程栈,找到对应的线程栈(注意10进制和16进制转换)如下:

《Spring Boot引起的“堆外内存泄漏”排查及经验总结》