JVMNotes

第 04 章:运行时数据区

zjc 于 2026-01-04 发布

这是《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

原因:

  1. 堆过小;
  2. 内存泄漏;
  3. 大查询;
  4. 批量加载;
  5. 缓存无上限;
  6. 流量突增;
  7. GC 配置不匹配。

4.3 Metaspace

Metaspace 存储类元数据,位于堆外,默认可伸缩。

参数:

-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

查看:

jstat -gcmetacapacity <pid>
jcmd <pid> VM.metaspace

增长来源:

  1. 加载类过多;
  2. 动态代理;
  3. 脚本引擎;
  4. 反射生成类;
  5. 热部署 ClassLoader 泄漏;
  6. 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);
    }
}

常见原因:

  1. 无终止递归;
  2. 递归深度过大;
  3. 框架调用链深;
  4. JSON 深层嵌套;
  5. 栈太小。

线程数过多:

线程数 * -Xss 大约等于线程栈总量

它不是唯一因素,但线程失控会同时消耗 CPU、内存和调度资源。

4.5 程序计数器

每个线程都有独立的 PC Register,记录当前字节码执行位置。线程切换后恢复执行依赖它。

特点:

  1. 线程私有;
  2. 占用很小;
  3. 执行 native 方法时值可能为 undefined;
  4. 通常不是排障重点。

4.6 本地方法栈

本地方法栈服务 native 方法。HotSpot 中通常与虚拟机栈实现合并,具体由虚拟机实现决定。

相关风险:

  1. JNI 泄漏;
  2. native 库崩溃;
  3. 堆外内存不足;
  4. UnsatisfiedLinkError;
  5. 压缩或加密库版本不匹配。

排查 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

方向:

  1. Netty 池化配置;
  2. 直接内存上限;
  3. Buffer 释放;
  4. 容器总内存;
  5. 泄漏检测。

4.8 Code Cache

Code Cache 存储 JIT 编译后的本地代码。

参数:

-XX:ReservedCodeCacheSize=256m

查看:

jcmd <pid> Compiler.codecache
jstat -printcompilation <pid>

满了之后可能:

  1. 退回解释执行;
  2. 性能下降;
  3. 出现 CodeCache is full 相关信息。

常见来源:

  1. 加载类过多;
  2. 动态代理;
  3. 大量生成代码;
  4. 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 左右

建议:

  1. Java 17 / 21 使用 MaxRAMPercentage
  2. 预留堆外和系统内存;
  3. 限制线程数;
  4. 监控 RSS;
  5. 观察容器 OOMKilled;
  6. 不要把 -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。

思考题

  1. JVM 进程内存为什么大于 -Xmx
  2. Metaspace 增长常见原因有哪些?
  3. StackOverflowError 和 OutOfMemoryError 有什么区别?
  4. Direct Memory 常用于哪些组件?
  5. 容器 limit 4Gi 时,如何设置堆和堆外预算?