Java應(yīng)用中本地內(nèi)存(native memory)泄漏排查方法及如何預(yù)防?
最近在開發(fā)的時(shí)候,遇到了內(nèi)存泄漏問題。排查的過程中也是走了一遍Java內(nèi)存泄漏的排查步驟。補(bǔ)充一點(diǎn),Java 在heap dump的時(shí)候,是不會(huì)把native memory給dump出來的。因此如果你的程序是native memory leak,那使用jmap等工具進(jìn)行heap dump拿到堆轉(zhuǎn)儲(chǔ)文件時(shí),是分析不出來什么有價(jià)值的東西的。 比如程序RSS內(nèi)存占用20GB,但是heap dump下來只有2GB。
那本文就想總結(jié)一下native memory內(nèi)存泄漏的排查。
主要有以下幾部分知識(shí)點(diǎn):
- NMT(本地內(nèi)存追蹤);
- pmap + gdb找到對(duì)應(yīng)內(nèi)存地址的內(nèi)容;
- 如何避免native memory內(nèi)存泄漏;
一、NMT(本地內(nèi)存追蹤,native memory tracking)
本地內(nèi)存跟蹤(NMT)是Java虛擬機(jī)(JVM)提供的一項(xiàng)功能,允許開發(fā)者監(jiān)控和分析Java應(yīng)用程序中本地內(nèi)存的分配和使用情況。它有助于識(shí)別和診斷本地內(nèi)存泄漏和過度內(nèi)存消耗。
下面詳細(xì)說下開啟NMT追蹤內(nèi)存的步驟:
1.1 開啟NMT開關(guān)
在Java程序的JVM參數(shù)里加下面的參數(shù):
-XX:NativeMemoryTracking=detail具體加的方式有:
1.1.1 使用命令行的方式
java -XX:NativeMemoryTracking=detail -jar YourApp.jar1.1.2 使用maven或者gradle構(gòu)建工具
如果你使用像Maven或Gradle這樣的構(gòu)建工具,你可以在構(gòu)建配置文件中配置JVM選項(xiàng)。例如,在Maven的pom.xml文件中,你可以添加以下配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>-XX:NativeMemoryTracking=detail</argLine>
</configuration>
</plugin>
</plugins>
</build>1.2 為NMT定義baseline
我們先用下面的命令定義一個(gè)baseline,這樣后面再執(zhí)行命令的時(shí)候,能跟這個(gè)baseline作對(duì)比。 可以更清楚的看到,內(nèi)存增長了多少。
jcmd <PID> VM.native_memory baseline scale=MB1.3 捕捉內(nèi)存details
使用如下命令,將得到native memory的快照,輸出到native_memory_detail文件里。
jcmd <PID> VM.native_memory detail scale=MB > native_memory_detailnative_memory_detail文件的內(nèi)容類似如下:
圖片
reserved — 系統(tǒng)為此進(jìn)程保留的內(nèi)存 committed — 當(dāng)前使用的數(shù)量
1.4 捕捉內(nèi)存diff
然后我們可以定期捕獲native memory diff,以檢查隨時(shí)間推移committed 內(nèi)存增加的組件。我們可以使用以下命令。
jcmd <PID> VM.native_memory detail.diff scale=MB > native_memory_diff仔細(xì)看,這個(gè)命令里用的是detail.diff。 使用這個(gè)diff版本的命令之后,我們會(huì)得到類似如下的輸出。 可以看到,在reserved 、committed值后面都有 + 號(hào),是與baseline做對(duì)比得到的。
圖片
其中Total: reserved=3111MB +521MB, committed=1401MB +842MB的意思是, reserved總計(jì)3111MB,比baseline多了521MB,committed同理。
+521MB表示的是變化量。
當(dāng)我們根據(jù)diff信息發(fā)現(xiàn)是哪些地方導(dǎo)致的內(nèi)存增長之后,可以使用jcmd <PID> help命令查看更多的選項(xiàng),然后針對(duì)性的track。 例如跟Class相關(guān)的。
jcmd <PID> VM.classloader_stats
jcmd <PID> VM.class_hierarchy
圖片
二、pmap命令 + gdb分析dump
使用 pmap -x [PID] 命令查看java程序的內(nèi)存使用。我們會(huì)看到類似如下的輸出:
Address Kbytes RSS Dirty Mode Mapping
0000000000400000 4 4 0 r-xp /usr/bin/pmap
0000000000600000 4 4 4 rw-p /usr/bin/pmap
...
00007f8b00000000 65512 40148 40148 rwx-- [ anon ]
00007f8b03ffa000 24 0 0 ----- [ anon ]
00007f8b04000000 65520 59816 59816 rwx-- [ anon ]
00007f8b07ffc000 16 0 0 ----- [ anon ]從左到右的列,依次是:
- Address: 內(nèi)存區(qū)域的起始地址
- Kbytes: 內(nèi)存區(qū)域大小(KB)
- RSS: 實(shí)際使用的物理內(nèi)存(KB)
- Dirty: 臟頁數(shù)量
- Mode: 內(nèi)存權(quán)限(r=讀, w=寫, x=執(zhí)行, s=共享, p=私有)
- Mapping: 內(nèi)存映射類型或文件名
我們接下來使用gdb來dump地址00007f8b00000000開始的內(nèi)存:
gdb -pid [pid]
dump memory mem.bin 0x00007f8b00000000 0x00007f8b00000000+65512接著將二進(jìn)制解析提取可讀字符串:
cat mem.bin | strings三、如何避免native memory內(nèi)存泄漏
3.1 正確管理Direct Memory:及時(shí)釋放ByteBuffer。
public class DirectMemoryManager {
private static final long MAX_DIRECT_MEMORY = 1024 * 1024 * 1024; // 1GB
private static final AtomicLong usedDirectMemory = new AtomicLong(0);
public static ByteBuffer allocateDirect(int size) {
long current = usedDirectMemory.get();
if (current + size > MAX_DIRECT_MEMORY) {
throw new OutOfMemoryError("Direct memory limit exceeded");
}
ByteBuffer buffer = ByteBuffer.allocateDirect(size);
usedDirectMemory.addAndGet(size);
return buffer;
}
public static void releaseDirect(ByteBuffer buffer) {
if (buffer.isDirect()) {
usedDirectMemory.addAndGet(-buffer.capacity());
buffer.clear();
}
}
}3.2 使用資源池:避免頻繁分配/釋放
public class DirectBufferPool {
private final Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
private final int bufferSize;
private final int maxPoolSize;
public DirectBufferPool(int bufferSize, int maxPoolSize) {
this.bufferSize = bufferSize;
this.maxPoolSize = maxPoolSize;
}
public ByteBuffer acquire() {
ByteBuffer buffer = pool.poll();
if (buffer == null) {
buffer = ByteBuffer.allocateDirect(bufferSize);
}
return buffer;
}
public void release(ByteBuffer buffer) {
if (buffer != null && buffer.isDirect() && pool.size() < maxPoolSize) {
buffer.clear();
pool.offer(buffer);
}
}
}3.3 啟用NMT監(jiān)控:及時(shí)發(fā)現(xiàn)內(nèi)存增長(會(huì)有性能損耗)
3.4 定期清理:使用GC和自定義清理機(jī)制
public class NativeMemoryCleaner {
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public static void startPeriodicCleanup() {
scheduler.scheduleAtFixedRate(() -> {
System.gc(); // 觸發(fā)GC,釋放Direct Memory
// 強(qiáng)制清理DirectByteBuffer
try {
Field cleanerField = Class.forName("java.nio.DirectByteBuffer")
.getDeclaredField("cleaner");
cleanerField.setAccessible(true);
// 這里可以添加自定義清理邏輯
} catch (Exception e) {
// 處理異常
}
}, 0, 5, TimeUnit.MINUTES);
}
}3.5 合理配置JVM參數(shù):限制內(nèi)存使用
# 限制Direct Memory大小
-XX:MaxDirectMemorySize=1g
# 啟用詳細(xì)GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
# 啟用NMT
-XX:NativeMemoryTracking=detail
# 設(shè)置合理的線程棧大小
-Xss256k






























