使用mtrace追蹤JVM堆外內(nèi)存泄露的方法
mtrace追蹤內(nèi)存泄露
glibc中提供了mtrace這個(gè)函數(shù)來開啟追蹤內(nèi)存分配的功能,開啟后每次應(yīng)用程序調(diào)用malloc或free函數(shù)時(shí),會將內(nèi)存分配釋放操作記錄在MALLOC_TRACE環(huán)境變量所指的文件里面,如下:
$ pid=`pgrep java`
# 配置gdb不調(diào)試信號,避免JVM收到信號后被gdb暫停
$ cat <<"EOF" > ~/.gdbinit
handle all nostop noprint pass
handle SIGINT stop print nopass
EOF
# 設(shè)置MALLOC_TRACE環(huán)境變量,將內(nèi)存分配操作記錄在malloc_trace.log里
$ gdb -q -batch -ex 'call setenv("MALLOC_TRACE", "./malloc_trace.log", 1)' -p $pid
# 調(diào)用mtrace開啟內(nèi)存分配追蹤
$ gdb -q -batch -ex 'call mtrace()' -p $pid
# 一段時(shí)間后,調(diào)用muntrace關(guān)閉追蹤
$ gdb -q -batch -ex 'call muntrace()' -p $pid然后查看malloc_trace.log,內(nèi)容如下:

可以發(fā)現(xiàn),在開啟mtrace后,glibc將所有malloc、free操作都記錄了下來,通過從日志中找出哪些地方執(zhí)行了malloc后沒有free,即是內(nèi)存泄露點(diǎn)。
于是glibc又提供了一個(gè)mtrace命令,其作用就是找出上面說的執(zhí)行了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分組統(tǒng)計(jì)一下,看看各Caller各泄露的次數(shù)及內(nèi)存量
$ 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可以發(fā)現(xiàn),0x7efe715e4b88這個(gè)調(diào)用點(diǎn),泄露了1010次,那怎么知道這個(gè)調(diào)用點(diǎn)在哪個(gè)函數(shù)里呢?
根據(jù)指令地址找函數(shù)
之前我們介紹過Linux進(jìn)程的虛擬內(nèi)存布局,如下:

- Stack:棧,向下擴(kuò)展,為線程分配的棧內(nèi)存。
- Memory Mapping Segment:內(nèi)存映射區(qū)域,通過mmap分配,如映射的*.so動態(tài)庫、動態(tài)分配的匿名內(nèi)存等。
- Heap:堆,向上擴(kuò)展,動態(tài)分配內(nèi)存的區(qū)域。
- Data Segment:數(shù)據(jù)段,一般用來存儲如C語言中的全局變量。
- Code Segment:代碼段,對于JVM來說,它從bin/java二進(jìn)制文件加載而來。
而對于JVM來說,bin/java只是一個(gè)啟動進(jìn)程的殼,真正的代碼基本都在動態(tài)庫中,如libjvm.so、libzip.so等。
而在Linux中,動態(tài)庫都是直接加載的,如下:

因此,通過如下步驟,即可知道某個(gè)指令地址來自哪個(gè)函數(shù),如下:
- 根據(jù)指令地址,找到其所屬的動態(tài)庫,以及動態(tài)庫在進(jìn)程虛擬內(nèi)存空間中的起始地址。
- 根據(jù)指令地址減去起始地址,算出指令在動態(tài)庫中的偏移量地址。
- 反匯編動態(tài)庫文件,根據(jù)偏移量地址查找指令所在函數(shù)。
- 找動態(tài)庫及起始地址
$ 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選項(xiàng),可以通過內(nèi)存地址找內(nèi)存映射區(qū)域,如上,Mapping列就是內(nèi)存映射區(qū)域?qū)?yīng)的動態(tài)庫文件,而Address列是其在進(jìn)程虛擬內(nèi)存空間中的起始地址。
- 計(jì)算指令在動態(tài)庫中的偏移量
# 指令地址減去動態(tài)庫起始地址 $ printf "%x" $((0x7efe715e4b88-0x00007efe715d9000)) bb88
- 反匯編并查找指令
$ objdump -d /opt/jdk8u222-b10/jre/lib/amd64/libzip.so | less -n

