这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 运行时数据区描述 JVM 内存的逻辑划分。堆只是其中一部分,进程 RSS 还包括 Metaspace、线程栈、Code Cache、GC 开销、Direct Memory 和 Native 库。理解这些区域,才能解释堆正常但容器 OOM、线程过多、类元数据溢出和 JIT 代码缓存满。
4.1 内存全景
JVM Process
|-- Heap
| |-- Young
| +-- Old / Regions
|-- Metaspace
|-- Thread Stacks
|-- Code Cache
|-- GC Structures
|-- Direct Memory
|-- Native Libraries
+-- OS overhead
粗略关系:
容器或 systemd 内存上限
>= 堆 + Metaspace + 线程栈 + Direct Memory + Code Cache + 其他 native
不同 GC 和 JDK 版本的 native 开销不同,必须通过实测和指标确认。
4.2 堆
查看:
jps -l
jstat -gc <pid> 1000
jstat -gcutil <pid> 1000
jcmd <pid> VM.flags
示例参数:
java -Xms2g -Xmx2g -XX:+UseG1GC -jar app.jar
分代视角:
Heap
|-- Eden
|-- Survivor 0
|-- Survivor 1
+-- Old
G1、ZGC、Shenandoah 将堆划分为 Region 或支持不同布局,逻辑分代视角不能完全等价于物理布局。
常见 OOM:
java.lang.OutOfMemoryError: Java heap space
原因:
- 堆过小;
- 内存泄漏;
- 大查询;
- 批量加载;
- 缓存无上限;
- 流量突增;
- GC 配置不匹配。
4.3 Metaspace
Metaspace 存储类元数据,位于堆外,默认可伸缩。
参数:
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
查看:
jstat -gcmetacapacity <pid>
jcmd <pid> VM.metaspace
增长来源:
- 加载类过多;
- 动态代理;
- 脚本引擎;
- 反射生成类;
- 热部署 ClassLoader 泄漏;
- Agent 或 AOP 增强。
OOM:
java.lang.OutOfMemoryError: Metaspace
分析:
jcmd <pid> GC.class_histogram
jcmd <pid> VM.classloader_stats
jcmd <pid> VM.classloaders
4.4 虚拟机栈
每个线程创建时分配栈空间。
参数:
-Xss512k
栈帧:
Stack Frame
|-- Local Variable Table
|-- Operand Stack
+-- Frame Data
StackOverflowError:
public class StackOverflowDemo {
public static void main(String[] args) {
recurse(0);
}
private static void recurse(int depth) {
recurse(depth + 1);
}
}
常见原因:
- 无终止递归;
- 递归深度过大;
- 框架调用链深;
- JSON 深层嵌套;
- 栈太小。
线程数过多:
线程数 * -Xss 大约等于线程栈总量
它不是唯一因素,但线程失控会同时消耗 CPU、内存和调度资源。
4.5 程序计数器
每个线程都有独立的 PC Register,记录当前字节码执行位置。线程切换后恢复执行依赖它。
特点:
- 线程私有;
- 占用很小;
- 执行 native 方法时值可能为 undefined;
- 通常不是排障重点。
4.6 本地方法栈
本地方法栈服务 native 方法。HotSpot 中通常与虚拟机栈实现合并,具体由虚拟机实现决定。
相关风险:
- JNI 泄漏;
- native 库崩溃;
- 堆外内存不足;
- UnsatisfiedLinkError;
- 压缩或加密库版本不匹配。
排查 native 问题时需要 core dump、native 栈和对应库符号。
4.7 直接内存
Direct Memory 常用于 NIO Buffer、网络框架、压缩和数据库驱动。
参数:
-XX:MaxDirectMemorySize=1g
示例:
ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024 * 1024);
System.out.println(buffer.capacity());
查看:
jcmd <pid> VM.flags
jcmd <pid> VM.native_memory summary
VM.native_memory 需要启用 NativeMemoryTracking:
java -XX:NativeMemoryTracking=summary -jar app.jar
OOM:
java.lang.OutOfMemoryError: Direct buffer memory
方向:
- Netty 池化配置;
- 直接内存上限;
- Buffer 释放;
- 容器总内存;
- 泄漏检测。
4.8 Code Cache
Code Cache 存储 JIT 编译后的本地代码。
参数:
-XX:ReservedCodeCacheSize=256m
查看:
jcmd <pid> Compiler.codecache
jstat -printcompilation <pid>
满了之后可能:
- 退回解释执行;
- 性能下降;
- 出现
CodeCache is full相关信息。
常见来源:
- 加载类过多;
- 动态代理;
- 大量生成代码;
- Code Cache 配置过小。
4.9 运行时内存查看
JCMD:
jcmd <pid> VM.uptime
jcmd <pid> GC.heap_info
jcmd <pid> VM.flags
jcmd <pid> VM.metaspace
jcmd <pid> VM.native_memory summary
JSTAT:
jstat -gc <pid> 1000
jstat -gcutil <pid> 1000
jstat -gccapacity <pid>
jstat -gcmetacapacity <pid>
操作系统视角:
ps -o pid,rss,vsz,cmd -p <pid>
cat /proc/<pid>/smaps_rollup
pmap -x <pid>
4.10 内存分配预算
示例:容器 limit 3Gi。
Heap 1.8G
Metaspace 256M
Thread stacks 300 * 1M = 300M
Direct memory 256M
Code cache 128M
GC/native 余量 200M 左右
建议:
- Java 17 / 21 使用
MaxRAMPercentage; - 预留堆外和系统内存;
- 限制线程数;
- 监控 RSS;
- 观察容器 OOMKilled;
- 不要把
-Xmx设为容器 limit。
示例:
java -XX:MaxRAMPercentage=65.0 -jar app.jar
4.11 生产排障
堆正常但容器被杀
kubectl describe pod <pod>
cat /sys/fs/cgroup/<path>/memory.events
jcmd <pid> VM.native_memory summary
方向:Direct Memory、线程栈、Metaspace、native 库、RSS 与 limit。
线程数持续增长
jstack <pid> | grep '^"' | wc -l
jcmd <pid> Thread.print | grep '^"' | wc -l
ps -T -p <pid> | wc -l
检查线程池无界队列、重复创建执行器和资源未关闭。
Metaspace 持续增长
jcmd <pid> GC.class_histogram | head -50
jcmd <pid> VM.classloader_stats
关注动态生成类和旧 ClassLoader。
本章小结
运行时数据区包括堆、Metaspace、虚拟机栈、本地方法栈、程序计数器,以及 Code Cache、Direct Memory 等 native 区域。-Xmx 只是堆上限,进程内存必须按完整预算设计。生产排障要同时看 JVM 内部指标和操作系统 RSS / cgroup。
思考题
- JVM 进程内存为什么大于
-Xmx? - Metaspace 增长常见原因有哪些?
- StackOverflowError 和 OutOfMemoryError 有什么区别?
- Direct Memory 常用于哪些组件?
- 容器 limit 4Gi 时,如何设置堆和堆外预算?