使用mtrace追踪JVM堆外内存泄露
原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明。
简介
在上篇文章中,介绍了使用tcmalloc或jemalloc定位native内存泄露的方法,但使用这个方法相当于更换了原生内存分配器,以至于使用时会有一些顾虑。
经过一些摸索,发现glibc自带的ptmalloc2分配器,也提供有追踪内存泄露的机制,即mtrace,这使得发生内存泄露时,可直接定位,而不需要额外安装及重启操作。
mtrace追踪内存泄露
glibc中提供了mtrace这个函数来开启追踪内存分配的功能,开启后每次应用程序调用malloc或free函数时,会将内存分配释放操作记录在MALLOC_TRACE环境变量所指的文件里面,如下:
$ pid=`pgrep java`
# 配置gdb不调试信号,避免JVM收到信号后被gdb暂停
$ cat <<"EOF" > ~/.gdbinit
handle all nostop noprint pass
handle SIGINT stop print nopass
EOF
# 设置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 -n
Memory not freed:
-----------------
Address Size Caller
0x00007efe08008cc0 0x18 at 0x7efe726e8e5d
0x00007efe08008ea0 0x160 at 0x7efe726e8e5d
0x00007efe6cabca40 0x58 at 0x7efe715dc432
0x00007efe6caa9ad0 0x1bf8 at 0x7efe715e4b88
0x00007efe6caab6d0 0x1bf8 at 0x7efe715e4b88
0x00007efe6ca679c0 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 -t
0x7efe715e4b88 1010 7231600
0x7efe715dc432 1010 88880
0x7efe715e4947 997 32669696
0x7efe726e8e5d 532 309800
0x7efe715eb2f4 1 72
0x7efe715eb491 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 0x7efe715e4b88
Address Kbytes RSS Dirty Mode Mapping
00007efe715d9000 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 stop
OK
profiler 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库函数了
使用mtrace追踪JVM堆外内存泄露的更多相关文章
- 解Bug之路-记一次JVM堆外内存泄露Bug的查找
解Bug之路-记一次JVM堆外内存泄露Bug的查找 前言 JVM的堆外内存泄露的定位一直是个比较棘手的问题.此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤 ...
- Netty堆外内存泄露排查与总结
导读 Netty 是一个异步事件驱动的网络通信层框架,用于快速开发高可用高性能的服务端网络框架与客户端程序,它极大地简化了 TCP 和 UDP 套接字服务器等网络编程. Netty 底层基于 JDK ...
- 一次完整的JVM堆外内存泄漏故障排查记录
前言 记录一次线上JVM堆外内存泄漏问题的排查过程与思路,其中夹带一些JVM内存分配机制以及常用的JVM问题排查指令和工具分享,希望对大家有所帮助. 在整个排查过程中,我也走了不少弯路,但是在文章中我 ...
- JVM堆外内存随笔
一 JVM堆外内存 1)java与io(file,socket)的操作都需要堆外内存与jvm内存进行互相拷贝,因为操作系统是不懂jvm的内存结构的(jvm的内存结构是自管理的),所以堆外内存存放的是操 ...
- Java堆外内存之七:JVM NativeMemoryTracking 分析堆外内存泄露
Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用情况的一个功能.我们可以利用jcmd(jdk自带)这个工具来访问NMT的数据. NMT介绍 工欲 ...
- 问题排查-JVM堆外内存问题排查
首先确认堆占用 jmap 查看heap内存使用情况 jmap -heap pid 1 可以查看到MetaspaceSize,CompressedClassSpaceSize,MaxMetaSize j ...
- JVM - 堆外内存
看了不少资料,总结下: 堆外内存 / 直接内存(Direct Memory)JDK1.4中引入的NIO类,基于channel和Buffer的I/O方式,可用Native库直接分配堆外内存,然后利用一个 ...
- JVM初探- 使用堆外内存减少Full GC
JVM初探-使用堆外内存减少Full GC 标签 : JVM 问题: 大部分主流互联网企业线上Server JVM选用了CMS收集器(如Taobao.LinkedIn.Vdian), 虽然CMS可与用 ...
- Netty堆外内存泄漏排查,这一篇全讲清楚了
上篇文章介绍了Netty内存模型原理,由于Netty在使用不当会导致堆外内存泄漏,网上关于这方面的资料比较少,所以写下这篇文章,专门介绍排查Netty堆外内存相关的知识点,诊断工具,以及排查思路提供参 ...
- Java堆外内存之三:堆外内存回收方法
一.JVM内存的分配及垃圾回收 对于JVM的内存规则,应该是老生常谈的东西了,这里我就简单的说下: 新生代:一般来说新创建的对象都分配在这里. 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面 ...
随机推荐
- Pytorch-PyG图神经网络依赖环境安装(Anaconda)
1.默认用户在Anaconda的虚拟环境中已安装Pytorch 2.打开anaconda prompt命令窗, activate "你的虚拟环境名称" 3.在激活后的虚拟环境下输入 ...
- 【Leetcode】 # 20 有效的括号 Rust Solution About Rust Stack implement
给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效.有效字符串需满足: 左括号必须用相同类型的右括号闭合. 左括号必须以正确的顺序闭合.注意空字符 ...
- 把jar包打成docker镜像并推送到Docker Hub
1.准备需要的jar包并复制到服务器某个目录下 2.在此目录下,创建Dockerfile的文本文件,并将以下内容添加到文件中: # 基础镜像 FROM openjdk:8-jre # author(可 ...
- Java求数组元素的最大值和最小值
代码如下: public static void main(String[] args) { int [] a = {1,2,3,88,2,90}; int max = a[0]; int min = ...
- Parallel 与 ConcurrentBag<T> 这对儿黄金搭档
〇.前言 日常开发中经常会遇到数据统计,特别是关于报表的项目.数据处理的效率和准确度当然是首要关注点. 本文主要介绍,如何通过 Parallel 来并行处理数据,并组合 ConcurrentBag&l ...
- python:map函数
参考示例 def test(x): return x * 2 mylist = [1, 2, 3, 4, 5] result = list(map(test, mylist)) print(resul ...
- 零基础入门——从零开始学习PHP反序列化笔记(一)
靶场环境搭建 方法一:PHPstudy搭建 GitHub地址 https://github.com/mcc0624/php_ser_Class 方法二:Docker部署 pull镜像文件 docker ...
- spring-mvc系列:详解@RequestMapping注解(value、method、params、header等)
目录 一.@RequestMapping注解的功能 二.@RequestMapping注解的位置 三.@RequestMapping注解的value属性 四.@RequestMapping注解的met ...
- .NET Core多线程 (1) Thread与Task
去年换工作时系统复习了一下.NET Core多线程相关专题,学习了一线码农老哥的<.NET 5多线程编程实战>课程,我将复习的知识进行了总结形成本专题.同时也特别推荐有兴趣的读者去学习一线 ...
- 使用kubeadm部署kubernetes
k8s版本:1.15.0 前期准备 节点: master:172.50.13.103(2核2G) node-1:172.50.13.104(2核2G) node-2:172.50.13.105(2核2 ...