首页 > 系统相关 >使用mtrace追踪JVM堆外内存泄露

使用mtrace追踪JVM堆外内存泄露

时间:2023-09-23 20:44:05浏览次数:43  
标签:malloc 调用 cLibrary 函数 堆外 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,内容如下:
image_2023-09-23_20230923162642
可以发现,在开启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进程的虚拟内存布局,如下:
linux_pmem

  • Stack:栈,向下扩展,为线程分配的栈内存。
  • Memory Mapping Segment:内存映射区域,通过mmap分配,如映射的*.so动态库、动态分配的匿名内存等。
  • Heap:堆,向上扩展,动态分配内存的区域。
  • Data Segment:数据段,一般用来存储如C语言中的全局变量。
  • Code Segment:代码段,对于JVM来说,它从bin/java二进制文件加载而来。

而对于JVM来说,bin/java只是一个启动进程的壳,真正的代码基本都在动态库中,如libjvm.so、libzip.so等。

而在Linux中,动态库都是直接加载的,如下:
image_2023-09-23_20230923171505
因此,通过如下步骤,即可知道某个指令地址来自哪个函数,如下:

  • 根据指令地址,找到其所属的动态库,以及动态库在进程虚拟内存空间中的起始地址。
  • 根据指令地址减去起始地址,算出指令在动态库中的偏移量地址。
  • 反汇编动态库文件,根据偏移量地址查找指令所在函数。
  1. 找动态库及起始地址
$ 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列是其在进程虚拟内存空间中的起始地址。

  1. 计算指令在动态库中的偏移量
# 指令地址减去动态库起始地址
$ printf "%x" $((0x7efe715e4b88-0x00007efe715d9000))
bb88
  1. 反汇编并查找指令
$ objdump -d /opt/jdk8u222-b10/jre/lib/amd64/libzip.so | less -n

image_2023-09-23_20230923172923
可以发现,进程地址0x7efe715e4b88上的指令,在inflateInit2_函数中。

当然,上面步骤有点复杂,其实也可以通过gdb来查,如下:

gdb -q -batch -ex 'info symbol 0x7efe715e4b88' -p $pid

gdb_symbol

这样,我们找到了泄露的原生函数名,那是什么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调用栈,如下:
java_stack
至此,我们堆外内存泄露的代码路径就找到了,只需要再看看代码,识别一下哪些代码路径确实会导致内存泄露即可。

注:经过测试,发现profiler其实可以直接使用指令地址,所以不转换为函数名称,也是OK的。

通过jna开启mtrace

gdb实际是C/C++的调试程序,通过gdb来直接调用native函数,可能会出现一些不确定因素。

众所周知,Java提供了JNI机制,可实现Java调用native函数,而jna(Java Native Access)则对JNI技术进行了封装,大大简化了Java调用native函数的开发工作。

因此,我们可以使用jna来调用mtrace等native函数,如下:

  1. 引入jna库
<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>4.2.2</version>
</dependency>
  1. 封装并调用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库函数了

标签:malloc,调用,cLibrary,函数,堆外,mtrace,内存,JVM
From: https://www.cnblogs.com/codelogs/p/17725023.html

相关文章

  • JVM堆内存(heap)详解
    JAVA堆内存管理是影响性能主要因素之一。堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的。先看下JAVA堆内存是如何划分的,如图:Java堆内存又溢出了!教你一招必杀技   JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(YoungGeneration)、......
  • JVM面试题、关键原理、JMM
    boolean:占用1个字节,取值为true或false。byte:占用1个字节,范围为-128到127。short:占用2个字节,范围为-32,768到32,767。int:占用4个字节,范围为-2,147,483,648到2,147,483,647。long:占用8个字节,范围为-9,223,372,036,854,775,808到9,223,372,036,854,775,807。float:占用4个字节,范......
  • Tomcat的增加/查看jvm虚拟内存&设置JRE
    Win 操作系统 第一种方法:修改 tomcat/bin/catalina.bat 文件增加一行 setJAVA_OPTS=-Xms128M-Xmx512M-XX:PermSize=64M-XX:MaxPermSize=128M  第二种方法:在环境变量中设置变量名:JAVA_OPTS变量值:-Xms512m-Xmx512m 第三种方法:前两种方法针对的是bin目录下有catalina.bat......
  • 全网最详细Java-JVM
    Java-JVM①JVM概述❶基本介绍JVM:全称JavaVirtualMachine,一个虚拟计算机,Java程序的运行环境(Java二进制字节码的运行环境)特点:Java虚拟机基于二进制字节码执行,由一套字节码指令集、一组寄存器、一个栈、一个垃圾回收堆、一个方法区等组成JVM屏蔽了与操作系统平台相关的......
  • idea设置项目启动的JVM运行内存大小
    idea设置项目启动的JVM运行内存大小场景在开发当中,idea默认服务启动要占用1G内存。其实每个项目本地开发和调试的时候,根本不需要1G内存,200M左右足以如果在微服务体系下,那效果更明显,相同的内存可以启动更多的服务刚好本人的电脑只有8G,公司的微服务项目启动后,电脑风扇疯狂的转动......
  • JVM调优总结
    1.操作命令简介【Linux系统常用排查命令】free[-h]:显示系统的内存使用情况,包括总内存、已使用内存、空闲内存等信息,留意交换区内存信息df[-h]:显示磁盘空间大小du-sh./*:查看当前目录下子目录的大小,一般用于大文件排查(如大型日志文件)、磁盘空间排查场景top:可以持续的监......
  • 了解JVM
    一.了解JVM1.1什么是JVMJVM是JavaVirtualMachine(Java虚拟机)的缩写,是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟计算机功能来实现的,JVM屏蔽了与具体操作系统平台相关的信息,Java程序只需生成在Java虚拟机上运行的字节码,就可以在多种平台上不加修改的运行。JVM在执行字节......
  • 手把手教你模拟 JVM 内存溢出场景
    Java全能学习+面试指南:https://javaxiaobear.cn今天我们主要自己模拟一个JVM内存溢出的场景。在模拟JVM内存溢出之前我们先来看下这样的几个问题。老年代溢出为什么那么可怕?元空间也有溢出?怎么优化?如何配置栈大小?避免栈溢出?进程突然死掉,没有留下任何信息时如何进......
  • JVM-CMS分享
    01.垃圾回收算法 垃圾回收算法-种类标记-清除(mark-sweep)标记-压缩(mark-compact)复制算法(copy)分代收集算法(GenerationalGC) 标记-清除(mark-sweep) 原理:     1.标记死亡的对象为空闲内存,并记录在一个空闲链表(free-list)之中。     2.清除确定不可用的对象。缺点: ......
  • JVM--2021面试题系列教程(附答案解析)
    JVM--2021面试题系列教程(附答案解析)--大白话解读--JavaPub版本前言序言再高大上的框架,也需要扎实的基础才能玩转,高频面试问题更是基础中的高频实战要点。适合阅读人群Java学习者和爱好者,有一定工作经验的技术人,准面试官等。阅读建议本教程是系列教程,包含Java基础,JVM,容......