【JVM深度解析】第03篇:运行时数据区深度剖析

张开发
2026/4/17 0:24:06 15 分钟阅读

分享文章

【JVM深度解析】第03篇:运行时数据区深度剖析
摘要运行时数据区Runtime Data Areas是 JVM 的内存管理核心决定了 Java 程序运行时的内存使用方式。本文深入剖析 JVM 规范定义的五大内存区域——程序计数器、虚拟机栈、本地方法栈、堆、方法区/元空间——的精确边界、生命周期、存储内容与溢出条件。重点讲解堆内存的对象分配机制指针碰撞、空闲列表、TLAB、对象在内存中的布局对象头/实例数据/填充以及方法区从 PermGen 到 Metaspace 的演进原因。通过多个 OOM 场景分析帮助开发者精准定位OutOfMemoryError的根源指导生产环境内存参数配置。引言OOM 了但不知道是哪块内存满了调了半天-Xmx但问题出在元空间打了 heap dump 却分析不出泄漏点……这些困境的根源是对 JVM 内存区域缺乏精准认知。JVM 把内存划分为功能各异的区域每个区域有自己的数据类型、生命周期、溢出条件和配置参数。搞清楚这张内存地图你才能看到 OOM 信息立刻知道是哪个区域出了问题正确配置各区域的大小参数理解 GC 为什么只管堆而不管栈和方法区一、运行时数据区总览1.1 五大区域与线程关系JVM 内存区域按线程私有和线程共享划分╔══════════════════════════════════════════════════════════════════╗ ║ JVM 运行时数据区 ║ ║ ║ ║ ┌──────────────────────────── 线程共享区域 ──────────────────────┐ ║ ║ │ │ ║ ║ │ ┌───────────────────────────────────────────────────────┐ │ ║ ║ │ │ 堆 (Heap) │ │ ║ ║ │ │ Young GenEden S0 S1 | Old Gen │ │ ║ ║ │ │ 所有对象实例、数组均分配于此 │ │ ║ ║ │ └───────────────────────────────────────────────────────┘ │ ║ ║ │ │ ║ ║ │ ┌───────────────────────────────────────────────────────┐ │ ║ ║ │ │ 方法区 / 元空间 (Method Area / Metaspace) │ │ ║ ║ │ │ 类信息 | 常量池 | 静态变量 | 即时编译代码 │ │ ║ ║ │ └───────────────────────────────────────────────────────┘ │ ║ ║ └───────────────────────────────────────────────────────────────┘ ║ ║ ║ ║ ┌──────────────────────────── 线程私有区域 ──────────────────────┐ ║ ║ │ │ ║ ║ │ Thread-1 Thread-2 Thread-3 Thread-N │ ║ ║ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐ │ ║ ║ │ │程序计数│ │程序计数│ │程序计数│ │程序计数│ │ ║ ║ │ │器(PC) │ │器(PC) │ │器(PC) │ │器(PC) │ │ ║ ║ │ ├───────┤ ├───────┤ ├───────┤ ├───────┤ │ ║ ║ │ │虚拟机栈│ │虚拟机栈│ │虚拟机栈│ │虚拟机栈│ │ ║ ║ │ │(Stack)│ │(Stack)│ │(Stack)│ │(Stack)│ │ ║ ║ │ ├───────┤ ├───────┤ ├───────┤ ├───────┤ │ ║ ║ │ │本地方法│ │本地方法│ │本地方法│ │本地方法│ │ ║ ║ │ │栈 │ │栈 │ │栈 │ │栈 │ │ ║ ║ │ └───────┘ └───────┘ └───────┘ └───────┘ │ ║ ║ └───────────────────────────────────────────────────────────────┘ ║ ╚══════════════════════════════════════════════════════════════════╝区域线程归属生命周期可能的 OOM程序计数器线程私有随线程生灭无唯一不会OOM虚拟机栈线程私有随线程生灭StackOverflowError / OOM本地方法栈线程私有随线程生灭StackOverflowError / OOM堆线程共享JVM 启动到关闭OOM: Java heap space方法区/元空间线程共享JVM 启动到关闭OOM: Metaspace二、程序计数器PC Register2.1 存储什么程序计数器记录当前线程正在执行的字节码指令的地址偏移量字节码文件部分 0: aload_0 ← PC 0 1: invokespecial #1 ← PC 1执行中 4: return ← PC 4 程序计数器的值 1当前指令偏移量执行 Java 方法PC 记录字节码指令地址执行 Native 方法PC 值为UndefinedNative 代码在 JVM 外部执行2.2 为什么每个线程需要独立的 PC多线程并发执行时CPU 需要在线程间切换。线程恢复执行时必须知道上次执行到哪条指令——这正是 PC 的作用。时间轴 t0: Thread-A 执行PC_A 42 t1: 上下文切换 → Thread-B 执行PC_B 17 t2: 上下文切换 → Thread-A 恢复从 PC_A 42 继续执行程序计数器是线程上下文切换的关键数据必须线程私有。三、虚拟机栈JVM Stack3.1 栈帧结构精解每调用一个方法就会在当前线程的虚拟机栈上压入一个栈帧Stack Frame虚拟机栈Thread-A ┌──────────────────────────────────────────────────┐ │ 栈帧compute(int x) │ ← 栈顶当前执行 │ ┌────────────────────────────────────────────┐ │ │ │ 局部变量表 (Local Variable Table) │ │ │ │ slot[0]: this实例方法的隐式第一个参数 │ │ │ │ slot[1]: int x 5 │ │ │ │ slot[2]: int result局部变量 │ │ │ ├────────────────────────────────────────────┤ │ │ │ 操作数栈 (Operand Stack) │ │ │ │ 深度由编译器在编译期确定 │ │ │ │ ┌─────┐ │ │ │ │ │ 5 │ ← 栈顶 │ │ │ │ └─────┘ │ │ │ ├────────────────────────────────────────────┤ │ │ │ 动态链接 (Dynamic Linking) │ │ │ │ 指向运行时常量池中本方法的符号引用 │ │ │ ├────────────────────────────────────────────┤ │ │ │ 方法返回地址 (Return Address) │ │ │ │ 方法退出后返回到调用者的下一条指令地址 │ │ │ └────────────────────────────────────────────┘ │ ├──────────────────────────────────────────────────┤ │ 栈帧process() │ │ ... │ ├──────────────────────────────────────────────────┤ │ 栈帧main() │ ← 栈底 └──────────────────────────────────────────────────┘3.2 局部变量表详解局部变量表以**变量槽Slot**为最小单位boolean、byte、char、short、int、float、引用reference、returnAddress占 1 个 Slotlong、double占 2 个 Slot高位在低地址实例方法的this隐式占据slot[0]Slot 可以复用已出作用域的变量的 Slot 可以被后续变量复用节省空间publicvoidexampleMethod(inta,longb,Stringc){// slot[0] this// slot[1] a (int, 1 slot)// slot[2-3] b (long, 2 slots)// slot[4] c (reference, 1 slot)if(true){inttemp10;// slot[5]出作用域后 slot[5] 可被复用}doubled3.14;// slot[5-6]复用了 slot[5]}3.3 栈溢出与 OOM# 栈深度超过限制-Xss 控制每个线程的栈大小java-Xss256kMyApp# 256KB/线程默认通常为 512KB 或 1MBStackOverflowError触发条件// 无限递归栈帧不断增长publicintinfiniteRecursion(){return1infiniteRecursion();// 栈深度超过 -Xss 限制}// Exception: java.lang.StackOverflowErrorOutOfMemoryError栈内存耗尽触发条件// 创建大量线程每个线程的栈都要占内存// 总内存有限线程数 × 栈大小 超过可用内存while(true){newThread(()-{try{Thread.sleep(Long.MAX_VALUE);}catch(InterruptedExceptione){}}).start();}// Exception: java.lang.OutOfMemoryError: unable to create native thread反直觉结论在内存固定时如果遇到无法创建新线程的 OOM缩小 -Xss反而能让每个线程占用更少内存从而支持更多线程。四、堆Heap最重要的内存区域4.1 堆的分代布局┌──────────────────────────────────────────────────────────────────┐ │ Java 堆 (Heap) │ │ 由 -Xms初始大小和 -Xmx最大大小控制 │ │ │ │ ┌──────────────────────────────────┐ ┌────────────────────────┐ │ │ │ 年轻代 (Young Generation) │ │ 老年代 (Old Generation) │ │ │ │ 由 -Xmn 或 -XX:NewRatio 控制 │ │ 占剩余堆空间 │ │ │ │ │ │ │ │ │ │ ┌─────────┐ ┌──────┐ ┌──────┐ │ │ ┌──────────────────┐ │ │ │ │ │ Eden │ │ S0 │ │ S1 │ │ │ │ 长期存活对象 │ │ │ │ │ │ 区 │ │(From)│ │ (To) │ │ │ │ 大对象直接进入│ │ │ │ │ │ 80% │ │ 10% │ │ 10% │ │ │ │ │ │ │ │ │ └─────────┘ └──────┘ └──────┘ │ │ └──────────────────┘ │ │ │ │ │ │ │ │ │ │ 新对象首先在 Eden 分配TLAB │ │ 触发 Major/Full GC │ │ │ │ Minor GCEden S0 → S1 │ │ │ │ │ └──────────────────────────────────┘ └────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘对象晋升到老年代的条件年龄阈值Minor GC 后 Survivor 中对象年龄达到-XX:MaxTenuringThreshold默认 15大对象直接进入老年代对象大小超过-XX:PretenureSizeThresholdSerial/ParNew 有效动态年龄判断Survivor 区中相同年龄的对象总大小超过 Survivor 容量 50%则该年龄及以上的对象晋升Survivor 容纳不下Minor GC 后存活对象放不进 Survivor直接进老年代4.2 对象分配机制TLABThread-Local Allocation Buffer快速路径线程请求 new Object() │ ▼ 当前线程的 TLAB 是否有空间 │ 是──┤──→ 在 TLAB 内直接分配无锁极快✅ │ 否──┤──→ 重新申请一个新的 TLAB │ │ │ 是──┤──→ 在新 TLAB 内分配 ✅ │ │ │ 否──┴──→ 在 Eden 公共区域分配需同步 │ │ │ 是─────┤──→ 在 Eden 分配 ✅ │ │ │ 否─────┴──→ 触发 Minor GC │ │ │ 回收后有空间──→ 分配 ✅ │ │ └─────────────────────────失败──→ Full GC → OOM两种分配方式指针碰撞Bump-the-pointer内存规整时使用Serial/ParNew 标记整理 ┌──────────────────────────────────────────────────────┐ │ 已分配对象 │ 新对象 │────────────── 空闲 ──────────│ └──────────────────────────────────────────────────────┘ ↑ 分配指针前移 sizeof(新对象) 即可 空闲列表Free List内存碎片化时使用CMS 标记清除 维护一个记录空闲内存块的列表每次分配时查找合适的空闲块4.3 对象内存布局HotSpot 中每个对象在内存中的布局┌─────────────────────────────────────────────────────────┐ │ 对象内存布局 │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 对象头 (Object Header) │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ │ │ Mark Word8 字节64位 JVM │ │ │ │ │ │ │ │ │ │ │ │ 正常状态 │ │ │ │ │ │ [哈希码(31b)][分代年龄(4b)][偏向锁(1b)][锁(2b)]│ │ │ │ │ │ │ │ │ │ │ │ 偏向锁[线程ID(54b)][epoch(2b)][年龄(4b)][1][01]│ │ │ │ │ │ 轻量锁[指向锁记录的指针(62b)][00] │ │ │ │ │ │ 重量锁[指向互斥量的指针(62b)][10] │ │ │ │ │ │ GC标记[空(62b)][11] │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ │ │ Klass Pointer类型指针4或8字节 │ │ │ │ │ │ 指向该对象的类元数据在元空间中 │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────┐ │ │ │ │ │ 数组长度仅数组对象4字节 │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 实例数据 (Instance Data) │ │ │ │ 父类字段 子类字段按类型分组相同类型连续存储 │ │ │ └──────────────────────────────────────────────────┘ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 对齐填充 (Alignment Padding) │ │ │ │ HotSpot 要求对象大小必须是 8 字节的倍数 │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘实际大小计算示例// 以下对象在 64位 JVM 开启指针压缩(-XX:UseCompressedOops)下的大小classDemo{inta;// 4字节longb;// 8字节Objectc;// 压缩后引用4字节不压缩8字节}// 对象头Mark Word(8) Klass Pointer(压缩后4) 12字节// 实例数据int(4) long(8) 引用(压缩后4) 16字节// 对齐(1216) 28不是8的倍数填充4字节// 总计32字节4.4 堆的 OOM 场景// 场景一对象太多堆空间不足Listbyte[]listnewArrayList();while(true){list.add(newbyte[1024*1024]);// 每次分配1MB}// java.lang.OutOfMemoryError: Java heap space// 场景二大对象直接超过堆大小byte[]hugenewbyte[Integer.MAX_VALUE];// java.lang.OutOfMemoryError: Java heap space// 场景三内存泄漏对象不断加入但永不释放staticMapString,ObjectcachenewHashMap();// 每次请求都往 cache 里放数据但从不清理// OOM: Java heap space慢速泄漏OOM 时的 Heap Dump# JVM 参数OOM 时自动生成 heap dump-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/var/log/heapdump.hprof# 使用 MATEclipse Memory Analyzer分析 heap dump# 或使用 jmap 手动生成jmap-dump:formatb,file/tmp/heapdump.hprofpid五、方法区与元空间5.1 方法区存储的内容方法区存储已被加载的类型信息是全局唯一的线程共享方法区 / 元空间内容 ┌──────────────────────────────────────────────────────────┐ │ 类型信息 │ │ - 全限定名com.example.UserService │ │ - 直接父类全限定名 │ │ - 接口列表 │ │ - 访问修饰符public/final/abstract │ │ │ │ 字段信息名称、类型、修饰符 │ │ │ │ 方法信息名称、返回类型、参数类型、修饰符、字节码、异常表、操作数栈深度│ │ │ │ 运行时常量池Runtime Constant Pool │ │ - 字面量字符串(hello)、数字(3.14) │ │ - 符号引用类/方法/字段的符号名称 │ │ - 直接引用解析后的内存地址 │ │ │ │ 静态变量static fields的引用 │ │ │ │ JIT 编译后的本地代码缓存 │ └──────────────────────────────────────────────────────────┘5.2 PermGen → Metaspace 演进JDK 7 及以前永久代Permanent GenerationPermGen ┌─────────────────────────────────────────────────────┐ │ Java Heap │ │ ┌──────────────────────────────┐ ┌──────────────┐ │ │ │ 年轻代 │ │ 老年代 │ │ │ └──────────────────────────────┘ └──────────────┘ │ │ ┌───────────────────────────────────────────────┐ │ │ │ 永久代 (PermGen) 最大 -XX:MaxPermSize256m │ │ │ └───────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ 问题 1. PermGen 大小固定难以预估容易出现 OOM: PermGen space 2. GC 频率低类卸载困难FullGC 才会清理PermGen 3. Oracle JVM 和 JRockit 合并时的架构阻碍 JDK 8元空间Metaspace ┌─────────────────────────────────────────────────────┐ │ Java Heap │ │ ┌──────────────────────────────┐ ┌──────────────┐ │ │ │ 年轻代 │ │ 老年代 │ │ │ └──────────────────────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ 元空间 (Metaspace) - 使用本地内存 │ │ 默认无上限物理内存有多大用多大 │ │ -XX:MaxMetaspaceSize256m 设置上限 │ └──────────────────────────────────────┘ 优势 1. 本地内存不受堆大小限制 2. 类加载器卸载时对应元空间立即回收 3. 不再有固定的 PermGen 大小限制5.3 字符串常量池的变迁字符串常量池String Pool / String Table的位置也随 JDK 版本迁移JDK 6在方法区PermGen中 ↓ 问题受 PermGen 大小限制大量 intern() 操作易 OOM JDK 7移到堆中Young/Old Gen ↓ 优势受堆大小控制GC 可回收intern() 更安全 JDK 8仍在堆中同 JDK 7// 字符串常量池机制演示Strings1hello;// 常量池中创建 helloStrings2hello;// 复用常量池中的引用Strings3newString(hello);// 堆上新对象值从常量池复制System.out.println(s1s2);// true同一引用System.out.println(s1s3);// falses3 是堆上新对象System.out.println(s1s3.intern());// trueintern()返回常量池引用5.4 元空间 OOM 场景// 场景大量动态生成类ClassLoader 未被回收// 典型案例CGLib 代理 未正确配置代理缓存while(true){EnhancerenhancernewEnhancer();enhancer.setSuperclass(SomeClass.class);enhancer.setUseCache(false);// 关闭缓存每次都生成新的代理类enhancer.setCallback((MethodInterceptor)(obj,method,args,proxy)-proxy.invokeSuper(obj,args));enhancer.create();// 每次创建新的 Class元空间不断增长}// java.lang.OutOfMemoryError: Metaspace元空间参数配置-XX:MetaspaceSize128m# 元空间初始大小也是首次扩容触发 Full GC 的阈值-XX:MaxMetaspaceSize512m# 元空间最大大小建议设置防止无限增长-XX:MinMetaspaceFreeRatio40# 元空间 GC 后最小空闲比例低于此值则扩容-XX:MaxMetaspaceFreeRatio70# 元空间 GC 后最大空闲比例高于此值则缩容六、本地方法栈Native Method Stack6.1 与虚拟机栈的关系本地方法栈与虚拟机栈类似区别在于虚拟机栈为 Java 方法服务本地方法栈为native方法服务如Object.hashCode()、System.currentTimeMillis()HotSpot JVM 将两者合并为一个栈实现不做区分。native 方法调用流程// Java 层publicclassSystem{publicstaticnativelongcurrentTimeMillis();// ↑ 这个 native 方法的实现在 C/C 代码中}// JNI 调用链// Java 方法 → 本地方法栈 → JNI → C/C 函数 → 操作系统调用七、运行时数据区全景参数速查7.1 常用内存配置参数# 堆内存 -Xms4g# 堆初始大小 4GB-Xmx4g# 堆最大大小 4GB与Xms相同避免动态扩容-Xmn2g# 年轻代大小 2GB或用 -XX:NewRatio2-XX:NewRatio2# 老年代:年轻代 2:1-XX:SurvivorRatio8# Eden:Survivor 8:1:1默认-XX:PretenureSizeThreshold3145728# 大于3MB直接进老年代Serial/ParNew有效-XX:MaxTenuringThreshold15# 对象晋升老年代年龄阈值# 元空间 -XX:MetaspaceSize128m# 元空间初始大小-XX:MaxMetaspaceSize512m# 元空间最大大小# 虚拟机栈 -Xss512k# 每个线程栈大小 512KB默认通常1MB# 指针压缩 -XX:UseCompressedOops# 开启普通对象指针压缩堆32GB时默认开启-XX:UseCompressedClassPointers# 开启类指针压缩# OOM 诊断 -XX:HeapDumpOnOutOfMemoryError# OOM 时自动 heap dump-XX:HeapDumpPath/var/log/jvm/# heap dump 保存路径-XX:PrintGCDetails# 打印 GC 详情7.2 OOM 类型速查OOM 信息出问题的区域常见原因Java heap space堆内存泄漏、堆太小、大对象Metaspace元空间动态生成类过多、ClassLoader 泄漏PermGen space永久代JDK7-类太多、PermGen 设置过小unable to create native thread本地内存线程栈线程数过多可减小-XssGC overhead limit exceeded堆GC 频繁但回收效果极差GC时间98%Direct buffer memory堆外内存NIO DirectBuffer 分配过多Map failed内存映射文件内存映射文件过多八、总结运行时数据区是 JVM 内存管理的核心骨架线程私有三区程序计数器执行现场、虚拟机栈方法调用链、本地方法栈Native调用随线程生灭不需要 GC 管理线程共享两区堆对象的家、方法区/元空间类型信息的家JVM 生命周期内持续存在需要 GC 管理堆的核心机制分代模型年轻代老年代、TLAB 快速分配、对象晋升策略、对象头Mark Word 承载锁信息GC信息元空间演进JDK 8 将 PermGen 移出堆用本地内存实现 Metaspace解决了固定大小限制问题OOM 精准定位不同区域 OOM 的错误信息不同参数配置也各异下一篇预告有了内存区域的完整认知我们就能进入核心战场——垃圾回收。JVM 如何判断对象死了标记-清除、标记-整理、复制算法各有什么特点Stop-The-World 到底是怎么发生的第04篇将深入垃圾回收算法的实现原理。系列导航上一篇【JVM深度解析】第02篇类加载机制深度解析下一篇【JVM深度解析】第04篇垃圾回收算法与实现原理系列目录JVM深度解析系列全集参考资料《深入理解Java虚拟机第3版》第2章 — 周志明著JVM Specification: Chapter 2 - The Structure of the Java Virtual MachineJEP 122: Remove the Permanent GenerationJava Object LayoutJOL工具 — 查看对象实际内存布局Aleksey Shipilёv: Java Objects Inside OutHotSpot VM TLAB 设计文档

更多文章