
一.JVM 简介



JVM是Java Virtual Machine的简称,意为Java虚拟机。
虚拟机是指通过软件模拟的具有完整硬件功能的、运⾏在⼀个完全隔离的环境中的完整计算机系统。
常⻅的虚拟机:JVM、VMwave、VirtualBox。
JVM和其他两个虚拟机的区别:
- VMwave与VirtualBox是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器;
- JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进 ⾏了裁剪。
JVM是⼀台被定制过的现实当中不存在的计算机。
JVM 运行流程
程序在执⾏之前先要把java代码转换成字节码(class⽂件),JVM⾸先需要把字节码通过⼀定的⽅式类加载器(ClassLoader) 把⽂件加载到内存中运行时数据区(RuntimeDataArea) ,⽽字节码⽂件是JVM的⼀套指令集规范,并不能直接交个底层操作系统去执⾏,因此需要特定的命令解析器**执 ⾏引擎(ExecutionEngine)**将字节码翻译成底层系统指令再交由CPU去执⾏,⽽这个过程中需要 调⽤其他语⾔的接⼝**本地库接口(NativeInterface)**来实现整个程序的功能,这就是这4个主要组成部分的职责与功能

二.JVM 运行时数据区

JVM运⾏时数据区域也叫内存布局,但需要注意的是它和Java内存模型((JavaMemoryModel,简 称JMM)完全不同,属于完全不同的两个概念,它由以下5⼤部分组成:

1.程序计数器
(1)程序计数器是线程私有的
- 每个线程独有一个程序计数器
- 保证线程切换 后,恢复线程时,能准确回到上一条中断位置,继续执行

(2)存储内容
如果当前线程正在执⾏的是⼀个Java⽅法 ,这个计数器记录的是虚拟机字节码指令的地址;如果正在执⾏的是⼀个Native⽅法 ,这个计数器值为空(本地方法由 JVM 底层调用,不由字节码执行,PC 无记录)。
(3)唯⼀⼀个没有规定任何OOM情况
- 空间极小,仅存指令地址
- JVM 规范明确:该区域不会发生内存溢出
(4)作用
指令跳转
JVM 解释器执行代码时,根据 PC 地址,逐条读取字节码执行;循环、分支、if、for、跳转语句,都靠 PC 记录跳转地址。
线程切换恢复多线程环境下,时间片耗尽切换线程:
- 暂停当前线程,保存 PC 地址
- 下次切换回来,读取 PC 地址,继续执行
异常、堆栈追踪 报错时的行号追踪,底层依赖 PC 记录的指令位置映射。
2.虚拟机栈


3.本地方法栈
本地⽅法栈和虚拟机栈类似,是给本地⽅法使⽤的

4.堆
最大的区域。
堆的作⽤:程序中创建的所有对象都在保存在堆中
5.元数据区(原称方法区)


6.栈溢出


7.堆溢出



三.JVM类加载

1.类加载过程

在加载Loading阶段,Java虚拟机需要完成以下三件事情:
1)通过⼀个类的全限定名来获取定义此类的⼆进制字节流。
2)将这个字节流所代表的静态存储结构转化为⽅法区的运⾏时数据结构。
3)在内存中⽣成⼀个代表这个类的java.lang.Class对象,作为⽅法区这个类的各种数据的访问⼊⼝。






2.双亲委派模型=>找.class文件


双亲委派模型的优点
-
避免重复加载类:⽐如A类和B类都有⼀个⽗类C类,那么当A启动时就会将C类加载起来,那么在B类进⾏加载时就不需要在重复加载C类了。
-
安全性:使⽤双亲委派模型也可以保证了Java的核⼼API不被篡改,如果没有使⽤双亲委派模 型,⽽是每个类加载器加载⾃⼰的话就会出现⼀些问题,⽐如我们编写⼀个称为java.lang.Object 类的话,那么程序运⾏的时候,系统就会出现多个不同的Object类,⽽有些Object类⼜是⽤⼾⾃⼰提供的因此安全性就不能得到保证了
也可以破坏双亲委派模型

四.垃圾回收相关
在Java中,所有的对象都是要存在内存中的(也可以说内存中存储的是⼀个个对象),因此我们将内存回收,也可以叫做死亡对象的回收。


