Java應用中本地內存(native memory)泄漏排查方法及如何預防?
最近在開發的時候,遇到了內存泄漏問題。排查的過程中也是走了一遍Java內存泄漏的排查步驟。補充一點,Java 在heap dump的時候,是不會把native memory給dump出來的。因此如果你的程序是native memory leak,那使用jmap等工具進行heap dump拿到堆轉儲文件時,是分析不出來什么有價值的東西的。 比如程序RSS內存占用20GB,但是heap dump下來只有2GB。
那本文就想總結一下native memory內存泄漏的排查。
主要有以下幾部分知識點:
- NMT(本地內存追蹤);
- pmap + gdb找到對應內存地址的內容;
- 如何避免native memory內存泄漏;
一、NMT(本地內存追蹤,native memory tracking)
本地內存跟蹤(NMT)是Java虛擬機(JVM)提供的一項功能,允許開發者監控和分析Java應用程序中本地內存的分配和使用情況。它有助于識別和診斷本地內存泄漏和過度內存消耗。
下面詳細說下開啟NMT追蹤內存的步驟:
1.1 開啟NMT開關
在Java程序的JVM參數里加下面的參數:
-XX:NativeMemoryTracking=detail
具體加的方式有:
1.1.1 使用命令行的方式
java -XX:NativeMemoryTracking=detail -jar YourApp.jar
1.1.2 使用maven或者gradle構建工具
如果你使用像Maven或Gradle這樣的構建工具,你可以在構建配置文件中配置JVM選項。例如,在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
我們先用下面的命令定義一個baseline,這樣后面再執行命令的時候,能跟這個baseline作對比。 可以更清楚的看到,內存增長了多少。
jcmd <PID> VM.native_memory baseline scale=MB
1.3 捕捉內存details
使用如下命令,將得到native memory的快照,輸出到native_memory_detail文件里。
jcmd <PID> VM.native_memory detail scale=MB > native_memory_detail
native_memory_detail文件的內容類似如下:
圖片
reserved — 系統為此進程保留的內存 committed — 當前使用的數量
1.4 捕捉內存diff
然后我們可以定期捕獲native memory diff,以檢查隨時間推移committed 內存增加的組件。我們可以使用以下命令。
jcmd <PID> VM.native_memory detail.diff scale=MB > native_memory_diff
仔細看,這個命令里用的是detail.diff
。 使用這個diff版本的命令之后,我們會得到類似如下的輸出。 可以看到,在reserved 、committed值后面都有 +
號,是與baseline做對比得到的。
圖片
其中Total: reserved=3111MB +521MB, committed=1401MB +842MB
的意思是, reserved總計3111MB,比baseline多了521MB,committed同理。
+521MB表示的是變化量。
當我們根據diff信息發現是哪些地方導致的內存增長之后,可以使用jcmd <PID> help
命令查看更多的選項,然后針對性的track。 例如跟Class相關的。
jcmd <PID> VM.classloader_stats
jcmd <PID> VM.class_hierarchy
圖片
二、pmap命令 + gdb分析dump
使用 pmap -x [PID]
命令查看java程序的內存使用。我們會看到類似如下的輸出:
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: 內存區域的起始地址
- Kbytes: 內存區域大小(KB)
- RSS: 實際使用的物理內存(KB)
- Dirty: 臟頁數量
- Mode: 內存權限(r=讀, w=寫, x=執行, s=共享, p=私有)
- Mapping: 內存映射類型或文件名
我們接下來使用gdb來dump地址00007f8b00000000開始的內存:
gdb -pid [pid]
dump memory mem.bin 0x00007f8b00000000 0x00007f8b00000000+65512
接著將二進制解析提取可讀字符串:
cat mem.bin | strings
三、如何避免native memory內存泄漏
3.1 正確管理Direct Memory:及時釋放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監控:及時發現內存增長(會有性能損耗)
3.4 定期清理:使用GC和自定義清理機制
public class NativeMemoryCleaner {
private static final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public static void startPeriodicCleanup() {
scheduler.scheduleAtFixedRate(() -> {
System.gc(); // 觸發GC,釋放Direct Memory
// 強制清理DirectByteBuffer
try {
Field cleanerField = Class.forName("java.nio.DirectByteBuffer")
.getDeclaredField("cleaner");
cleanerField.setAccessible(true);
// 這里可以添加自定義清理邏輯
} catch (Exception e) {
// 處理異常
}
}, 0, 5, TimeUnit.MINUTES);
}
}
3.5 合理配置JVM參數:限制內存使用
# 限制Direct Memory大小
-XX:MaxDirectMemorySize=1g
# 啟用詳細GC日志
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
# 啟用NMT
-XX:NativeMemoryTracking=detail
# 設置合理的線程棧大小
-Xss256k