原理与实践指南)
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, FreeBlockDictionaryFreeChunk::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 (reserved1056899KB, committed4995KB) (classes #442) (malloc131KB #259) (mmap: reserved1056768KB, committed4864KB)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 (reserved1056882KB, committed1053042KB) (classes #483) (malloc114KB #629) (mmap: reserved1056768KB, committed1052928KB) ( Metadata: ) ( reserved8192KB, committed4352KB) ( used3492KB) ( free860KB) ( waste0KB 0.00%) ( Class space:) ( reserved1048576KB, committed512KB) ( used326KB) ( free186KB) ( waste0KB 0.00%)4. Thread 内存分析4.1 线程内存组成NMT 报告的线程内存包括三部分Thread (reserved258568KB, committed258568KB) (thread #127) (stack: reserved258048KB, committed258048KB) (malloc390KB #711) (arena130KB #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 (reserved266273KB, committed4001KB) (malloc33KB #309) (mmap: reserved266240KB, committed3968KB)5.2 Code Cache 调优参数初始大小-XX:InitialCodeCacheSize默认值平台相关保留大小-XX:ReservedCodeCacheSize默认 240MB扩展大小-XX:CodeCacheExpansionSize默认 32KB-64KB5.3 Code Cache 监控与优化监控工具-XX:PrintCodeCacheJVM 关闭时输出使用情况jcmd pid Compiler.codecacheJDK9优化建议对于长时间运行的服务适当增加-XX:ReservedCodeCacheSize使用-XX:UseCodeCacheFlushing允许在 Code Cache 满时回收无用代码考虑使用-XX:TieredCompilation分层编译策略6. GC 内存分析6.1 GC 内存组成垃圾收集器使用的辅助数据结构会占用额外内存包括卡表Card Table记忆集Remembered Set标记位图Marking Bitmaps标记栈Marking StackNMT 报告示例GC (reserved164403KB, committed164403KB) (malloc92723KB #6540) (mmap: reserved71680KB, committed71680KB)6.2 不同收集器的内存开销不同垃圾收集器的内存开销差异显著Serial GC内存开销最小GC (reserved27319KB, committed27319KB) (malloc7KB #79) (mmap: reserved27312KB, committed27312KB)CMS中等内存开销GC (reserved167318KB, committed167318KB) (malloc140006KB #373) (mmap: reserved27312KB, committed27312KB)G1内存开销最大特别是大堆时GC (reserved446473KB, committed446473KB) (malloc102409KB #12680) (mmap: reserved344064KB, committed344064KB)6.3 GC 内存调优建议根据应用特点选择收集器内存敏感应用考虑 Serial GC 或 CMS大堆应用权衡 G1 的停顿时间和内存开销监控 GC 内存使用随着堆大小增加GC 内存开销可能非线性增长区域大小设置对于 G1合理设置-XX:G1HeapRegionSize可以影响记忆集大小7. 其他内存区域除了上述主要内存区域NMT 还能追踪以下内存使用7.1 Symbol 内存存储符号表信息包括字符串常量池JDK8 后从 PermGen 移出Symbol (reserved1525KB, committed1525KB) (malloc1041KB #3176) (arena484KB #1)7.2 Internal 内存JVM 内部使用的内存不属于其他任何类别Internal (reserved1055KB, committed1055KB) (malloc1055KB #1357)7.3 NMT 自身内存NMT 跟踪功能自身使用的内存Native Memory Tracking (reserved85KB, committed85KB) (malloc5KB #53) (tracking overhead80KB)8. NMT 使用实践与注意事项8.1 NMT 启用步骤启动 JVM 时添加参数-XX:NativeMemoryTrackingdetail获取基线数据jcmd pid VM.native_memory baseline获取差异报告jcmd pid VM.native_memory summary.diff或detail.diff8.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 的基线数据特别是在应用启动后和稳定运行阶段这样可以在出现内存增长时快速定位问题来源。