JVM 内存结构、堆细分、对象生命周期、内存模型全解析
我理解 JVM 内存体系,可以从整体结构、线程私有/共享、对象分配路径、永久代/元空间区别等几个方面去系统化地看。
JVM 的设计实际上兼顾了:线程隔离、垃圾回收效率、方法元数据存储、栈式执行模型等多个目标。
一、JVM 内存整体结构(JDK8 之后)
JDK8 之后的 JVM 内存区域可以分为两大类:
线程私有区域 和 线程共享区域
1️⃣ 线程私有(每个线程独占)
这些区域随着线程创建/销毁:
① 程序计数器(PC寄存器)
- 保存当前线程执行的字节码指令地址
- 线程切换后能恢复执行位置
- 没有 OOM 风险
② Java 虚拟机栈(Stack)
- 存储局部变量、操作数栈、方法调用链等
- 每个方法调用对应一个栈帧(Stack Frame)
- 当栈深度过深会抛出:
StackOverflowErrorOutOfMemoryError: unable to create new native thread
③ 本地方法栈(Native Stack)
- 用于存储 native 方法调用
- 与 JVM 栈类似,只是针对 JNI 和本地代码
2️⃣ 线程共享(所有线程共享)
这些区域属于整个 JVM 进程:
① 堆(Heap)
- JVM 中最大的一块内存
- 所有对象实例、数组都存放在堆中
- 垃圾收集的核心区域
- 会发生 OOM:
java.lang.OutOfMemoryError: Java heap space
② 方法区(Method Area,JDK8 之后使用 Metaspace)
- 主要存储:
- 类元数据(Class 信息)
- 方法信息
- 常量池
- 静态变量
- JDK8 之后使用 元空间(Metaspace) 替代永久代
③ 运行时常量池(Runtime Constant Pool)
- 存放字面量、符号引用、方法引用
- 属于方法区的一部分
二、永久代(PermGen) vs 元空间(Metaspace)
永久代(JDK7 及之前)
-
存储类元数据、静态变量、常量池等
-
内存大小由 JVM 参数控制,如:
-XX:PermSize -XX:MaxPermSize -
过多动态类加载会导致:
java.lang.OutOfMemoryError: PermGen space
元空间(JDK8+)
-
类元数据不再存于 JVM 内部,而是存在 本地内存(Native Memory)
-
默认可以自动增长,减少 OOM 风险
-
受参数限制:
-XX:MetaspaceSize -XX:MaxMetaspaceSize
设计意图:
- 永久代经常 OOM,且垃圾回收对它管理不佳
- 将类元数据移至本地内存后,更加灵活、安全
三、堆内存的细分结构(非常重要)
Java 堆分为两个区域:
Java Heap
├── 新生代(Young Generation)
│ ├── Eden(伊甸园)
│ └── Survivor(S0/S1)
└── 老年代(Old Generation)
新生代(Young)
- 新生分配的对象都在 Eden
- 当 Eden 填满时触发 Minor GC
- 存活对象转移到 Survivor 区
Survivor 区采用:
S0 与 S1 轮换(复制算法)
一个 Always Empty,一个接收复制对象
老年代(Old)
- 经多次 GC 仍存活的对象晋升到老年代
- 老年代使用标记-整理(Mark-Compact) 或 CMS/ G1/ ZGC
四、JVM 中对象的分配过程(HotSpot 为例)
对象分配总体流程如下:
① new 对象 → 先检查栈上是否能分配(逃逸分析)
如果方法内对象没有逃逸到外部:
- JVM 会直接在栈上分配(Stack Allocation)
- 无 GC 压力
- 还会触发锁消除、标量替换等优化
② 大部分对象在 Eden 区分配
- Eden 内存足够:
直接在 Eden 分配(使用指针碰撞 Bump-the-pointer) - Eden 不够 → 触发 Minor GC
③ Minor GC 之后,存活对象进入 Survivor S0
标记对象 → 拷贝到 S0 → 清空 Eden
④ 多次 Minor GC 后,对象晋升到老年代
晋升条件包括:
- 对象达到年龄阈值(默认 15)
- Survivor区放不下(担保机制)
⑤ 大对象可能直接进入老年代
例如大型数组
参数控制:
-XX:PretenureSizeThreshold
五、垃圾回收过程(对象生命周期本质上是 GC 过程)
对象在堆中的生命周期本质上是:
创建 → 存活 → 晋升 → 回收
新生代 GC(Minor GC)
- 频繁发生
- 使用复制算法(清理快)
老年代 GC(Major/Full GC)
- 较少发生
- 用标记-整理或 CMS/G1/ZGC
- 停顿较长
六、JVM 有哪些类加载机制(补充整合)
三种主要方式:
1. 隐式加载
- new 对象
- 调用静态方法
- 调用静态变量
- 反射
2. 显式加载
Class.forName()(会初始化)ClassLoader.loadClass()(只加载不初始化)
3. 自定义类加载器
- 用于模块隔离、热加载、服务发现
七、对象能否被回收的判断方式(可达性分析 GC Roots)
GC Roots 包括:
- 栈帧中的局部变量表
- 方法区中的静态变量
- JNI 本地方法引用
- 常量池引用
八、JVM 内存整体总结图(重点)
线程私有--------------------------| 程序计数器 PC || Java栈 Stack || 本地方法栈 Native |--------------------------线程共享--------------------------| Heap(堆) || ├─ Eden || ├─ S0 / S1 || └─ Old |--------------------------| 方法区(Metaspace) || 运行时常量池 |--------------------------
九、个人理解总结(面试很加分)
我认为 JVM 的内存结构整个核心目标是:
让对象分配高效(Eden、TLAB)、让 GC 有效分代(新生代+老年代)、让类元数据分离(Metaspace)、让线程隔离执行(栈+PC)
JVM 的设计体现了一个“以 GC 为中心的内存管理哲学”,所有区域的划分都是为了提升 GC 和执行效率。
🔥 一句话总总结
JVM 内存由线程私有(PC/Stack/Native Stack)与线程共享(Heap/Metaspace)组成;
堆分为年轻代和老年代,采用分代收集;
对象分配优先 Eden,存活晋升老年代;
元空间替代永久代,不再 OOM 于 PermGen;
类加载遵循生命周期与双亲委派模型。
