JVM
- JVM内存区域划分
-
- [程序计数器(Program Counter Register)](#程序计数器(Program Counter Register))
- [Java 虚拟机栈(JVM Stacks)](#Java 虚拟机栈(JVM Stacks))
- [本地方法栈(Native Method Stacks)](#本地方法栈(Native Method Stacks))
- [Java 堆(Heap)](#Java 堆(Heap))
- [元数据区(Metaspace)/ 方法区(Method Area)](#元数据区(Metaspace)/ 方法区(Method Area))
- 类加载机制
- 垃圾回收机制(GC)
-
- [GC 工作两大步骤](#GC 工作两大步骤)
- [如何找到垃圾 ------ 对象存活判断](#如何找到垃圾 —— 对象存活判断)
-
- [引用计数法(Reference Counting)](#引用计数法(Reference Counting))
- [可达性分析(Reachability Analysis)⭐](#可达性分析(Reachability Analysis)⭐)
- 如何释放垃圾
- [分代回收(Generational Collection) ⭐](#分代回收(Generational Collection) ⭐)
- 三者之间的联系与总结
JVM内存区域划分
JVM 从操作系统申请一大块内存后,会根据功能划分为几个不同的运行时数据区。这些区域各司其职,有的线程私有,有的线程共享。

线程共享区域
元数据区 (Metaspace) -XX:PermSize -XX:MaxPermSize
常量池
类元信息 (Class)
Cache
JIT编译产物
堆区 (Heap) -Xms -Xmx
新生代 (Young Generation) -Xmn
Eden
S0 (Survivor)
S1 (Survivor)
老年代 (Old Generation)
线程私有区域
虚拟机栈 (JVM Stacks) -Xss
栈帧-3 (方法C)
局部变量表
操作栈
动态连接
方法返回地址
栈帧-2 (方法B)
局部变量表
操作栈
动态连接
方法返回地址
栈帧-1 (方法A)
局部变量表
操作栈
动态连接
方法返回地址
程序计数器 (Program Counter Register)
本地方法栈 (Native Method Stacks)
这张图采用自顶向下的布局,分别展示了线程私有的程序计数器、本地方法栈和虚拟机栈(含 -Xss 参数及三个栈帧内部结构),线程共享的堆区(包含新生代 Eden/S0/S1、老年代及 -Xms/-Xmx/-Xmn 参数)以及元数据区(包含常量池、类元信息、Cache、JIT 编译产物及 PermSize 相关参数)
程序计数器(Program Counter Register)
- 作用:记录当前线程正在执行的字节码指令地址。当线程切换后,能恢复到正确的执行位置。
- 特点 :
- 内存占用非常小。
- 线程私有(每个线程都有自己的 PC)。
- 不会发生
OutOfMemoryError。
Java 虚拟机栈(JVM Stacks)
- 作用 :描述 Java 方法执行的线程内存模型。每个方法执行时都会创建一个栈帧。
- 栈帧包含 :
- 局部变量表:存储方法参数和方法内的局部变量。基本类型直接存储值,引用类型存储对象的地址。
- 操作数栈:用于存放计算过程中的中间结果,字节码指令会频繁压栈和出栈。
- 动态链接:指向运行时常量池中该方法的符号引用,用于支持多态。
- 方法返回地址:方法正常或异常退出后,返回到调用方的位置。
- 特点 :
- 线程私有。
- 栈深度超过限制 →
StackOverflowError(如无限递归)。 - 动态扩展失败 →
OutOfMemoryError。
🔍 注意:局部变量是基本类型或对象引用时,存储在栈上;但对象本身始终在堆上。
本地方法栈(Native Method Stacks)
- 作用 :为虚拟机调用的 native 方法(如用 C/C++ 实现的底层方法)服务。
- 特点:与虚拟机栈类似,只是服务的对象不同。HotSpot 虚拟机中,本地方法栈和虚拟机栈合二为一。
Java 堆(Heap)
- 作用 :所有线程共享 的内存区域,用于存储 new 出来的对象 和 数组。
- 分代设计 (为了优化垃圾回收):
- 新生代(Young Generation):又分为 Eden 区、S0(Survivor From)、S1(Survivor To)。大多数对象在这里诞生并很快消亡。
- 老年代(Old Generation / Tenured):存放生命周期较长的对象,从新生代多次回收后晋升而来。
- 特点 :
- 垃圾回收的主要区域,也称为 "GC 堆"。
- 可通过
-Xms和-Xmx控制堆大小。 - 内存不足时抛出
OutOfMemoryError。
📌 new出来的对象的存储位置小总结:⭐
- 局部变量(包括对象引用) → 栈上的局部变量表
- 成员变量(实例变量) → 堆上的对象内部
- 静态成员变量 → 元数据区(见下文)
元数据区(Metaspace)/ 方法区(Method Area)
- 作用 :存储已被虚拟机加载的类元信息 、静态变量 、常量池 (如 String 常量池)、即时编译器编译后的代码等。
- 元信息包括 :
- 类的全限定名、访问修饰符(public/private 等)。
- 父类、实现的接口列表。
- 字段信息(字段名、类型、修饰符)。
- 方法信息(方法名、参数类型、返回值、修饰符等)。
- Java 8 前后的变化 :
- Java 7 及以前:方法区位于永久代(PermGen),有默认大小上限,容易 OOM。
- Java 8 开始 :使用元数据区(Metaspace) ,不再使用虚拟机内存,而是使用本地内存(Native Memory),默认大小仅受物理内存限制。
- 特点 :
- 线程共享。
- 回收目标主要是常量池和类的卸载(条件比较苛刻)。
- 内存不足 →
OutOfMemoryError。
✨ 反射的核心依据:反射之所以能动态获取类的字段、方法等信息,正是因为元数据区保存了完整的类结构元数据。
类加载机制
垃圾回收机制(GC)
手动释放内存太麻烦、太容易出错(如忘记释放、重复释放、野指针等)。Java 引入自动垃圾回收,由 JVM 自动识别不再使用的内存并释放。
GC 工作两大步骤
- 找到垃圾
不再使用的对象
2. 释放垃圾
回收内存
如何找到垃圾 ------ 对象存活判断
引用计数法(Reference Counting)
- 原理:每个对象附带一个整数计数器,每多一个引用就 +1,引用失效就 -1。计数器为 0 时即为垃圾。
- 采用者:Python、PHP 等。
- 缺点(Java 未采用):
缺点2_循环引用
相互引用
引用断开
引用断开
对象 A
对象 B
外部引用 a = null
外部引用 b = null
计数仍为 1
计数仍为 1
无法回收的垃圾
缺点1_额外内存
每个对象额外 4 字节存储计数器
内存开销大
循环引用示例(两个对象互相引用,外部引用断开后依然无法回收):
java
class Test {
Test t = null;
}
Test a = new Test();
Test b = new Test();
a.t = b;
b.t = a;
a = null;
b = null; // 此时 a、b 对象引用计数均为 1,但已无法使用
可达性分析(Reachability Analysis)⭐
- 原理 :从一组 GC Roots 出发,沿着引用链遍历所有对象。能被遍历到的对象标记为 可达 ,未被标记的即为 垃圾。
堆中对象
GC Roots 起点
栈中局部变量引用
静态变量引用
常量池引用
JNI 引用
对象 A
可达
对象 B
可达
对象 C
可达
垃圾对象
不可达
-
过程:
- 确定当前的 GC Roots(随时可获取)。
- 从 GC Roots 开始遍历所有引用链。
- 每访问到一个对象,标记为 可达。
- 遍历结束后,未被标记的对象就是 不可达,即垃圾。
-
触发频率 :JVM 会周期性地执行可达性分析(通常在内存分配达到一定阈值时触发,或者在空闲时执行)。
如何释放垃圾
标记-清除(Mark-Sweep)
- 步骤:标记所有垃圾对象 → 直接清除,回收内存。
- 优点:简单、无需移动对象。
- 缺点 :产生大量内存碎片,导致后续分配大对象时失败。
清除垃圾
回收后
对象1 存活
空闲碎片
对象3 存活
空闲碎片
对象5 存活
回收前
对象1 存活
对象2 垃圾
对象3 存活
对象4 垃圾
对象5 存活
标记-复制(Mark-Copy)
- 步骤 :将内存分为两块,每次只使用其中一块。GC 时将存活对象复制到另一块,然后整体清空原块。
- 优点:无碎片,分配速度快。
- 缺点 :
- 内存利用率低(最多只用一半)。
- 存活对象多时,复制成本高。
- 复制存活对象到 To 区
- 清空 From 区,交换角色
回收后状态
From 区
已清空,变为空闲
To 区
存放复制过来的存活对象
回收前状态
From 区
使用中,含存活+垃圾
To 区
空闲
标记-整理(Mark-Compact)
- 步骤 :标记存活对象 → 将所有存活对象向一端移动(类似顺序表搬运) → 清理边界以外的内存。
- 优点:解决内存碎片,且内存利用率高。
- 缺点:移动对象有开销(尤其在对象数量多时)。
存活对象向一端移动
整理后
存活
存活
存活
连续可用空间
整理前
存活
垃圾
存活
垃圾
存活
分代回收(Generational Collection) ⭐
Java 将标记-复制 和标记-整理 相结合,根据对象的年龄(熬过 GC 的次数)采用不同策略,扬长避短。
对象的年龄与区域划分
- 年龄:每经历一轮 GC 仍存活,年龄 +1。
- 区域划分 :
- 新生代(Young Generation) :年龄小的对象。
- 伊甸区(Eden):新对象诞生地(占新生代 80%)。
- 幸存区(Survivor):S0 和 S1,各占 10%,用于复制回收。
- 老年代(Old Generation):年龄大的对象,预期长期存活。
- 新生代(Young Generation) :年龄小的对象。
Minor GC
存活对象复制
角色互换
下次复制到对端
年龄达到阈值
晋升老年代
Major/Full GC
堆内存
新生代
老年代
伊甸区 80%
幸存区 S0 10%
幸存区 S1 10%
标记-整理
或标记-清除
对象晋升流程
Minor GC 存活
年龄=1
多次 GC 仍存活
年龄递增
达到阈值
年龄 ≥ 15
新对象
伊甸区
幸存区 From
幸存区 To
老年代
为什么这样设计?
- 绝大多数新对象 朝生夕死 ,伊甸区回收时只需复制极少量存活对象到幸存区,标记-复制开销极低。
- 幸存区 S0/S1 反复复制,让真正长期存活的对象 熬成婆,最终晋升老年代。
- 老年代对象生命周期长,使用标记-整理(或标记-清除)避免浪费一半空间,且回收频率低,整理成本可接受。
分代回收的优点
- 扬长避短 :
- 新生代:复制算法 → 无碎片 + 复制量小(大部分对象已死)。
- 老年代:整理/清除算法 → 高空间利用率 + 移动成本相对低频。
- 整体效率高,是 Java 主流 GC 策略(G1、ZGC 等也基于分代或分区思想)。
三者之间的联系与总结
| 核心内容 | 主要关注点 | 常见问题 |
|---|---|---|
| 内存区域划分 | 数据存放在哪里(栈、堆、元数据区......) | 栈溢出、堆溢出、元数据区溢出 |
| 类加载机制 | 字节码如何被加载进元数据区,并生成 Class 对象 | NoClassDefFoundError、ClassCastException |
| 垃圾回收 | 如何自动清理不再使用的堆内存 | 内存泄漏、频繁 Full GC、停顿时间过长 |
三者协同工作:
- 类加载器将字节码装载到元数据区,同时生成
Class对象放在堆中。 - 程序运行时,方法调用在栈帧中推进,局部变量指向堆中的对象。
- 当对象不再被任何栈帧引用(不可达 GC Roots)时,垃圾回收器在合适的时机回收其堆内存。