虾皮Java面试被问:JVM Native Memory Tracking追踪堆外内存泄漏
摘要:JVM Native Memory Tracking (NMT) 是诊断堆外内存泄漏的重要工具,可监控12类JVM内部内存区域。本文详细介绍了NMT的启用配置(summary/detail模式)、监控命令(baseline/diff对比)及典型泄漏场景(DirectByteBuffer、线程泄漏等)。通过示例解析NMT输出报告,提供诊断流程:建立基线→模拟操作→差异分析→结合pmap等工具定
JVM Native Memory Tracking (NMT) 追踪堆外内存泄漏
📊 NMT基础概念
什么是NMT?
Native Memory Tracking是HotSpot JVM内置的追踪工具,用于监控JVM内部的内存使用情况。
监控范围
bash
复制
下载
# NMT追踪的内存区域: 1. Java Heap # Java堆 2. Class # 类元数据 3. Thread # 线程栈 4. Code # JIT编译代码 5. GC # 垃圾收集器 6. Compiler # 编译器 7. Internal # 命令行解析、JVMTI等 8. Symbol # 符号表 9. Native Memory Tracking # NMT自身开销 10. Arena Chunk # 分配器竞技场 11. Test # 测试代码 12. Other # 未分类内存
🔧 NMT启用与配置
启动参数配置
bash
复制
下载
# 基础启用
java -XX:NativeMemoryTracking=summary -jar app.jar
# 详细模式(提供更多细节)
java -XX:NativeMemoryTracking=detail -jar app.jar
# 基准模式(记录初始内存使用)
java -XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintNMTStatistics -jar app.jar
运行时监控命令
bash
复制
下载
# 1. 获取当前内存使用情况 jcmd <pid> VM.native_memory summary # 2. 获取详细内存使用情况 jcmd <pid> VM.native_memory detail # 3. 获取按类别统计 jcmd <pid> VM.native_memory summary scale=MB # 4. 基线测量(记录当前状态作为基准) jcmd <pid> VM.native_memory baseline # 5. 对比差异(与基线对比) jcmd <pid> VM.native_memory summary.diff # 6. 详细差异对比 jcmd <pid> VM.native_memory detail.diff # 7. 按类别显示差异 jcmd <pid> VM.native_memory summary.diff scale=KB
🔍 常见堆外内存泄漏场景
1. Direct ByteBuffer泄漏
java
复制
下载
// 常见的DirectByteBuffer泄漏代码
public class DirectMemoryLeak {
private List<ByteBuffer> bufferList = new ArrayList<>();
public void leakMemory() {
// 每次分配100MB直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(100 * 1024 * 1024);
bufferList.add(buffer); // 忘记清理
}
// 正确做法:需要显式释放
public void cleanDirectBuffer(ByteBuffer buffer) {
if (buffer.isDirect()) {
// 需要等GC回收或手动调用Cleaner
((DirectBuffer) buffer).cleaner().clean();
}
}
}
2. MappedByteBuffer泄漏
java
复制
下载
public class MappedBufferLeak {
private List<MappedByteBuffer> mappedBuffers = new ArrayList<>();
public void mapFile(String filePath) throws IOException {
RandomAccessFile file = new RandomAccessFile(filePath, "rw");
FileChannel channel = file.getChannel();
// 内存映射,会占用Native Memory
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size()
);
mappedBuffers.add(buffer);
// 忘记关闭:channel.close() 和 file.close()
}
}
3. JNI代码内存泄漏
java
复制
下载
public class JNIMemoryLeak {
static {
System.loadLibrary("nativeLib");
}
// Native方法可能分配内存
public native void allocateNativeMemory(long size);
public native void freeNativeMemory();
public void causeLeak() {
allocateNativeMemory(1024 * 1024); // 分配1MB
// 忘记调用freeNativeMemory()
}
}
4. 线程创建过多
java
复制
下载
public class ThreadLeak {
private ExecutorService executor = Executors.newCachedThreadPool();
public void createThreads() {
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 每个线程栈占用1MB(默认),10k线程占用10GB Native Memory
}
}
📈 NMT输出解析
示例输出分析
text
复制
下载
Native Memory Tracking:
Total: reserved=6643041KB, committed=397016KB
- Java Heap (reserved=5070848KB, committed=317440KB)
(mmap: reserved=5070848KB, committed=317440KB)
- Class (reserved=1070513KB, committed=12609KB)
(classes #1199)
(malloc=9329KB #1525)
(mmap: reserved=1067184KB, committed=3280KB)
- Thread (reserved=26546KB, committed=26546KB)
(thread #26)
(stack: reserved=26432KB, committed=26432KB)
(malloc=96KB #52)
(arena=18KB #50)
- Code (reserved=249632KB, committed=2592KB)
(malloc=32KB #299)
(mmap: reserved=249600KB, committed=2560KB)
- GC (reserved=47739KB, committed=47739KB)
(malloc=10423KB #117)
(mmap: reserved=37316KB, committed=37316KB)
- Compiler (reserved=132KB, committed=132KB)
(malloc=1KB #21)
(arena=131KB #3)
- Internal (reserved=580KB, committed=580KB)
(malloc=548KB #651)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=1525KB, committed=1525KB)
(malloc=976KB #111)
(arena=549KB #1)
- Native Memory Tracking (reserved=161KB, committed=161KB)
(malloc=99KB #1561)
(tracking overhead=62KB)
- Arena Chunk (reserved=175KB, committed=175KB)
(malloc=175KB)
- Unknown (reserved=33KB, committed=33KB)
(mmap: reserved=33KB, committed=33KB)
关键指标解读
bash
复制
下载
# 重点监控区域: 1. Thread区域持续增长 → 线程泄漏 2. Internal区域异常大 → 可能DirectByteBuffer泄漏 3. GC区域过大 → GC配置问题 4. Arena Chunk异常增长 → 内存池泄漏
🚨 堆外内存泄漏诊断流程
步骤1:启用NMT监控
bash
复制
下载
# 启动应用
java -XX:NativeMemoryTracking=detail \
-XX:+UnlockDiagnosticVMOptions \
-XX:+PrintNMTStatistics \
-jar application.jar
# 获取进程ID
jps -l
# 建立基线
jcmd <pid> VM.native_memory baseline
步骤2:模拟操作并收集数据
bash
复制
下载
# 执行疑似泄漏的操作 curl http://localhost:8080/leaky-endpoint # 等待一段时间 sleep 30 # 获取差异报告 jcmd <pid> VM.native_memory summary.diff
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
步骤3:分析泄漏点
bash
复制
下载
# 详细差异分析 jcmd <pid> VM.native_memory detail.diff > nmt_diff.txt # 重点关注增长的区域 grep -A 5 "Total: " nmt_diff.txt grep "committed" nmt_diff.txt | sort -k2 -nr
步骤4:使用pmap进一步分析
bash
复制
下载
# 查看进程内存映射
pmap -x <pid> | sort -k2 -nr | head -20
# 查看anon内存段
pmap <pid> | grep anon | awk '{sum+=$2} END {print sum}'
🔧 高级诊断技巧
结合其他工具分析
bash
复制
下载
# 1. 使用gdb分析Native Memory gdb -p <pid> (gdb) malloc_info 0 /tmp/malloc_info.xml # 2. 使用strace追踪系统调用 strace -f -e trace=mmap,munmap,brk -p <pid> 2>&1 | grep -v ENOENT # 3. 使用/proc文件系统 cat /proc/<pid>/maps | grep heap # 查看堆内存 cat /proc/<pid>/smaps | grep -A 15 "heap" # 详细堆信息 # 4. 监控系统内存 watch -n 1 'ps aux | grep java | grep -v grep'
自动化监控脚本
bash
复制
下载
#!/bin/bash
# monitor_nmt.sh
PID=$1
INTERVAL=10
OUTPUT_DIR="./nmt_logs"
mkdir -p $OUTPUT_DIR
while true; do
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 收集NMT数据
jcmd $PID VM.native_memory detail > "$OUTPUT_DIR/nmt_detail_$TIMESTAMP.txt"
jcmd $PID VM.native_memory summary > "$OUTPUT_DIR/nmt_summary_$TIMESTAMP.txt"
# 收集系统内存信息
ps -p $PID -o pid,rss,vsz,pcpu,pmem,cmd >> "$OUTPUT_DIR/system_mem_$TIMESTAMP.txt"
pmap $PID | tail -1 >> "$OUTPUT_DIR/pmap_$TIMESTAMP.txt"
# 分析增长趋势
if [ -f "$OUTPUT_DIR/nmt_summary_previous.txt" ]; then
diff "$OUTPUT_DIR/nmt_summary_previous.txt" \
"$OUTPUT_DIR/nmt_summary_$TIMESTAMP.txt" \
> "$OUTPUT_DIR/diff_$TIMESTAMP.txt"
fi
cp "$OUTPUT_DIR/nmt_summary_$TIMESTAMP.txt" \
"$OUTPUT_DIR/nmt_summary_previous.txt"
sleep $INTERVAL
done
🛠️ 代码层面的预防措施
1. DirectByteBuffer管理类
java
复制
下载
public class DirectMemoryManager {
private static final List<ByteBuffer> BUFFERS = new ArrayList<>();
private static final Cleaner CLEANER = Cleaner.create();
public static ByteBuffer allocateDirect(long size) {
ByteBuffer buffer = ByteBuffer.allocateDirect((int) size);
BUFFERS.add(buffer);
// 注册清理操作
CLEANER.register(buffer, () -> {
if (buffer.isDirect()) {
((DirectBuffer) buffer).cleaner().clean();
}
});
return buffer;
}
public static void cleanAll() {
BUFFERS.forEach(buffer -> {
if (buffer.isDirect() && buffer.capacity() > 0) {
((DirectBuffer) buffer).cleaner().clean();
}
});
BUFFERS.clear();
}
}
2. 资源关闭模板
java
复制
下载
public class ResourceTemplate {
public static void withMappedBuffer(String filePath, Consumer<MappedByteBuffer> action) {
RandomAccessFile file = null;
FileChannel channel = null;
try {
file = new RandomAccessFile(filePath, "rw");
channel = file.getChannel();
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, channel.size()
);
action.accept(buffer);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
try {
if (channel != null) channel.close();
if (file != null) file.close();
} catch (IOException e) {
// 记录日志
}
}
}
}
3. 线程池监控
java
复制
下载
public class MonitoredThreadPool {
private final ThreadPoolExecutor executor;
private final ScheduledExecutorService monitor;
public MonitoredThreadPool() {
this.executor = new ThreadPoolExecutor(
10, 100, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
// 监控线程池状态
this.monitor = Executors.newSingleThreadScheduledExecutor();
this.monitor.scheduleAtFixedRate(() -> {
System.out.println("Thread pool stats:");
System.out.println(" Active threads: " + executor.getActiveCount());
System.out.println(" Pool size: " + executor.getPoolSize());
System.out.println(" Queue size: " + executor.getQueue().size());
// 检查Native Memory使用
try {
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("pmap " + getPid());
// 解析输出...
} catch (IOException e) {
e.printStackTrace();
}
}, 0, 30, TimeUnit.SECONDS);
}
private String getPid() {
return ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
}
}
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc
需要全套面试笔记及答案
【点击此处即可/免费获取】
📊 监控告警配置
Prometheus监控指标
yaml
复制
下载
# NMT监控指标采集
- job_name: 'jvm_nmt'
static_configs:
- targets: ['localhost:12345']
metrics_path: /nmt/metrics
params:
pid: ['<java_pid>']
Grafana仪表板配置
json
复制
下载
{
"panels": [
{
"title": "Native Memory Usage",
"targets": [
{
"expr": "jvm_nmt_memory_reserved{region=\"Internal\"}",
"legendFormat": "Internal Memory"
},
{
"expr": "jvm_nmt_memory_committed{region=\"Thread\"}",
"legendFormat": "Thread Stack"
}
]
}
]
}
🚨 紧急处理方案
内存泄漏紧急处理
bash
复制
下载
# 1. 立即收集证据 jcmd <pid> VM.native_memory detail > emergency_nmt_$(date +%s).txt jstack <pid> > emergency_threads_$(date +%s).txt # 2. 尝试清理DirectBuffer jmap -histo:live <pid> | grep DirectByteBuffer # 3. 如果可能,触发Full GC jcmd <pid> GC.run # 4. 限制Native Memory使用 # 重启时添加参数 -XX:MaxDirectMemorySize=512m # 限制直接内存 -XX:ThreadStackSize=256k # 减小线程栈大小 -XX:MetaspaceSize=128m # 限制元空间
📝 最佳实践总结
预防措施
-
定期监控:生产环境开启NMT,定期收集数据
-
代码审查:重点审查DirectByteBuffer、MappedByteBuffer使用
-
资源管理:使用try-with-resources或模板方法
-
容量规划:合理设置JVM参数限制Native Memory
-
压力测试:模拟长时间运行,观察Native Memory增长
诊断要点
-
基线对比:使用baseline和diff功能
-
趋势分析:关注committed内存的持续增长
-
区域定位:确定是Thread、Internal还是Arena泄漏
-
结合工具:NMT+pmap+jstack综合分析
调优建议
bash
复制
下载
# 推荐配置 -XX:NativeMemoryTracking=summary # 生产环境开启 -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics # JVM退出时打印统计 -XX:MaxDirectMemorySize=1g # 限制直接内存 -XX:ThreadStackSize=512k # 优化线程栈大小 -XX:MetaspaceSize=256m # 设置合适的元空间 -XX:MaxMetaspaceSize=512m # 限制最大元空间
通过系统化的NMT监控和诊断,可以有效发现和解决堆外内存泄漏问题,保证Java应用的稳定运行。
更多推荐



所有评论(0)