可以發(fā)現(xiàn),進(jìn)程地址0x7efe715e4b88上的指令,在inflateInit2_函數(shù)中。
當(dāng)然,上面步驟有點(diǎn)復(fù)雜,其實(shí)也可以通過gdb來查,如下:
gdb -q -batch -ex 'info symbol 0x7efe715e4b88' -p $pid

這樣,我們找到了泄露的原生函數(shù)名,那是什么java代碼調(diào)用到這個(gè)函數(shù)的呢?
通過原生函數(shù)名找Java調(diào)用棧
通過arthas的profiler命令,可以采樣到原生函數(shù)的調(diào)用棧,如下:
[arthas@1]$ profiler execute 'start,event=inflateInit2_,alluser' Profiling started [arthas@1]$ profiler stop OK profiler output file: .../arthas-output/20230923-173944.html
打開這個(gè)html文件,可以發(fā)現(xiàn)相關(guān)的Java調(diào)用棧,如下:

外內(nèi)存泄露的代碼路徑就找到了,只需要再看看代碼,識別一下哪些代碼路徑確實(shí)會導(dǎo)致內(nèi)存泄露即可。
注:經(jīng)過測試,發(fā)現(xiàn)profiler其實(shí)可以直接使用指令地址,所以不轉(zhuǎn)換為函數(shù)名稱,也是OK的。
通過jna開啟mtrace
gdb實(shí)際是C/C++的調(diào)試程序,通過gdb來直接調(diào)用native函數(shù),可能會出現(xiàn)一些不確定因素。
眾所周知,Java提供了JNI機(jī)制,可實(shí)現(xiàn)Java調(diào)用native函數(shù),而jna(Java Native Access)則對JNI技術(shù)進(jìn)行了封裝,大大簡化了Java調(diào)用native函數(shù)的開發(fā)工作。
因此,我們可以使用jna來調(diào)用mtrace等native函數(shù),如下:
- 引入jna庫
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.2.2</version>
</dependency>- 封裝并調(diào)用native函數(shù)
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而調(diào)用一些C庫函數(shù)了
以上就是使用mtrace追蹤JVM堆外內(nèi)存泄露的方法的詳細(xì)內(nèi)容,更多關(guān)于mtrace追蹤JVM內(nèi)存泄露的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
完整的醫(yī)院就診掛號系統(tǒng)基于Spring MVC + Spring + MyBatis實(shí)現(xiàn)
這篇文章主要介紹了基于Spring MVC + Spring + MyBatis實(shí)現(xiàn)的醫(yī)院就診掛號系統(tǒng),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
JavaEE簡介_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了JavaEE簡介,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-07-07
play for scala 實(shí)現(xiàn)SessionFilter 過濾未登錄用戶跳轉(zhuǎn)到登錄頁面
這篇文章主要介紹了play for scala 實(shí)現(xiàn)SessionFilter 過濾未登錄用戶跳轉(zhuǎn)到登錄頁面的相關(guān)資料,需要的朋友可以參考下2016-11-11
詳解SpringBoot如何優(yōu)雅的進(jìn)行前后端通信
現(xiàn)在的項(xiàng)目基本上都是前后端分離的項(xiàng)目,如何打通前后端,接收前端傳過來的參數(shù)呢,下面小編就來和大家詳細(xì)介紹一下SpringBoot如何優(yōu)雅的進(jìn)行前后端通信2024-03-03
Java基于jeeplus vue實(shí)現(xiàn)簡單工作流過程圖解
這篇文章主要介紹了Java基于jeeplus vue實(shí)現(xiàn)簡單工作流過程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-04-04
SpringCloud聲明式Feign客戶端調(diào)用工具使用
這篇文章主要為大家介紹了SpringCloud聲明式Feign客戶端調(diào)用工具使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
關(guān)于springboot 中使用httpclient或RestTemplate做MultipartFile文件跨服務(wù)傳輸
這篇文章主要介紹了關(guān)于springboot 中使用httpclient或RestTemplate做MultipartFile文件跨服務(wù)傳輸?shù)膯栴},本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
在idea中創(chuàng)建SpringBoot模塊的兩種方式
這篇文章主要介紹了在idea中創(chuàng)建一個(gè)SpringBoot模塊,本文給大家分享兩種方式,每種方式分步驟給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05
Java線程隊(duì)列LinkedBlockingQueue的使用
本文主要介紹了Java線程隊(duì)列LinkedBlockingQueue的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06

