原创:扣钉日记(微信公众号ID:codelogs),欢送分享,非公众号转载保留此申明。
简介
在上篇文章中,介绍了应用tcmalloc或jemalloc定位native内存泄露的办法,但应用这个办法相当于更换了原生内存分配器,以至于应用时会有一些顾虑。
通过一些摸索,发现glibc自带的ptmalloc2分配器,也提供有追踪内存泄露的机制,即mtrace,这使得产生内存泄露时,可间接定位,而不须要额定装置及重启操作。
mtrace追踪内存泄露
glibc中提供了mtrace这个函数来开启追踪内存调配的性能,开启后每次应用程序调用malloc或free函数时,会将内存调配开释操作记录在MALLOC_TRACE环境变量所指的文件外面,如下:
$ pid=`pgrep java`# 配置gdb不调试信号,防止JVM收到信号后被gdb暂停$ cat <<"EOF" > ~/.gdbinithandle all nostop noprint passhandle SIGINT stop print nopassEOF# 设置MALLOC_TRACE环境变量,将内存调配操作记录在malloc_trace.log里$ gdb -q -batch -ex 'call setenv("MALLOC_TRACE", "./malloc_trace.log", 1)' -p $pid# 调用mtrace开启内存调配追踪$ gdb -q -batch -ex 'call mtrace()' -p $pid# 一段时间后,调用muntrace敞开追踪$ gdb -q -batch -ex 'call muntrace()' -p $pid
而后查看malloc_trace.log,内容如下:
能够发现,在开启mtrace后,glibc将所有malloc、free操作都记录了下来,通过从日志中找出哪些地方执行了malloc后没有free,即是内存泄露点。
于是glibc又提供了一个mtrace命令,其作用就是找出下面说的执行了malloc后没有free的记录,如下:
$ mtrace malloc_trace.log | less -nMemory not freed:----------------- Address Size Caller0x00007efe08008cc0 0x18 at 0x7efe726e8e5d0x00007efe08008ea0 0x160 at 0x7efe726e8e5d0x00007efe6cabca40 0x58 at 0x7efe715dc4320x00007efe6caa9ad0 0x1bf8 at 0x7efe715e4b880x00007efe6caab6d0 0x1bf8 at 0x7efe715e4b880x00007efe6ca679c0 0x8000 at 0x7efe715e4947# 按Caller分组统计一下,看看各Caller各泄露的次数及内存量$ mtrace malloc_trace.log | sed '1,/Caller/d'|awk '{s[$NF]+=strtonum($2);n[$NF]++;}END{for(k in s){print k,n[k],s[k]}}'|column -t0x7efe715e4b88 1010 72316000x7efe715dc432 1010 888800x7efe715e4947 997 326696960x7efe726e8e5d 532 3098000x7efe715eb2f4 1 720x7efe715eb491 1 38
能够发现,0x7efe715e4b88这个调用点,泄露了1010次,那怎么晓得这个调用点在哪个函数里呢?
依据指令地址找函数
之前咱们介绍过Linux过程的虚拟内存布局,如下:
- Stack:栈,向下扩大,为线程调配的栈内存。
- Memory Mapping Segment:内存映射区域,通过mmap调配,如映射的*.so动静库、动态分配的匿名内存等。
- Heap:堆,向上扩大,动静分配内存的区域。
- Data Segment:数据段,个别用来存储如C语言中的全局变量。
- Code Segment:代码段,对于JVM来说,它从bin/java二进制文件加载而来。
而对于JVM来说,bin/java只是一个启动过程的壳,真正的代码根本都在动静库中,如libjvm.so、libzip.so等。
而在Linux中,动静库都是间接加载的,如下:
因而,通过如下步骤,即可晓得某个指令地址来自哪个函数,如下:
- 依据指令地址,找到其所属的动静库,以及动静库在过程虚拟内存空间中的起始地址。
- 依据指令地址减去起始地址,算出指令在动静库中的偏移量地址。
- 反汇编动静库文件,依据偏移量地址查找指令所在函数。
找动静库及起始地址
$ pmap -x $pid -p -A 0x7efe715e4b88Address Kbytes RSS Dirty Mode Mapping00007efe715d9000 108 108 0 r-x-- /opt/jdk8u222-b10/jre/lib/amd64/libzip.so---------------- ------- ------- -------total kB 108 163232 160716
通过pmap的-A选项,能够通过内存地址找内存映射区域,如上,Mapping列就是内存映射区域对应的动静库文件,而Address列是其在过程虚拟内存空间中的起始地址。
计算指令在动静库中的偏移量
# 指令地址减去动静库起始地址$ printf "%x" $((0x7efe715e4b88-0x00007efe715d9000))bb88
反汇编并查找指令
$ objdump -d /opt/jdk8u222-b10/jre/lib/amd64/libzip.so | less -n
能够发现,过程地址0x7efe715e4b88
上的指令,在inflateInit2_
函数中。
当然,下面步骤有点简单,其实也能够通过gdb来查,如下:
gdb -q -batch -ex 'info symbol 0x7efe715e4b88' -p $pid
这样,咱们找到了泄露的原生函数名,那是什么java代码调用到这个函数的呢?
通过原生函数名找Java调用栈
通过arthas的profiler命令,能够采样到原生函数的调用栈,如下:
[arthas@1]$ profiler execute 'start,event=inflateInit2_,alluser'Profiling started[arthas@1]$ profiler stopOKprofiler output file: .../arthas-output/20230923-173944.html
关上这个html文件,能够发现相干的Java调用栈,如下:
至此,咱们堆外内存泄露的代码门路就找到了,只须要再看看代码,辨认一下哪些代码门路的确会导致内存泄露即可。
注:通过测试,发现profiler其实能够间接应用指令地址,所以不转换为函数名称,也是OK的。
通过jna开启mtrace
gdb理论是C/C++的调试程序,通过gdb来间接调用native函数,可能会呈现一些不确定因素。
家喻户晓,Java提供了JNI机制,可实现Java调用native函数,而jna(Java Native Access)则对JNI技术进行了封装,大大简化了Java调用native函数的开发工作。
因而,咱们能够应用jna来调用mtrace等native函数,如下:
引入jna库
<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>4.2.2</version></dependency>
封装并调用native函数
public class JnaTool { public interface CLibrary extends Library { void malloc_stats(); void malloc_trim(int pad); void setenv(String name, String value, int overwrite); void mtrace(); void muntrace(); } private static CLibrary cLibrary; static { try { cLibrary = (CLibrary) Native.loadLibrary("c", CLibrary.class); } catch (Exception e) { e.printStackTrace(); } } public static void mtrace(String traceFile) { if (cLibrary == null) return; cLibrary.setenv("MALLOC_TRACE", traceFile, 1); cLibrary.mtrace(); } public static void muntrace() { if (cLibrary == null) return; cLibrary.muntrace(); } public static void mallocStats() { if (cLibrary == null) return; cLibrary.malloc_stats(); } public static void mallocTrim() { if (cLibrary == null) return; cLibrary.malloc_trim(0); }}
这样,就能够防止应用gdb而调用一些C库函数了