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        # 限制元空间

📝 最佳实践总结

预防措施

  1. 定期监控:生产环境开启NMT,定期收集数据

  2. 代码审查:重点审查DirectByteBuffer、MappedByteBuffer使用

  3. 资源管理:使用try-with-resources或模板方法

  4. 容量规划:合理设置JVM参数限制Native Memory

  5. 压力测试:模拟长时间运行,观察Native Memory增长

诊断要点

  1. 基线对比:使用baseline和diff功能

  2. 趋势分析:关注committed内存的持续增长

  3. 区域定位:确定是Thread、Internal还是Arena泄漏

  4. 结合工具: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应用的稳定运行。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