1.死亡对象的判断-->引用指向


(1)引用计数




引用计数的缺陷:




(2)可达性分析
通过⼀系列称为**"GCRoots"**的对象作为起始点,从这些节点开始向下搜索,搜索⾛过的路径称之为"引⽤链",当⼀个对象到GCRoots没有任何的引⽤链相连时(从GCRoots到这个对象不可达)时,证明此对象是不可⽤的。



(
1.栈上局部变量引用的对象,是当前线程正在执行的代码里正在使用的对象,JVM 绝对不能回收。
2.常量池引用的对象是全局共享的,随时可能被使用,不能回收。
3.静态变量引用的对象,只要类还在就一直存在,属于全局可达对象。
还有一些其他的:
- 本地方法栈中 JNI 引用的对象:被 native 方法引用的对象,JVM 不能回收
- JVM 内部引用:比如
System ClassLoader、Thread对象等 JVM 内部使用的对象 - 被同步锁持有的对象:比如
synchronized (obj)中的obj,正在被锁持有,不能回收

总之,判断是否可以作为gcroots就看这个引用是不是「全局可达、或当前正在被使用、且不能被回收」的引用
)

2.垃圾回收算法
(1)标记清除
⾸先标记出所有需要回收的对象,在标记完成后统⼀回收所有被标记的对象
"标记-清除"算法的不⾜主要有两个:
- 效率问题:标记和清除这两个过程的效率都不⾼
- 空间问题:标记清除后会产⽣⼤量不连续的内存碎⽚,空间碎⽚太多可能会导致以后在程序运⾏中需要分配较⼤对象时,⽆法找到⾜够连续内存⽽不得不提前触发另⼀次垃圾收集。

(2)复制

(3)标记整理
把非垃圾对象前移 把后面的对象全部删除

开销可能也很大
(4)分代-->针对不同情况 采取不同方案
当前JVM垃圾收集都采⽤的是"分代收集(GenerationalCollection)"算法,这个算法并没有新思想, 只是根据对象存活周期的不同将内存划分为⼏块。⼀般是把Java堆分为新⽣代和⽼年代。在新⽣代 中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采⽤复制算法;⽽⽼年代中对象存活率⾼、没有额外空间对它进⾏分配担保,就必须采⽤"标记-清理"或者"标记-整理"算法。
(
哪些对象会进⼊新⽣代?哪些对象会进⼊⽼年代?
- 新⽣代:⼀般创建的对象都会进⼊新⽣代;
- ⽼年代:⼤对象和经历了N次(⼀般情况默认是15次)垃圾回收依然存活下来的对象会从新⽣代移动到⽼年代。
)



