目录
前言
一、JVM 简介
二、JVM 发展史
1. Sun Classic VM(1996 年,JDK1.0)
2. Exact VM(JDK1.2)
3. HotSpot VM(当前主流)
4. JRockit(专注服务器端)
5. J9 VM(IBM)
6. Taobao JVM(国产深度定制)
关键补充:JVM 与《Java 虚拟机规范》
三、JVM 执行流程
1. 核心组件
2. 完整执行流程
四、运行时数据区(内存布局)
1. 堆(线程共享)
2. 虚拟机栈(线程私有)
3. 本地方法栈(线程私有)
4. 程序计数器(线程私有)
5. 方法区(线程共享)
6. 内存溢出异常(OOM)详解
(1)Java 堆溢出(java.lang.OutOfMemoryError: Java heap space)
(2)虚拟机栈 / 本地方法栈溢出
(3)元空间溢出(java.lang.OutOfMemoryError: Metaspace)
五、类加载机制
1. 类的生命周期
2. 类加载核心流程
(1)加载(Loading)
(2)验证(Verification)
(3)准备(Preparation)
(4)解析(Resolution)
(5)初始化(Initialization)
3. 双亲委派模型
(1)类加载器层次结构
(2)双亲委派模型的工作流程
(3)双亲委派模型的核心优势
4. 破坏双亲委派模型的案例(SPI 机制:JDBC)
(1)问题背景
(2)解决方案:线程上下文类加载器
(3)核心源码(DriverManager)
(4)总结
六、垃圾回收(GC)
1. 对象存活判断算法
(1)引用计数法(淘汰)
(2)可达性分析算法(主流)
(3)引用的扩展分类(JDK1.2)
2. 垃圾回收算法
(1)标记 – 清除算法(基础)
(2)复制算法(新生代首选)
(3)标记 – 整理算法(老年代首选)
(4)分代收集算法(实际应用)
面试题:Minor GC 与 Full GC 的区别
3. 垃圾收集器(HotSpot)
(1)核心概念
(2)收集器分类与特性
(3)重点收集器详解
① CMS 收集器(低停顿首选)
② G1 收集器(全区域首选)
4. 一个对象的一生(总结)
七、Java 内存模型(JMM)
1. 核心概念:主内存与工作内存
2. 内存间交互操作(8 种原子操作)
3. JMM 的三大核心特性
(1)原子性
(2)可见性
(3)有序性
4. volatile 关键字详解
(1)核心特性
(2)不保证原子性的示例
(3)volatile 的适用场景
(4)禁止指令重排序的作用
5. 双重检查锁定单例模式(volatile 应用)
(1)问题代码(未用 volatile)
(2)修复方案(volatile 修饰 instance)
总结
前言
作为 Java 开发者,我们每天都在编写代码、运行程序。但你是否曾想过,你的代码是如何从.java文件变成可执行程序的?为什么有时程序会突然变慢甚至崩溃?这些问题背后,都离不开 Java 虚拟机的支持。
JVM 就像一位幕后导演,它不直接参与表演(不执行你的代码),但决定了表演的规则、舞台的布置、演员的调度。掌握 JVM 的工作原理,不仅能让你写出更高效的代码,还能在遇到性能问题时快速定位并解决。
一、JVM 简介
JVM(Java Virtual Machine,Java 虚拟机)是通过软件模拟的、具备完整硬件功能的计算机系统,运行在完全隔离的环境中,是 Java 程序跨平台运行的核心基础。
与 VMware、VirtualBox 等通用虚拟机不同,JVM 是一款 “定制化的虚拟计算机”,核心差异体现在指令集模拟上:
- VMware/VirtualBox:模拟物理 CPU 的完整指令集,包含大量寄存器,可运行任意操作系统。
- JVM:仅模拟 Java 字节码的指令集,仅保留 PC 寄存器(程序计数器),其他寄存器均被裁剪,专门为执行 Java 程序设计。
JVM 的核心价值是 “一次编译,到处执行”——Java 源代码编译为字节码(.class 文件)后,无需针对不同操作系统重新编译,可在任何安装了对应 JVM 的平台上运行,屏蔽了底层硬件和操作系统的差异。
二、JVM 发展史
自 1996 年 Java 1.0 发布以来,JVM 经历了多代演进,不同厂商推出了各具特色的实现,主流版本如下:
1. Sun Classic VM(1996 年,JDK1.0)
- 世界上第一款商业 JVM,伴随 Java 1.0 诞生,是 Java 早期生态的核心支撑。
- 核心特性:内部仅提供解释器,无内置编译器(JIT);若需使用 JIT 编译器,需额外外挂,且一旦启用 JIT,将完全接管执行系统,解释器不再工作(二者无法协同)。
- 命运:JDK1.4 时被完全淘汰,目前 HotSpot 虚拟机中仍内置了该版本的兼容实现。
2. Exact VM(JDK1.2)
- 为解决 Classic VM 的性能问题而生,是现代高性能 JVM 的雏形。
- 核心特性:支持热点探测(识别高频执行的 “热点代码”,编译为字节码加速执行);实现了解释器与编译器的混合工作模式,兼顾启动速度和运行效率。
- 命运:仅在 Solaris 平台短暂使用,其他平台仍沿用 Classic VM,最终因兼容性和生态问题被 HotSpot 替代。
3. HotSpot VM(当前主流)
- 历史渊源:最初由 Longview Technologies 公司设计,1997 年被 Sun 收购,2009 年随 Sun 被 Oracle 收购;JDK1.3 时成为默认虚拟机,至今占据绝对市场地位。
- 核心特性:名称源于 “热点代码探测技术”—— 通过计数器识别最具编译价值的代码,触发即时编译(JIT)或栈上替换;编译器与解释器协同工作,在程序响应时间和执行性能间取得最佳平衡。
- 应用场景:Sun/Oracle JDK 和 OpenJDK 的默认虚拟机,覆盖服务器、桌面、移动端、嵌入式等全场景。
4. JRockit(专注服务器端)
- 核心定位:专为服务器端应用优化,不关注启动速度,仅保留 JIT 编译器,无解释器实现,所有代码均通过 JIT 编译后执行。
- 核心优势:运行速度快,被称为 “世界上最快的 JVM”,部分场景性能提升超 70%;提供 JRockit Real Time(面向延迟敏感型应用,响应时间达毫秒 / 微秒级)和 MissionControl(低开销监控分析工具)。
- 命运:2008 年随 BEA 被 Oracle 收购,Oracle 在 JDK8 中完成其与 HotSpot 的整合,将 JRockit 的优秀特性移植到 HotSpot 中。
5. J9 VM(IBM)
- 全称:IBM Technology for Java Virtual Machine(IT4J),内部代号 J9。
- 核心定位:与 HotSpot 类似,是多用途 JVM,适用于服务器端、桌面应用、嵌入式等场景,广泛用于 IBM 的各类 Java 产品。
- 核心优势:号称 “世界上最快的 Java 虚拟机”,在 IBM 自有产品上稳定性极强。
- 命运:2017 年 IBM 将其开源,命名为 OpenJ9,交由 Eclipse 基金会管理,更名为 Eclipse OpenJ9。
6. Taobao JVM(国产深度定制)
- 研发背景:由阿里 AliJVM 团队基于 OpenJDK HotSpot 开发,是国内首个开源的高性能服务器级 JVM,为阿里云计算、金融、电商等高并发场景量身定制。
- 核心特性:
- GCIH(GC invisible heap)技术:实现 “堆外存储”,将长生命周期对象移至堆外,GC 无法管理,降低 GC 回收频率、提升回收效率。
- 跨进程对象共享:GCIH 中的对象可在多个 JVM 进程间共享,节省内存开销。
- 优化 JNI 调用:通过 crc32 指令实现 JVM intrinsic,降低 JNI 调用开销。
- 硬件适配:针对 Intel CPU 优化,牺牲部分兼容性换取极致性能。
- ZenGC:专为大数据场景设计的垃圾收集器。
- 应用场景:已全面应用于淘宝、天猫等阿里核心产品,替换了 Oracle 官方 JVM。
关键补充:JVM 与《Java 虚拟机规范》
上述所有 JVM 实现(HotSpot、J9、Taobao JVM 等)均需遵循《Java 虚拟机规范》——Oracle 发布的 Java 领域权威著作,详细定义了 JVM 的组成、指令集、内存模型等核心规范,确保不同厂商的 JVM 能正确执行 Java 字节码。本文后续内容均以 HotSpot(Oracle JDK 默认虚拟机)为核心展开。
三、JVM 执行流程
JVM 的核心职责是将 Java 字节码转换为底层系统指令并执行,完整流程涉及四大核心组件,执行步骤如下:
1. 核心组件
- 类加载器(ClassLoader):负责加载字节码文件(.class)到内存。
- 运行时数据区(Runtime Data Area):存储类信息、对象实例、线程状态等数据。
- 执行引擎(Execution Engine):将字节码翻译为底层系统指令,交由 CPU 执行。
- 本地库接口(Native Interface):调用 C/C++ 等本地方法库,实现 Java 无法直接完成的底层功能(如操作系统交互、硬件操作)。
2. 完整执行流程
四、运行时数据区(内存布局)
运行时数据区是 JVM 在运行过程中分配和管理内存的区域,与 “Java 内存模型(JMM)” 是完全不同的概念 —— 前者是 JVM 的物理内存划分,后者是并发编程的内存访问规范。运行时数据区分为 5 个部分,按 “线程共享 / 私有” 可分为两类:
| 线程共享 | 堆、方法区(元空间) | 所有线程共用,生命周期与 JVM 一致,需垃圾回收 |
| 线程私有 | 虚拟机栈、本地方法栈、程序计数器 | 每个线程独立拥有,生命周期与线程一致,无需垃圾回收 |
1. 堆(线程共享)
- 核心作用:存储 Java 程序中创建的所有对象实例(包括数组),是 JVM 内存中最大的区域,也是垃圾回收的主要目标。
- 内存参数:通过 JVM 参数配置大小,-Xms指定最小启动内存,-Xmx指定最大运行内存(如-Xms20m -Xmx20m表示堆内存固定为 20MB)。
- 区域划分:
- 新生代(Young 区):存储新建对象,分为 1 个 Eden 区和 2 个 Survivor 区(S0/S1,也叫 From/To 区),默认比例为 Eden:S0:S1=8:1:1。
- 老年代(Old 区):存储经过多次垃圾回收后仍存活的对象、大对象(超过新生代阈值的对象)。
- 垃圾回收逻辑:新生代触发 Minor GC(复制算法),老年代触发 Full GC(标记 – 整理算法);Minor GC 时,Eden 区存活对象复制到空闲的 Survivor 区,清理 Eden 和已使用的 Survivor 区;对象在 Survivor 区复制次数达到阈值(默认 15 次,由MaxTenuringThreshold参数控制)后,晋升到老年代。
2. 虚拟机栈(线程私有)
- 核心作用:描述 Java 方法的执行内存模型,每个方法执行时都会创建一个 “栈帧”,栈帧入栈(方法调用)和出栈(方法返回)的过程即方法的执行过程。
- 生命周期:与线程一致,线程启动时创建,线程终止时销毁,不存在线程安全问题。
- 栈帧组成:
- 局部变量表:存储方法参数和局部变量,包含 8 大基本数据类型、对象引用(地址指针),内存空间在编译期间确定,执行时不可修改。
- 操作数栈:方法执行时的临时数据存储区,通过入栈、出栈操作完成运算(如a+b需先将 a、b 入栈,执行加法后将结果入栈)。
- 动态连接:指向运行时常量池的方法引用,用于将符号引用转换为直接引用(支持方法重写等动态特性)。
- 方法返回地址:存储方法执行完毕后返回的 PC 寄存器地址,确保线程能恢复到之前的执行位置。
- 关键概念:“线程私有” 指每个线程拥有独立的虚拟机栈,线程切换时无需担心栈数据冲突,因为处理器同一时刻仅执行一条线程的指令。
3. 本地方法栈(线程私有)
- 核心作用:与虚拟机栈功能类似,区别在于虚拟机栈为 Java 方法服务,本地方法栈为 Native 方法(由 C/C++ 实现,被 Java 调用的方法)服务。
- 生命周期:与线程一致,线程终止时销毁。
- 异常:若本地方法执行时栈深度超过限制,会抛出 StackOverflowError;若扩展栈时无法申请到足够内存,会抛出 OutOfMemoryError。
4. 程序计数器(线程私有)
- 核心作用:记录当前线程执行的字节码行号指示器,相当于线程的 “执行进度条”。
- 具体逻辑:
- 若线程执行 Java 方法,计数器存储当前执行的字节码指令地址。
- 若线程执行 Native 方法,计数器值为空(Undefined)。
- 核心特点:JVM 规范中唯一没有规定 OutOfMemoryError 的区域 —— 内存占用极小,且生命周期与线程一致,无需额外内存分配。
5. 方法区(线程共享)
- 核心作用:存储被虚拟机加载的类信息(类名、父类、接口、字段、方法)、常量、静态变量、即时编译器编译后的代码等数据。
- 实现差异:
- JDK7 及之前:称为 “永久代(PermGen)”,属于 JVM 内存的一部分,大小通过-XX:PermSize和-XX:MaxPermSize配置。
- JDK8 及之后:改为 “元空间(Metaspace)”,使用本地内存(操作系统内存),大小不再受 JVM 内存限制,仅受本地内存大小约束。
- 关键变化(JDK8):
- 字符串常量池从永久代移至堆中(减少永久代内存溢出风险)。
- 静态变量、类元信息仍存储在元空间。
- 运行时常量池:方法区的一部分,存储字面量(字符串、final 常量、基本数据类型值)和符号引用(类全限定名、字段 / 方法名称及描述符),是字节码文件中 “常量池表” 的运行时体现。
6. 内存溢出异常(OOM)详解
运行时数据区的各区域均可能出现内存溢出,不同区域的异常场景和排查方式不同:
(1)Java 堆溢出(java.lang.OutOfMemoryError: Java heap space)
- 触发条件:不断创建对象,且 GC Roots 到对象存在可达路径(避免被 GC 回收),当对象数量超过堆最大容量时触发。
- 测试配置:JVM 参数-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError(堆固定 20MB,溢出时生成内存快照)。
- 测试代码: public class HeapOOMTest {
static class OOMObject {}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject()); // 无限创建对象,无法回收
}
}
} - 排查思路:通过 MAT 等工具分析内存快照,判断是 “内存泄漏”(对象无用但无法回收)还是 “内存溢出”(对象确实需要存活,但堆内存不足);前者修复代码释放引用,后者增大-Xmx参数。
(2)虚拟机栈 / 本地方法栈溢出
HotSpot 将虚拟机栈和本地方法栈合二为一,溢出分为两种情况:
- StackOverflowError:线程请求的栈深度超过虚拟机允许的最大深度(单线程递归过深)。
- 测试配置:-Xss128k(减小栈容量,易触发溢出)。
- 测试代码: public class StackOverflowTest {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak(); // 无限递归
}
public static void main(String[] args) {
StackOverflowTest test = new StackOverflowTest();
try {
test.stackLeak();
} catch (Throwable e) {
System.out.println("栈深度:" + test.stackLength);
throw e;
}
}
}
- OutOfMemoryError:虚拟机扩展栈时无法申请到足够内存(多线程创建过多,耗尽系统内存)。
- 测试代码: public class StackOOMTest {
public void dontStop() {
while (true) {}
}
public void stackLeakByThread() {
while (true) {
new Thread(() -> dontStop()).start(); // 无限创建线程
}
}
public static void main(String[] args) {
new StackOOMTest().stackLeakByThread();
}
} - 注意:该代码会耗尽系统内存,运行前需保存工作。
- 测试代码: public class StackOOMTest {
- 排查思路:StackOverflowError 需减少递归深度或增大-Xss;OutOfMemoryError 需减少线程数,或通过减小堆内存、栈容量换取更多线程创建空间。
(3)元空间溢出(java.lang.OutOfMemoryError: Metaspace)
- 触发条件:加载过多类(如动态生成类、依赖过多 jar 包),元空间(本地内存)不足。
- 测试配置:-XX:MaxMetaspaceSize=10m(限制元空间最大 10MB)。
- 排查思路:增大-XX:MaxMetaspaceSize参数,或减少不必要的类加载(如清理冗余依赖、避免动态类滥用)。
五、类加载机制
类加载是 JVM 将字节码文件转换为可执行代码的过程,核心包括 “类加载流程”“双亲委派模型”“破坏双亲委派模型的场景” 三部分。
1. 类的生命周期
一个类从加载到卸载的完整生命周期为:加载 → 连接(验证→准备→解析) → 初始化 → 使用 → 卸载。其中 “加载、验证、准备、解析、初始化” 是类加载的核心流程,顺序固定。
2. 类加载核心流程
(1)加载(Loading)
- 核心任务:
- 通过类的全限定名(如java.lang.String)获取定义该类的二进制字节流(可从.class 文件、网络、内存等来源获取)。
- 将字节流的静态存储结构转换为方法区的运行时数据结构(类元信息)。
- 在内存中生成一个代表该类的 java.lang.Class 对象,作为方法区中该类数据的访问入口(存储在堆中)。
- 关键说明:“加载” 是 “类加载(Class Loading)” 的第一步,二者不可混淆 ——“类加载” 是完整流程(加载→连接→初始化),“加载” 仅指上述三步操作。
(2)验证(Verification)
- 核心目的:确保字节码文件的信息符合《Java 虚拟机规范》,避免恶意或无效字节码危害 JVM 安全。
- 验证内容:
- 文件格式验证:验证字节码文件的魔数、版本号、常量池格式等(确保是合法的.class 文件)。
- 字节码验证:验证字节码指令的语义合法性(如避免非法跳转、类型转换错误)。
- 符号引用验证:验证常量池中的符号引用(类、字段、方法引用)是否有效(如引用的类是否存在)。
(3)准备(Preparation)
- 核心任务:为类中静态变量(被static修饰的变量)分配内存,并设置 “默认初始值”(而非代码中指定的初始值)。
- 示例:若类中有public static int value = 123;,准备阶段会为value分配内存,设置默认值 0(int 类型默认值),123的赋值会在初始化阶段执行。
- 特殊情况:若静态变量被final修饰(public static final int value = 123;),准备阶段会直接设置为 123——final静态变量是 “常量”,编译时已确定值,存储在常量池。
(4)解析(Resolution)
- 核心任务:将常量池中的 “符号引用” 替换为 “直接引用”。
- 概念区分:
- 符号引用:用字符串描述目标(如Ljava/lang/String;表示 String 类),与内存地址无关。
- 直接引用:指向目标的内存地址(如对象指针、方法入口地址),可直接访问目标。
- 解析对象:类、接口、字段、方法、方法类型等。
(5)初始化(Initialization)
- 核心任务:执行类构造器<clinit>()方法,完成静态变量的赋值(代码中指定的初始值)和静态代码块的执行。
- 关键细节:
- <clinit>()方法由编译器自动生成,整合了静态变量赋值语句和静态代码块(按代码顺序执行)。
- 若类中无静态变量和静态代码块,编译器不会生成<clinit>()方法。
- 父类的<clinit>()方法会先于子类执行(保证父类静态变量初始化完成)。
- 初始化阶段是类加载流程中唯一由应用程序主导的阶段,仅在类被首次主动使用时触发(如创建对象、调用静态方法、访问静态变量)。
3. 双亲委派模型
(1)类加载器层次结构
JVM 中存在多层类加载器,自 JDK1.2 以来形成固定架构:
- 启动类加载器(Bootstrap ClassLoader):最顶层,由 C++ 实现,是 JVM 的一部分;加载 JDK 核心类库($JAVA_HOME/lib目录下的 jar 包,如 rt.jar)。
- 扩展类加载器(Extension ClassLoader):由 Java 实现,加载$JAVA_HOME/lib/ext目录下的扩展类库。
- 应用程序类加载器(Application ClassLoader):由 Java 实现,也称系统类加载器;加载 CLASSPATH 指定的类(应用程序代码、第三方 jar 包)。
- 自定义类加载器:继承java.lang.ClassLoader,由开发者实现,用于加载特殊来源的类(如加密字节码、网络字节码)。
(2)双亲委派模型的工作流程
“双亲委派” 是类加载的核心规则:当一个类加载器收到类加载请求时,不会直接加载,而是先将请求委派给父类加载器;父类加载器若无法加载(搜索范围中无该类),子类加载器才会尝试自己加载。流程如下:
(3)双亲委派模型的核心优势
4. 破坏双亲委派模型的案例(SPI 机制:JDBC)
双亲委派模型虽有优势,但在某些场景下需 “破坏” 以满足功能需求,最典型的案例是 Java 的 SPI(Service Provider Interface)机制,以 JDBC 为例:
(1)问题背景
- JDBC 的核心接口(java.sql.Driver、DriverManager)定义在 JDK 的 rt.jar 包中,由启动类加载器加载。
- 数据库驱动(如 MySQL 的com.mysql.cj.jdbc.Driver)是第三方实现,存在于 CLASSPATH 中的 mysql-connector-java.jar 包,需由应用程序类加载器加载。
- 按双亲委派模型,启动类加载器加载的DriverManager无法访问应用程序类加载器加载的第三方驱动(父类加载器无法访问子类加载器加载的类),导致驱动无法被调用。
(2)解决方案:线程上下文类加载器
DriverManager通过 “线程上下文类加载器”(Thread.currentThread ().getContextClassLoader ())破坏双亲委派模型,核心流程如下:
(3)核心源码(DriverManager)
private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized (DriverManager.class) {
if (callerCL == null) {
// 获取线程上下文类加载器(默认是应用程序类加载器)
callerCL = Thread.currentThread().getContextClassLoader();
}
}
// 用callerCL加载第三方驱动
for (DriverInfo aDriver : registeredDrivers) {
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
return aDriver.driver.connect(url, info);
} catch (SQLException ex) {
// 异常处理
}
}
}
throw new SQLException("No suitable driver found");
}
(4)总结
线程上下文类加载器的本质是 “逆向委派”—— 允许父类加载器通过子类加载器加载类,打破了双亲委派模型 “父类加载器优先” 的规则,解决了 SPI 机制中核心类库与第三方实现的协作问题。
六、垃圾回收(GC)
垃圾回收是 JVM 自动回收堆和方法区中无用对象的过程,核心目标是释放内存,避免内存泄漏。GC 的核心流程是 “判断对象是否存活 → 回收无用对象”,涉及 “对象存活判断算法”“垃圾回收算法”“垃圾收集器” 三部分。
1. 对象存活判断算法
GC 前需先判断对象是否 “死亡”(无用),主流 JVM 采用 “可达性分析算法”,辅助以 “引用计数法”(已淘汰)。
(1)引用计数法(淘汰)
- 核心逻辑:给每个对象添加引用计数器,有引用时计数器 + 1,引用失效时 – 1;计数器为 0 时,对象死亡。
- 优点:实现简单,判定效率高。
- 缺点:无法解决 “循环引用” 问题 —— 两个对象互相引用,计数器均为 1,但实际已无用,无法被回收。
- 示例(循环引用): public class ReferenceCountTest {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB]; // 占用内存,便于观察GCpublic static void testGC() {
ReferenceCountTest a = new ReferenceCountTest();
ReferenceCountTest b = new ReferenceCountTest();
a.instance = b; // a引用b
b.instance = a; // b引用a
a = null; // 取消引用
b = null; // 取消引用
System.gc(); // 触发GC
}public static void main(String[] args) {
testGC();
}
} - 运行结果:GC 日志显示6092K->856K,说明循环引用的对象被回收,证明 HotSpot 未使用引用计数法。
(2)可达性分析算法(主流)
- 核心逻辑:以 “GC Roots” 为起始点,向下搜索引用链;若对象到 GC Roots 无任何引用链相连(不可达),则判定为死亡对象。
- GC Roots 的可选对象(Java 语言中):
- 虚拟机栈(栈帧本地变量表)中引用的对象(如方法参数、局部变量)。
- 方法区中类静态属性引用的对象(如static Object obj = new Object())。
- 方法区中常量引用的对象(如final Object obj = new Object())。
- 本地方法栈中 JNI(Native 方法)引用的对象。
- 示例:对象 Object5-Object7 虽互相引用,但到 GC Roots 不可达,被判定为可回收对象。
(3)引用的扩展分类(JDK1.2)
JDK1.2 后,Java 对 “引用” 的概念进行扩展,分为四级(强度递减),影响对象的存活判定:
2. 垃圾回收算法
垃圾回收算法是 GC 的核心思想,不同算法适用于不同场景,主流算法包括以下四种:
(1)标记 – 清除算法(基础)
- 核心流程:分为 “标记” 和 “清除” 两步。
- 标记:通过可达性分析,标记所有需要回收的死亡对象。
- 清除:遍历堆内存,回收所有被标记的对象,释放内存空间。
- 优点:实现简单,无需移动对象。
- 缺点:
- 效率低:标记和清除过程均需遍历所有对象,耗时较长。
- 内存碎片:回收后产生大量不连续的内存碎片,后续分配大对象时,可能因无法找到足够连续内存而提前触发 GC。
(2)复制算法(新生代首选)
- 核心流程:将可用内存划分为大小相等的两块(A 区和 B 区),每次仅使用一块。
- 标记:标记 A 区中存活的对象。
- 复制:将 A 区存活对象复制到 B 区,按顺序排列(无内存碎片)。
- 清除:清空 A 区,下次使用 B 区,重复上述流程。
- 优点:实现简单,运行高效,无内存碎片。
- 缺点:内存利用率低(仅 50%),不适用于对象存活率高的场景(需频繁复制)。
- 优化(HotSpot 新生代实现):新生代中 98% 的对象 “朝生夕死”,无需 1:1 划分内存,而是分为 1 个 Eden 区(大)和 2 个 Survivor 区(小),默认比例 Eden:S0:S1=8:1:1。
- 初始分配:对象优先在 Eden 区创建。
- Minor GC 触发:Eden 区满时,标记 Eden 和 S0 区的存活对象,复制到 S1 区,清空 Eden 和 S0 区。
- Survivor 区切换:下次 Minor GC 时,标记 Eden 和 S1 区的存活对象,复制到 S0 区,清空 Eden 和 S1 区。
- 晋升老年代:对象在 S0 和 S1 区之间复制次数达到阈值(默认 15 次)后,晋升到老年代;若 Survivor 区空间不足,直接晋升老年代(分配担保)。
- 内存利用率:新生代可用内存为 90%(8+1),仅 10% 用于存储存活对象。
(3)标记 – 整理算法(老年代首选)
- 核心流程:结合 “标记 – 清除” 和 “复制” 的优点,适用于对象存活率高的老年代。
- 标记:标记所有死亡对象。
- 整理:将所有存活对象向内存一端移动,紧凑排列。
- 清除:清空存活对象另一端的内存空间。
- 优点:无内存碎片,内存利用率高。
- 缺点:效率低于复制算法(需移动对象,额外消耗 CPU 资源)。
(4)分代收集算法(实际应用)
- 核心思想:根据对象存活周期的不同,将堆分为新生代和老年代,采用不同的回收算法,兼顾效率和内存利用率。
- 新生代:对象存活时间短、存活率低,采用复制算法(高效、无碎片)。
- 老年代:对象存活时间长、存活率高,采用标记 – 整理算法(无碎片、高利用率)。
- 补充:方法区的回收(元空间):回收废弃常量和无用类(类的所有实例已回收、类加载器已回收、无引用指向类对象),频率较低。
面试题:Minor GC 与 Full GC 的区别
| 触发区域 | 新生代(Eden/Survivor) | 老年代(可伴随 Minor GC) |
| 触发条件 | Eden 区满 | 老年代满、元空间满、调用 System.gc () 等 |
| 回收算法 | 复制算法 | 标记 – 整理算法(或标记 – 清除) |
| 执行效率 | 快(对象存活率低,复制量小) | 慢(对象存活率高,需整理),约为 Minor GC 的 10 倍 |
| 影响范围 | 仅新生代线程,影响小 | 全堆回收,影响大(STW 时间长) |
3. 垃圾收集器(HotSpot)
垃圾收集器是垃圾回收算法的具体实现,HotSpot 虚拟机提供了多款收集器,适用于不同场景(吞吐量、低停顿),核心收集器如下:
(1)核心概念
- 并行(Parallel):多条 GC 线程同时工作,用户线程暂停(STW)。
- 并发(Concurrent):GC 线程与用户线程同时执行(交替运行),用户线程无需长时间暂停。
- 吞吐量:CPU 用于运行用户代码的时间 / CPU 总消耗时间(吞吐量 = 用户代码时间 / (用户代码时间 + GC 时间))。
(2)收集器分类与特性
| Serial | 新生代 | 单线程,STW | 复制算法 | 串行 | 实现简单,单 CPU 效率高 | 多 CPU 场景效率低,STW 时间长 |
| ParNew | 新生代 | 多线程(Serial 多线程版),STW | 复制算法 | 并行 | 支持与 CMS 配合,多 CPU 效率高 | 单 CPU 场景有线程交互开销 |
| Parallel Scavenge | 新生代 | 多线程,吞吐量优先 | 复制算法 | 并行 | 自适应调节策略,可控制吞吐量和停顿时间 | 不支持与 CMS 配合 |
| Serial Old | 老年代 | 单线程,STW | 标记 – 整理 | 串行 | 实现简单,Client 模式默认 | 多 CPU 场景效率低 |
| Parallel Old | 老年代 | 多线程,吞吐量优先 | 标记 – 整理 | 并行 | 与 Parallel Scavenge 搭配,吞吐量最优 | 停顿时间较长 |
| CMS(Concurrent Mark Sweep) | 老年代 | 并发收集,低停顿 | 标记 – 清除 | 并发 | 响应速度快,STW 时间短 | 占用 CPU 资源,产生内存碎片,无法处理浮动垃圾 |
| G1(Garbage First) | 全区域 | 分区收集,兼顾吞吐量与低停顿 | 标记 – 整理 + 复制 | 并发 + 并行 | 无内存碎片,可预测停顿时间 | 实现复杂,小内存场景效率低 |
(3)重点收集器详解
① CMS 收集器(低停顿首选)
- 核心目标:获取最短回收停顿时间,适用于 B/S 系统、互联网应用等对响应速度敏感的场景。
- 运作流程(4 步):
- 初始标记(Initial Mark):标记 GC Roots 直接关联的对象,速度快,需 STW。
- 并发标记(Concurrent Mark):遍历引用链,标记所有死亡对象,与用户线程并发执行,无需 STW。
- 重新标记(Remark):修正并发标记期间因用户线程执行导致标记变动的对象,需 STW(停顿时间短于初始标记)。
- 并发清除(Concurrent Sweep):回收死亡对象,与用户线程并发执行,无需 STW。
- 核心缺点:
- CPU 敏感:并发阶段占用 CPU 资源,导致应用程序吞吐量下降(默认 GC 线程数 = (CPU 数 + 3)/4)。
- 浮动垃圾:并发清除阶段用户线程仍产生新垃圾,无法在本次 GC 中回收,需预留内存空间(避免 OOM)。
- 内存碎片:基于标记 – 清除算法,产生大量碎片,可能导致提前触发 Full GC。
② G1 收集器(全区域首选)
- 核心定位:面向服务端应用,旨在替代 CMS,兼顾吞吐量和低停顿,适用于大堆内存场景(如 10GB 以上)。
- 核心特性:
- 分区收集:将堆划分为多个大小相等的 Region(区域),每个 Region 可动态标记为 Eden、Survivor、Old、Humongous(存储大对象,超过 Region 大小 50%)。
- 优先回收:基于 “Garbage First” 策略,优先回收垃圾最多的 Region(垃圾回收率最高)。
- 无内存碎片:整体采用标记 – 整理算法,局部(Region 之间)采用复制算法,回收后自动压缩内存。
- 可预测停顿时间:通过参数-XX:MaxGCPauseMillis设置最大停顿时间,G1 会动态调整回收 Region 数量,确保停顿时间不超标。
- 运作流程(4 步):
- 初始标记:标记 GC Roots 直接关联的对象,与 Minor GC 同步执行,需 STW。
- 并发标记:遍历引用链,标记死亡对象,与用户线程并发执行;同时计算每个 Region 的垃圾回收率。
- 最终标记(Remark):修正并发标记期间的标记变动,采用 SATB(快照原子性)算法,STW 时间短。
- 筛选回收(Clean Up/Copy):筛选垃圾回收率高的 Region,复制存活对象到空 Region,清空原 Region;与 Minor GC 同步执行,需 STW。
4. 一个对象的一生(总结)
七、Java 内存模型(JMM)
JMM(Java Memory Model)是 JVM 定义的内存访问规范,用于屏蔽不同硬件和操作系统的内存访问差异,确保 Java 程序在多线程环境下的并发安全性(原子性、可见性、有序性)。
1. 核心概念:主内存与工作内存
JMM 定义了线程与内存的交互规则,将内存分为两类:
- 主内存:存储所有线程共享的变量(实例字段、静态字段、数组元素),是物理内存的抽象。
- 工作内存:每个线程私有的内存区域,存储主内存变量的副本拷贝;线程对变量的所有操作(读取、赋值)均在工作内存中执行,无法直接访问主内存。
2. 内存间交互操作(8 种原子操作)
JMM 定义了 8 种原子操作,确保主内存与工作内存之间的数据同步,操作需满足原子性、不可分割:
- lock(锁定):作用于主内存变量,标记为线程独占状态。
- unlock(解锁):作用于主内存变量,释放锁定状态,允许其他线程锁定。
- read(读取):作用于主内存变量,将变量值传输到线程工作内存。
- load(载入):作用于工作内存变量,将 read 操作获取的变量值存入工作内存副本。
- use(使用):作用于工作内存变量,将变量值传递给执行引擎(如运算)。
- assign(赋值):作用于工作内存变量,将执行引擎的结果赋给变量副本。
- store(存储):作用于工作内存变量,将变量值传输到主内存。
- write(写入):作用于主内存变量,将 store 操作获取的变量值存入主内存。
3. JMM 的三大核心特性
并发程序正确执行需保证三大特性,JMM 通过上述操作和关键字(volatile、synchronized)实现:
(1)原子性
- 定义:一个操作或多个操作要么全部执行且不被打断,要么全部不执行。
- JMM 保证:read、load、assign、use、store、write 等基本操作原子性;更大范围的原子性(如i++)需通过 synchronized或java.util.concurrent.locks锁实现。
- 示例:i++并非原子操作(分为读取 i、i+1、赋值给 i 三步),多线程环境下可能出现线程安全问题。
(2)可见性
- 定义:当一个线程修改共享变量的值,其他线程能立即感知到该修改。
- 实现方式:
- volatile:修饰的变量修改后,会立即同步到主内存,其他线程读取时直接从主内存加载(禁用工作内存缓存)。
- synchronized:解锁前,线程需将工作内存中变量的修改同步到主内存;加锁时,线程清空工作内存副本,从主内存重新加载变量(happen-before 原则)。
- final:final 修饰的变量初始化完成后,其值对所有线程可见(不可修改)。
(3)有序性
- 定义:线程内操作按代码顺序执行(线程内串行);线程间观察时,操作可能无序(指令重排序、工作内存与主内存同步延迟)。
- 指令重排序:编译器或 CPU 为优化性能,在不影响单线程执行结果的前提下,调整指令执行顺序(如a=1; b=2可能被重排为b=2; a=1)。
- 实现方式:
- volatile:禁止指令重排序(修饰的变量前后的指令不可重排)。
- synchronized:保证同一时刻只有一个线程执行同步块,间接保证有序性。
- happen-before 原则(先天有序性):无需任何关键字,JMM 默认保证的有序性,包括:
- 程序次序规则:线程内代码按书写顺序执行。
- 锁定规则:unlock 操作先行发生于后面对同一锁的 lock 操作。
- volatile 变量规则:对 volatile 变量的写操作先行发生于读操作。
- 传递规则:A 先行发生于 B,B 先行发生于 C,则 A 先行发生于 C。
- 线程启动规则:Thread.start () 先行发生于线程内所有操作。
- 线程中断规则:interrupt () 先行发生于线程检测到中断事件。
- 线程终结规则:线程内所有操作先行发生于线程终止检测(如 join ())。
- 对象终结规则:对象初始化完成先行发生于 finalize () 方法。
4. volatile 关键字详解
volatile是 JMM 提供的最轻量级同步机制,仅保证可见性和有序性,不保证原子性。
(1)核心特性
(2)不保证原子性的示例
public class VolatileAtomicTest {
public static volatile int num = 0;
public static void increase() {
num++; // 非原子操作,volatile无法保证
}
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
increase();
}
});
threads[i].start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(num); // 结果小于1000,存在线程安全问题
}
}
- 原因:num++分为读取 num、num+1、赋值给 num 三步,volatile 仅保证读取时获取最新值,但运算过程中可能被其他线程修改,导致结果覆盖。
(3)volatile 的适用场景
需满足以下条件之一:
- 示例(正确场景): volatile boolean shutdownRequested;
public void shutdown() {
shutdownRequested = true; // 单一线程修改
}
public void work() {
while (!shutdownRequested) { // 多线程读取
// 执行任务
}
}
(4)禁止指令重排序的作用
- 示例: // x、y为非volatile变量,flag为volatile变量
x = 2; // 语句1
y = 0; // 语句2
flag = true; // 语句3
x = 4; // 语句4
y = -1; // 语句5 - 规则:语句 3(volatile 写)不能重排到语句 1、2 之前,也不能重排到语句 4、5 之后;语句 1、2 之间,语句 4、5 之间可重排。
- 意义:确保执行到语句 3 时,语句 1、2 的修改已完成且可见,避免多线程环境下因重排导致的逻辑错误。
5. 双重检查锁定单例模式(volatile 应用)
(1)问题代码(未用 volatile)
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Singleton(); // 非原子操作
}
}
}
return instance;
}
}
- 问题:instance = new Singleton()分为三步:1. 分配内存;2. 初始化成员变量;3. 赋值给 instance(instance 非 null)。JIT 编译可能重排为 1→3→2,导致线程 B 在第二次检查时看到 instance 非 null,但未初始化完成,使用时抛出异常。
(2)修复方案(volatile 修饰 instance)
public class Singleton {
private volatile static Singleton instance = null; // 禁止重排
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); // 重排被禁止,确保1→2→3执行
}
}
}
return instance;
}
}
- 原理:volatile 禁止instance = new Singleton()的指令重排,确保对象初始化完成后,才将 instance 赋值为非 null,避免线程安全问题。
总结
内存布局:堆、栈、方法区各司其职
类加载:双亲委派确保安全,SPI 等场景需要打破规则
垃圾回收:分代收集是核心思想,不同场景选择不同收集器
内存模型:JMM 为并发编程提供保障
网硕互联帮助中心





评论前必须登录!
注册