1. Native Memory Tracking 概述
Native Memory Tracking (NMT) 是 HotSpot JVM 提供的一个用于追踪 JVM 内部原生内存使用情况的工具。它能够帮助我们深入了解 JVM 运行时各个子系统对原生内存的分配情况,包括 Java 堆、类元数据、线程、代码缓存等关键区域的内存使用细节。
NMT 通过 JVM 参数-XX:NativeMemoryTracking=[off|summary|detail]开启,其中:
off:关闭 NMT(默认值)summary:仅追踪内存使用的总量detail:追踪每个子系统的内存使用情况
启用 NMT 后,我们可以通过jcmd <pid> VM.native_memory命令获取内存使用报告。NMT 报告会按照内存类型分类展示,包括 reserved(保留)和 committed(已提交)的内存大小。
注意:NMT 本身会引入约 5-10% 的性能开销,因此在生产环境中应谨慎使用。建议仅在诊断内存问题时开启,并在问题解决后关闭。
2. Java Heap 内存分析
2.1 Java Heap 的基本结构
Java Heap 是 JVM 内存中最重要的部分,用于存储对象实例。在 NMT 报告中,Java Heap 通常占据最大的内存份额。一个典型的 Java Heap 内存报告如下:
[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffff93ea36d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 ...... [0x00000000c0000000 - 0x0000000100000000] committed 1048576KB from [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c这段报告显示:
- 虚拟地址范围:0x00000000c0000000 - 0x0000000100000000
- 保留内存:1048576KB (1GB)
- 已提交内存:1048576KB (1GB)
2.2 不同垃圾收集器的 Heap 表现
使用不同垃圾收集器时,Java Heap 的布局会有所不同。例如,使用 CMS 收集器时,NMT 报告会显示分代信息:
[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffffa5cc76d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 [0x0000ffffa5c8bf68] Universe::reserve_heap(unsigned long, unsigned long)+0x2d0 [0x0000ffffa570fa10] GenCollectedHeap::allocate(unsigned long, unsigned long*, int*, ReservedSpace*)+0x160 [0x0000ffffa5711fdc] GenCollectedHeap::initialize()+0x104 [0x00000000d5550000 - 0x0000000100000000] committed 699072KB from [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffffa572a450] CardGeneration::CardGeneration(ReservedSpace, unsigned long, int, GenRemSet*)+0xb8 [0x0000ffffa55dc85c] ConcurrentMarkSweepGeneration::ConcurrentMarkSweepGeneration(ReservedSpace, unsigned long, int, CardTableRS*, bool, FreeBlockDictionary<FreeChunk>::DictionaryChoice)+0x54 [0x00000000c0000000 - 0x00000000d5550000] committed 349504KB from [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffffa5729fe0] Generation::Generation(ReservedSpace, unsigned long, int)+0x98这里可以清晰地看到:
- 新生代 (DefNewGeneration):0x00000000c0000000 - 0x00000000d5550000 (约 349MB)
- 老年代 (ConcurrentMarkSweepGeneration):0x00000000d5550000 - 0x0000000100000000 (约 699MB)
2.3 Java Heap 调优建议
初始和最大堆大小:使用
-Xms和-Xmx(或-XX:InitialHeapSize和-XX:MaxHeapSize)设置相同的值,避免运行时动态调整带来的性能开销。预分配物理内存:使用
-XX:+AlwaysPreTouch参数让 JVM 在启动时就分配所有物理内存,减少运行时停顿,但会增加启动时间。内存释放:考虑使用
-XX:+UseAdaptiveSizePolicyWithSystemGC和-XX:MaxHeapFreeRatio等参数控制堆内存的释放行为。
3. Class 元数据内存分析
3.1 Class 内存的演变
Class 内存主要用于存储类元数据(metadata)。在 JVM 的发展过程中,这部分内存的实现经历了重大变化:
- JDK7 及之前:永久代(PermGen)存储类元数据和字符串常量池
- JDK8 及之后:元空间(Metaspace)取代永久代,字符串常量池移至堆中
NMT 中的 Class 内存报告如下:
Class (reserved=1056899KB, committed=4995KB) (classes #442) (malloc=131KB #259) (mmap: reserved=1056768KB, committed=4864KB)3.2 压缩类指针的影响
当启用-XX:+UseCompressedClassPointers(默认开启)时,JVM 会为类指针分配一个连续的压缩类空间(Compressed Class Space)。这会显著影响 Class 内存的使用:
[0x0000000100000000 - 0x0000000140000000] reserved 1048576KB for Class from [0x0000ffff93ea28d0] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x90 [0x0000ffff93c16694] Metaspace::allocate_metaspace_compressed_klass_ptrs(char*, unsigned char*)+0x42c压缩类空间的大小由-XX:CompressedClassSpaceSize控制,默认值为 1GB。即使设置了-XX:MaxMetaspaceSize,压缩类空间也可能保留更多内存,这是 HotSpot 实现的一个已知行为。
3.3 Metaspace 调优建议
限制元空间大小:使用
-XX:MaxMetaspaceSize设置上限,防止元空间无限增长。监控类加载:大量动态类加载可能导致元空间膨胀,需要监控类加载数量。
JDK10+ 的改进:新版 JDK 提供了更详细的 Class 内存报告,区分了 klass 和非 klass 内存:
Class (reserved=1056882KB, committed=1053042KB) (classes #483) (malloc=114KB #629) (mmap: reserved=1056768KB, committed=1052928KB) ( Metadata: ) ( reserved=8192KB, committed=4352KB) ( used=3492KB) ( free=860KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=512KB) ( used=326KB) ( free=186KB) ( waste=0KB =0.00%)4. Thread 内存分析
4.1 线程内存组成
NMT 报告的线程内存包括三部分:
Thread (reserved=258568KB, committed=258568KB) (thread #127) (stack: reserved=258048KB, committed=258048KB) (malloc=390KB #711) (arena=130KB #234)- stack:线程栈内存
- malloc:线程对象分配的内存
- arena:线程本地分配缓冲区(TLAB)外的内存
4.2 线程栈大小
线程栈大小因平台而异:
- x86 平台:默认 1MB (
-Xss1M) - AArch64 平台:默认 2MB (
-Xss2M)
可以通过-Xss或-XX:ThreadStackSize调整栈大小,但需注意:
- 栈大小过小可能导致
StackOverflowError - 栈大小过大会限制可创建的线程数量
4.3 线程内存优化建议
合理设置栈大小:根据应用需求调整
-Xss参数,平衡内存使用和安全性。控制线程数量:使用线程池限制最大线程数,避免创建过多线程导致内存耗尽。
监控线程创建:使用 NMT 或
jstack监控线程数量和栈内存使用情况。
5. Code Cache 内存分析
5.1 Code Cache 的作用
Code Cache 用于存储 JVM 生成的本地代码,包括:
- JIT 编译的方法代码
- 运行时生成的适配器代码
- 方法解释器
NMT 报告示例:
Code (reserved=266273KB, committed=4001KB) (malloc=33KB #309) (mmap: reserved=266240KB, committed=3968KB)5.2 Code Cache 调优参数
- 初始大小:
-XX:InitialCodeCacheSize(默认值平台相关) - 保留大小:
-XX:ReservedCodeCacheSize(默认 240MB) - 扩展大小:
-XX:CodeCacheExpansionSize(默认 32KB-64KB)
5.3 Code Cache 监控与优化
监控工具:
-XX:+PrintCodeCache:JVM 关闭时输出使用情况jcmd <pid> Compiler.codecache(JDK9+)
优化建议:
- 对于长时间运行的服务,适当增加
-XX:ReservedCodeCacheSize - 使用
-XX:+UseCodeCacheFlushing允许在 Code Cache 满时回收无用代码 - 考虑使用
-XX:+TieredCompilation分层编译策略
- 对于长时间运行的服务,适当增加
6. GC 内存分析
6.1 GC 内存组成
垃圾收集器使用的辅助数据结构会占用额外内存,包括:
- 卡表(Card Table)
- 记忆集(Remembered Set)
- 标记位图(Marking Bitmaps)
- 标记栈(Marking Stack)
NMT 报告示例:
GC (reserved=164403KB, committed=164403KB) (malloc=92723KB #6540) (mmap: reserved=71680KB, committed=71680KB)6.2 不同收集器的内存开销
不同垃圾收集器的内存开销差异显著:
Serial GC:内存开销最小
GC (reserved=27319KB, committed=27319KB) (malloc=7KB #79) (mmap: reserved=27312KB, committed=27312KB)CMS:中等内存开销
GC (reserved=167318KB, committed=167318KB) (malloc=140006KB #373) (mmap: reserved=27312KB, committed=27312KB)G1:内存开销最大(特别是大堆时)
GC (reserved=446473KB, committed=446473KB) (malloc=102409KB #12680) (mmap: reserved=344064KB, committed=344064KB)
6.3 GC 内存调优建议
根据应用特点选择收集器:
- 内存敏感应用:考虑 Serial GC 或 CMS
- 大堆应用:权衡 G1 的停顿时间和内存开销
监控 GC 内存使用:随着堆大小增加,GC 内存开销可能非线性增长
区域大小设置:对于 G1,合理设置
-XX:G1HeapRegionSize可以影响记忆集大小
7. 其他内存区域
除了上述主要内存区域,NMT 还能追踪以下内存使用:
7.1 Symbol 内存
存储符号表信息,包括字符串常量池(JDK8 后从 PermGen 移出):
Symbol (reserved=1525KB, committed=1525KB) (malloc=1041KB #3176) (arena=484KB #1)7.2 Internal 内存
JVM 内部使用的内存,不属于其他任何类别:
Internal (reserved=1055KB, committed=1055KB) (malloc=1055KB #1357)7.3 NMT 自身内存
NMT 跟踪功能自身使用的内存:
Native Memory Tracking (reserved=85KB, committed=85KB) (malloc=5KB #53) (tracking overhead=80KB)8. NMT 使用实践与注意事项
8.1 NMT 启用步骤
- 启动 JVM 时添加参数:
-XX:NativeMemoryTracking=detail - 获取基线数据:
jcmd <pid> VM.native_memory baseline - 获取差异报告:
jcmd <pid> VM.native_memory summary.diff或detail.diff
8.2 NMT 限制
无法追踪的内存:
- 第三方本地库分配的内存
- JNI 调用分配的内存
- 某些操作系统保留的内存区域
性能影响:NMT 会带来 5-10% 的性能开销,生产环境慎用
平台差异:不同操作系统和 JVM 实现可能报告略有差异
8.3 内存问题排查流程
- 使用
top或ps确认进程内存使用异常 - 使用 NMT 确定 JVM 内部内存分配情况
- 结合
pmap或vmmap分析进程内存映射 - 使用
jmap分析堆内存使用详情 - 使用
jstack分析线程状态和数量
9. 实际案例分析
9.1 案例一:元空间溢出
现象:应用频繁部署后出现OutOfMemoryError: Metaspace
分析步骤:
- NMT 报告显示 Class 内存持续增长
- 使用
-XX:MaxMetaspaceSize限制大小 - 使用
-XX:TraceClassLoading和-XX:TraceClassUnloading监控类加载/卸载 - 发现自定义类加载器未正确释放
解决方案:
- 修复类加载器泄漏
- 适当增加
-XX:MaxMetaspaceSize - 添加元空间监控告警
9.2 案例二:线程栈内存耗尽
现象:高并发时出现OutOfMemoryError: unable to create native thread
分析步骤:
- NMT 显示大量线程栈内存占用
ulimit -a确认系统线程数限制- 计算总内存 / 单个线程栈大小 ≈ 实际最大线程数
解决方案:
- 调整
-Xss减小单个线程栈大小 - 优化线程池配置,限制最大线程数
- 考虑垂直扩展(增加内存)或水平扩展(增加实例)
9.3 案例三:G1 记忆集内存过大
现象:大堆应用使用 G1 收集器,GC 内存占用超过预期
分析步骤:
- NMT 显示 GC 内存随堆大小非线性增长
- 确认
-XX:+UseG1GC和堆大小设置 - 分析 G1 日志确认 region 大小和记忆集情况
解决方案:
- 调整
-XX:G1HeapRegionSize(默认根据堆大小自动计算) - 评估是否可以使用 CMS 替代
- 适当增加物理内存配置
10. 高级话题与未来方向
10.1 NMT 与容器化环境
在容器化环境中,NMT 报告的内存可能与 cgroup 限制产生冲突:
- 内存限制:容器内存限制可能小于 JVM 预期的可用内存
- NMT 调整:需要结合
-XX:MaxRAMPercentage等参数正确配置 - 监控集成:将 NMT 数据导出到 Prometheus 等监控系统
10.2 JDK 新版本改进
较新 JDK 版本对 NMT 的增强:
- 更细粒度的分类:如 JDK10+ 的 Class 内存细分
- 低开销模式:开发中的低开销 NMT 实现
- 更好的容器支持:自动感知 cgroup 限制
10.3 替代工具与互补技术
- JDK Flight Recorder:低开销的内存事件记录
- Async Profiler:原生内存分配分析
- PMAP 分析:操作系统层面的内存映射检查
在实际应用中,我经常发现开发团队过度关注堆内存而忽视原生内存的使用情况。通过 NMT 的分析,我们能够更全面地理解 JVM 的内存行为,特别是在处理内存泄漏或优化大型应用时。一个实用的技巧是定期收集 NMT 的基线数据,特别是在应用启动后和稳定运行阶段,这样可以在出现内存增长时快速定位问题来源。