面试题:请问了解MinorGC和FullGC么,这两种GC有什么不⼀样吗?
| 回收类型 | 回收范围 | 触发场景 |
|---|---|---|
| Minor GC / Young GC (开销小 频率高) | 仅回收 新生代(Eden + Survivor) | Eden 区空间不足时触发 |
| Major GC (开销大 频率低) | 仅回收 老年代 | 老年代空间不足时触发(CMS 等老年代收集器会单独触发) |
| Full GC | 回收 新生代 + 老年代 + 元空间 / 方法区 | 老年代空间不足、System.gc ()、元空间不足等 |
3.垃圾收集器
垃圾收集器是为了保证程序能够正常、持久运⾏的⼀种技术,它是将程序中不⽤的死亡对象也就是垃圾对象进⾏清除,从⽽保证了新对象能够正常申请到内存空间。
如果说上⾯我们讲的收集算法是内存回收的⽅法论,那么垃圾收集器就是内存回收的具体实现。
| 项目 | CMS | G1(G One) | ZGC |
|---|---|---|---|
| 回收范围 | 仅老年代 | 新生代 + 老年代(混合) | 全堆 |
| 算法 | 标记 - 清除(无压缩) | 标记 - 复制(分区压缩) | 全并发压缩 |
| 分区 | 无 | Region 分区 | 分区 |
| 延迟 | 低(2 次 STW) | 低(可控) | 极低 |
| 碎片 | 有 | 无 | 无 |
| 大内存 | 差 | 一般 | 极强 |
| 适用版本 | JDK8 主流 | JDK8~13 | JDK11+ |
| 核心问题 | 碎片、Full GC | 堆大效率一般 | 无 |
-
CMS
- 并发清除,不压缩 → 内存碎片
- 两次 STW:初始标记、重新标记
- 缺点:碎片、并发开销、容易 Full GC
-
G1
- 分区 Region,优先回收垃圾多的区域
- 混合回收,自带压缩,无碎片
- 可控 STW 时间,替代 CMS
- 一次gc 只回收其中一部分区域
-
ZGC
- 新一代低延迟 GC,全并发
- 超大堆友好,毫秒级 STW
- 未来主流,替代 G1/CMS
4.总结:一个对象的一生
⼀个对象的⼀⽣:我是⼀个普通的Java对象,我出⽣在Eden区,在Eden区我还看到和我⻓的很像 的⼩兄弟,我们在Eden区中玩了挺⻓时间。有⼀天Eden区中的⼈实在是太多了,我就被迫去了 Survivor 区的"From"区(S0区),⾃从去了Survivor区,我就开始漂了,有时候在Survivor的 "From"区,有时候在Survivor的"To"区(S1区),居⽆定所。直到我18岁的时候,爸爸说我 成⼈了,该去社会上闯闯了。于是我就去了年⽼代那边,年⽼代⾥,⼈很多,并且年龄都挺⼤的,我 在这⾥也认识了很多⼈。在⽼年代⾥,我⽣活了很多年(每次GC加⼀岁)然后被回收了。
五.JMM(Java 内存模型(Java Memory Model))
不是 JVM 堆 / 栈内存结构,是「抽象规则」 ,用来解决:多线程下:共享变量可见性、原子性、有序性 问题。
1.JMM 核心本质
JMM 是一套语法规则,规定:
- 线程如何访问共享变量
- 变量如何在线程间传递
- 限制编译器、CPU 指令重排
核心结构
- 共享内存:堆、方法区(全局共享)
- 私有内存 :每个线程本地内存 (抽象概念,不是硬件)
- 缓存变量副本、临时数据
工作机制:
- 线程读 / 写共享变量 → 先读写自己的本地内存
- 再同步到主内存
- 若无同步,多线程互相不可见
2.JMM 三大核心问题(必考)
1. 可见性
问题 :多线程共享变量,一个线程修改,另一个看不到 。原因 :变量存在本地内存缓存 ,没及时刷新主内存。解决 :volatile、synchronized、Lock
2. 原子性
问题 :多线程并发操作,多步操作被拆分 ,出现数据错乱。例:i++ (读→改→写 三步,非原子)解决 :synchronized、Lock、原子类(Atomic)
3. 有序性
问题 :编译器 / CPU 为优化,乱序执行 (指令重排)。多线程下逻辑错乱。解决 :volatile、synchronized、内存屏障
3.JMM 核心机制:内存屏障
JMM 靠内存屏障 禁止重排、强制刷新内存:
- Load:读屏障 → 强制读主内存
- Store :写屏障 → 强制写主内存
volatile底层就是靠内存屏障实现。
4.volatile 三大作用(JMM 核心)
- 保证可见性读写强制走主内存,禁止本地缓存。
- 保证有序性禁止指令重排(前后屏障)。
- ❌ 不保证原子性 不能解决
i++这种复合操作。
5.JMM 与 硬件内存模型区别
- JMM :抽象规则,Java 语言层面规范
- CPU 缓存 :硬件物理 ,CPU 高速缓存JMM 本质:屏蔽不同硬件缓存差异,统一多线程规则。
6.JMM 八大内存操作(简答 / 背诵)
主内存 ↔ 本地内存 交互 8 种操作:read、load、write、store、use、assign、lock、unlock
约束:
- 不能乱序
- 共享变量修改必须同步主内存
7.终极背诵版
- JMM 是 Java 内存模型,是抽象规则,非物理内存。
- 分为主内存 (共享)+ 线程本地内存(私有)。
- 解决多线程三大问题:可见性、原子性、有序性。
- 核心手段:volatile、synchronized、内存屏障。
- volatile :保证可见性、有序性,不保证原子性。
- 指令重排是 JMM 有序性问题的根源。




