JVM内存布局
JVM 进程总内存空间 (OS Physical RAM)
├── 1. Java 堆内存区 (Java Heap) 【线程共享 | GC核心管理】
│ ├── 年轻代 (Young Generation)
│ │ ├── Eden 区
│ │ ├── Survivor 0 (S0 / From) 区
│ │ └── Survivor 1 (S1 / To) 区
│ ├── 老年代 (Old Generation)
│ └── 字符串常量池 (String Table) 【JDK 7+ 迁入堆内】
│
└── 2. 本地内存 / 堆外内存 (Native Memory / Off-Heap)
├── 2.1 类元数据与代码区 (Class & Code Space) 【线程共享】
│ ├── 元空间 (Metaspace) 【JDK 8+ 存储 Class 结构、字段、方法字节码、运行时常量池】
│ └── JIT 代码缓存 (Code Cache) 【存储 C1/C2 编译器生成的机器原生指令】
│
├── 2.2 JVM 本地堆区 (JVM C-Heap) 【线程共享 | JVM C++ 层面分配】
│ ├── 对象监视器 (ObjectMonitor) 【重量级锁 C++ 结构体】
│ ├── 符号表 (Symbol Table) & 内部元数据
│ └── Unsafe / Foreign API (FFM) 动态内存申请区
│
├── 2.3 线程私有内存区 (Per-Thread Native Space) 【线程私有 | 随线程生命周期创建/销毁】
│ ├── Java 虚拟机栈 (JVM Stack)
│ │ └── 栈帧 (Stack Frame)
│ │ ├── 局部变量表 (Local Variable Table)
│ │ ├── 操作数栈 (Operand Stack)
│ │ ├── 动态链接 (Dynamic Linking)
│ │ └── 方法出口/返回地址 (Return Value)
│ ├── 本地方法栈 (Native Method Stack) 【服务于 JNI / Native C/C++ 方法】
│ ├── 程序计数器 (Program Counter Register) 【记录当前执行指令地址,无 OOM】
│ └── 线程控制开销 【OS 线程控制块 (TCB) & 保护页 (Guard Pages)】
│
├── 2.4 GC 辅助数据区 (GC Memory Overhead) 【线程共享】
│ ├── 卡表 (Card Table)
│ ├── 记忆集 (Remembered Sets / RSet - G1 GC)
│ └── 标记位图 (Mark Bitmaps - ZGC/Shenandoah)
│
└── 2.5 堆外 Direct / I/O 缓冲区 (Direct & Mapped Memory) 【线程共享】
├── 直接内存 (Direct Memory / DirectByteBuffer) 【Netty/NIO 高性能传输】
└── 内存映射文件 (Memory-Mapped Files / MappedByteBuffer / MMAP)在日常开发与运维中,当我们用 top 或 ps 命令看到 Java 进程的 RES(物理常驻内存) 远超 -Xmx 设置的堆大小时,往往是因为堆外 Native 内存被大量占用。
为了实现对 JVM 进程内存的全方位排查,本文将按照 JVM 全景内存树 的层次结构,逐一列出各内存区域的本地观测命令与排查工具。
一、 最强排查总纲: Native Memory Tracking (NMT)
在观测各个细分区域前,强烈建议 在应用启动时开启 HotSpot 的 NMT 功能。它是唯一能从 JVM 内部将 Native 内存分类统计的工具。
1. 开启参数
# 生产环境推荐 summary(性能损耗 < 1%)
java -XX:NativeMemoryTracking=summary -Xms4g -Xmx4g -jar app.jar
# 排查疑难杂症时推荐 detail(记录 CallSite 栈,开销略高)
java -XX:NativeMemoryTracking=detail -jar app.jar
2. 查看全景内存报告
jcmd <PID> VM.native_memory summary
3. 基线对比(排查堆外内存泄漏利器)
# 1. 刚启动或正常运行时建立基线 (Baseline)
jcmd <PID> VM.native_memory baseline
# 2. 运行一段时间或内存飙升后进行对比 (Diff)
jcmd <PID> VM.native_memory summary.diff
(结果中会清晰标注出哪个 Native 区域增加了 +KB/MB)
二、 分区域本地观测指令与工具速查
JVM 进程总内存空间 (OS Physical RAM)
├── 1. Java 堆内存区 (Java Heap)
├── 2.1 类元数据与代码区 (Metaspace & Code Cache)
├── 2.2 JVM 本地堆区 (C-Heap & ObjectMonitor 监视器)
├── 2.3 线程私有内存区 (JVM Stacks & TCB)
├── 2.4 GC 辅助数据区 (Card Table & RSet)
└── 2.5 堆外 Direct / I/O 缓冲区 (Direct Memory & MMAP)
1. Java 堆内存区 (Java Heap)
包含:年轻代(Eden、S0、S1)、老年代、字符串常量池(String Table)。
命令行实战(
jstat实时监控):# 每 1000ms 打印一次堆各区域使用率和 GC 统计 jstat -gcutil <PID> 1000 # 查看堆的具体容量与容量上限(KB) jstat -gccapacity <PID>查看堆内存摘要(
jmap/jcmd):# 查看堆内存配置与各代使用情况 jcmd <PID> GC.heap_info # 传统命令(注意:jmap -heap 在某些 JDK 版本会导致进程短暂 Pause) jmap -heap <PID>对象直方图(查看哪些对象最占内存):
# 打印存活对象的数量与字节大小(前 20 项) jcmd <PID> GC.class_histogram | head -n 20Dump 堆内存离线分析:
jcmd <PID> GC.heap_dump /tmp/heap.hprof # 使用 Eclipse MAT (Memory Analyzer Tool) 或 IDEA 分析 .hprof 文件
2. 类元数据与代码区 (Metaspace & Code Cache)
包含:元空间(Metaspace,存储 Klass 结构/方法字节码)、JIT 代码缓存(Code Cache)。
NMT 指标项:对应报告中的
Class和Code。查看元空间详细统计 (
jcmd):# 查看 Metaspace 的占用、块大小、已加载类数量 jcmd <PID> VM.metaspace查看类加载数量与动态追踪:
# 统计已加载类数量与占用 jcmd <PID> GC.class_stats # 实时打印类的加载与卸载日志(启动参数) java -Xlog:class+load=info,class+unload=info -jar app.jar查看 Code Cache 占用:
# 查看 JIT 编译生成的机器码缓存占用 jcmd <PID> Compiler.codecache
3. JVM 本地堆区 (JVM C-Heap & 监视器)
包含:重量级锁结构体(ObjectMonitor)、符号表(Symbol Table)、Unsafe/FFM 动态分配内存。
重点:
ObjectMonitor位于 C-Heap 中,JVM 并没有直接暴露 "Monitor 数量 * 字节数" 的独立命令,需结合 NMT 的Internal区域与线程锁日志综合判定。
NMT 指标项:对应报告中的
Internal(内含 Monitor)和Symbol。检测锁竞争与重量级锁( Monitor ):
# 1. 查看线程 Dump,排查被阻塞在 Object.wait() 或 synchronized 上的线程数量 jcmd <PID> Thread.print | grep -i "waiting to lock" -A 3 # 2. 统计死锁和锁持有的总体情况 jcmd <PID> Thread.print | grep -i "locked <0x"观测符号表 (Symbol Table):
# 查看符号表与字符串表的统计信息(占用大量 Internal/Symbol 内存时排查) jcmd <PID> VM.stringtable -verbose jcmd <PID> VM.symboltable -verbose
4. 线程私有内存区 (Per-Thread Native Space)
包含:Java 虚拟机栈(-Xss)、本地方法栈、程序计数器、OS 线程控制块 (TCB) & 保护页。
NMT 指标项:对应报告中的
Thread。统计进程中的操作系统线程数:
# Linux 下统计该 JVM 进程创建的所有轻量级进程/线程数 pstree -p <PID> | wc -l # 或 ls /proc/<PID>/task | wc -l推算线程栈 Native 总内存:
5. GC 辅助数据区 (GC Memory Overhead)
包含:卡表(Card Table)、记忆集(Remembered Sets / RSet,如 G1)、标记位图(ZGC/Shenandoah)。
NMT 指标项:对应报告中的
GC。观测 G1 GC 的 RSet 内存开销:
# 启动参数开启 GC 详细统计日志 java -Xlog:gc+remset*=trace -jar app.jar(注:在大堆场景下,G1 的 RSet 可能会在 NMT 的
GC项中占用数个 GB 的 Native 内存。)
6. 堆外 Direct / I/O 缓冲区 (Direct Memory & MMAP)
包含:通过 ByteBuffer.allocateDirect() 分配的直接内存、MappedByteBuffer 映射文件。
NMT 指标项:对应报告中的
Other或Unknown。使用 JMX (JConsole / VisualVM /
jcmd) 实时获取:通过 JMX 节点
java.nio:type=BufferPool,name=direct可获取精准数值:# 使用 jcmd 读取 Direct Buffer Pool 指标 jcmd <PID> ManagementAgent.start_local # 配合 JConsole 或 mbean 命令行工具查看: # - Count (缓冲区数量) # - MemoryUsed (实际已使用字节数) # - TotalCapacity (申请的总容量)使用
pmap排查内存映射文件 (MMAP):# 打印内存地址映射表,过滤出大于 10MB 的匿名内存块或 mapped file pmap -x <PID> | awk '$3 > 10240 {print $0}' | sort -n -k3
三、 操作系统(OS)视角工具
当你想验证 JVM 各区域内存相加是否等于系统物理占用时,请使用以下 OS 级命令:
1. top / ps:查看物理内存 (RES) 与虚拟内存 (VIRT)
# 查看指定进程的内存指标
top -p <PID>
# 输出字段说明:
# VIRT: 虚拟内存(预订保留的内存,不一定占用真实物理内存)
# RES : 常驻物理内存(JVM 实际吃掉的操作系统物理内存)
2. /proc/<PID>/smaps:精确分析物理内存占用
# 查看该 Java 进程各个内存段的真实 Pss/Rss 物理内存分布
cat /proc/<PID>/smaps_rollup
# 查找哪块 Native 内存段占用了最大物理内存
cat /proc/<PID>/smaps | grep -E "(Rss|Pss|Size):" | head -n 30
四、 总结:常用命令一览表
提示:建议将
jcmd <PID> VM.native_memory baseline与summary.diff写入日常运维脚本,在遇到堆外内存泄漏时能够第一时间抓取内存增量快照。