02.Java虚拟机
# 01.JVM 体系结构
# 0、Java虚拟机概述
Java虚拟机(JVM)是一个虚拟计算机,用于执行Java字节码,屏蔽了底层操作系统的差异,实现了跨平台运行JVM 负责字节码加载、验证、执行和优化,包括 JIT 编译和垃圾回收等机制JVM 通过类加载器、堆栈管理、GC 机制等提供自动化的内存管理,减少手动管理的负担举例说明:假设有一个简单的Java程序
1. 编译:
javac HelloWorld.java生成HelloWorld.class字节码文件2. 运行:
java HelloWorld启动JVM,加载并执行字节码3. 执行过程:
- 类加载:JVM加载
HelloWorld.class - 字节码验证:确保字节码合法
- 解释/编译:JVM解释或编译字节码为机器码
- 执行:调用
System.out.println输出 "Hello, World!" - 垃圾回收:程序结束后,JVM回收内存
- 类加载:JVM加载
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } }1
2
3
4
5
# 1、类加载子系统
# 1) 类加载过程 七阶段
- JVM 的类加载子系统负责从
.class文件中加载字节码,并将其转换为 JVM 运行时可以理解的 Class 对象- 类的生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,并由不同的 类加载器 负责加载
类加载过程(加载、验证、准备、解析、初始化、使用、卸载)① 加载:读取 class 字节码,生成Class对象Class<?> clazz = Class.forName("com.example.MyClass");1
② 验证:检查字节码格式、元数据、字节码合法性③ 准备:为类变量分配内存,初始化默认值(不执行代码)class Example { // JVM 只会给 a 赋默认值 0,真正赋值 10 会在初始化阶段进行 static int a = 10; }1
2
3
4
④ 解析:符号引用转换为直接引用- 符号引用(如
com/example/MyClass)存储的,而不是直接的内存地址 - 直接引用 解析后,JVM 将符号引用替换为实际内存地址,便于后续执行
- 符号引用(如
⑤ 初始化:执行<clinit>方法,初始化静态变量和代码块初始化顺序:父类先于子类,静态变量和静态代码块按顺序执行
<clinit>方法只执行一次,且在类的首次主动使用时触发// 准备阶段:a = 0(默认值) // 初始化阶段:执行 static 代码块,a = 20,输出 "Static Block Executed" class Example { static int a = 10; static { a = 20; System.out.println("Static Block Executed"); } }1
2
3
4
5
6
7
8
9
⑥ 使用:类进入运行状态,可创建对象或调用静态方法⑦ 卸载:无引用的Class对象被回收- 当一个
Class没有任何实例且没有被其他类引用,JVM 的 垃圾回收器(GC) 可能会回收该Class对象,释放方法区的内存 - 但在 JVM 运行期间,Bootstrap ClassLoader 加载的类不会被卸载
- 当一个
# 2) 类加载器
- JVM 采用“父委托机制”(Parent Delegation Model)来加载类,避免重复加载,提升安全性
JVM 内置类加载器
| 类加载器 | 作用 | 代码获取方式 |
|---|---|---|
| Bootstrap ClassLoader | 加载 java.lang 包及核心类(rt.jar / java.base) | 由 C/C++ 实现,非 Java 代码 |
| Extension ClassLoader | 加载 lib/ext/ 目录下的类(扩展类) | sun.misc.Launcher$ExtClassLoader |
| Application ClassLoader | 加载应用程序的 classpath 下的类 | sun.misc.Launcher$AppClassLoader |
自定义类加载器自定义 ClassLoader 适用于 动态加载类(如热部署、插件机制)
class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] bytes = loadClassData(name); // 读取字节码 return defineClass(name, bytes, 0, bytes.length); } }1
2
3
4
5
6
7
# 3) 运行时数据区
JVM 运行时将内存划分为多个区域区域 作用 方法区(Metaspace) 存储类的元信息、静态变量、常量池 堆(Heap) 存储对象实例,GC 主要管理的区域 栈(Stack) 线程私有,存储局部变量和方法调用栈帧 程序计数器(PC Register) 记录当前线程执行的指令地址 本地方法栈(Native Stack) 供 Native 方法(JNI)使
示例(栈与堆)class Example { int x = 10; // 堆内存(Heap) void method() { int y = 5; // 栈内存(Stack) } }1
2
3
4
5
6
# 2、执行引擎
JVM 执行引擎(Execution Engine)负责 将 Java 字节码转换为机器码,并交由 CPU 执行
主要有 三种执行方式
编译方式 运行方式 优势 劣势 适用场景 解释器 逐行解释执行启动快 运行慢 短时间运行的程序、JVM 启动时JIT 编译器 热点代码编译为机器码运行快、动态优化 启动慢 服务器端、高性能应用AOT 编译器 预编译为本地代码启动快 缺乏 JIT 优化 云计算、微服务、嵌入式
1)解释器逐行解释执行Java 字节码,每次运行都需要重新解析相同的代码启动速度快,无需额外的编译步骤适用于一次性执行的代码,如脚本、短时间运行的任务// 逐行读取字节码,每次执行 System.out.println(i) 时都重新解释 // 无法缓存执行结果,导致重复解析相同代码,影响性能 for (int i = 0; i < 1000; i++) { System.out.println(i); }1
2
3
4
5
2)JIT 编译器(即时编译)动态分析代码,识别热点代码(高频调用的代码),将其编译成本地机器码
缓存已编译的机器码,下次调用时可直接执行,避免重复解释
适用于长时间运行的应用,如 Web 服务器、大数据计算、游戏引擎等
缺点
- 初次执行较慢,因为 JIT 需要运行一段时间收集信息,识别热点代码后才会优化
- 占用额外 CPU 资源,运行时进行编译需要 CPU 计算,可能影响其他任务的执行
// JIT 会发现 compute() 方法被频繁调用,将其编译成本地机器码,下次直接执行 for (int i = 0; i < 1000; i++) { compute(); }1
2
3
4
3)AOT 编译器(Ahead-of-Time)- 在程序运行前,将 Java 字节码直接编译为本地机器码,省去 JIT 运行时编译的开销
- 优点:
- 启动速度快,不依赖 JIT,即可获得接近本地代码的执行性能
- 适用于资源受限的环境(如云计算、容器、微服务)
- 缺点:
- 缺乏 JIT 的动态优化能力,可能在长时间运行的应用中不如 JIT 高效
- 需要额外的编译时间和存储空间
- 适用于云计算、微服务、嵌入式系统等需要快速启动的应用
# 02.JVM 内存管理
- 1️⃣
对象创建(类加载、内存分配) - 2️⃣
对象分配(新生代 / 老年代 / 大对象) - 3️⃣
对象访问(句柄 / 直接指针) - 4️⃣
垃圾回收(可达性分析 & GC 选择) - 5️⃣
逃逸分析优化(减少 GC 负担) - 6️⃣
根据对象生命周期决定内存分配策略
# 1、对象创建
1️⃣ 对象创建:JVM 如何创建一个对象?
1)类加载检查- JVM 先检查类是否已加载 到 方法区(Method Area),如果未加载,则触发 类加载(Class Loading)
2)分配内存(在堆中申请对象所需空间)指针碰撞(Bump-the-Pointer):适用于 堆内存连续 的场景,只需移动指针即可分配内存
空闲列表(Free List):适用于 堆内存碎片化 的情况,需要维护一个空闲块列表来寻找合适的内存区域
3)初始化对象默认值初始化(0/false/null)
调用构造方法(Constructor) 进行自定义初始化
示例: 对象 p 的创建流程JVM 检查
Person类是否已加载到 方法区在 堆内存 分配
Person对象(使用指针碰撞或空闲列表)对象头 存储 GC 年龄、类型指针、锁状态等信息
name = "Alice",age = 25赋值,调用构造方法完成初始化public class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } public static void main(String[] args) { Person p = new Person("Alice", 25); } }1
2
3
4
5
6
7
8
9
10
11
12
13
# 2、对象 内存分配
Java 使用 分代回收 机制,内存被划分为
新生代:Eden+Survivor(S0/S1)老年代:存放长期存活的对象永久代:存储类元数据、方法等
新生代 & 老年代 的对象分配规则
新对象优先在 Eden 区分配(默认情况)
Eden 区满了触发 Minor GC,存活的对象移动到 Survivor(S0 / S1)
Survivor 区对象经过 N 次 GC 后(默认 15 次,
-XX:MaxTenuringThreshold可调节),晋升到 老年代大对象(如 1MB+ 的数组)直接进入老年代,避免在 Survivor 复制增加开销(
-XX:PretenureSizeThreshold控制)
# 3、对象的访问方式
JVM 主要有 两种对象访问方式:句柄 vs 直接指针
句柄访问(Handle)
p → 句柄地址 → 对象地址JVM 维护一个 句柄池,对象引用指向 句柄地址,再由句柄找到 实际对象地址
适用于对象频繁移动的 GC 策略(如 G1 GC)
直接指针访问(Direct Pointer)
p → 对象地址引用直接指向对象,提高访问速度
适用于普通 GC(如 Parallel GC、CMS),因为对象位置不经常变化
# 4、垃圾回收(GC)
# 1)可达性分析
JVM 采用 可达性分析算法 判断对象是否存活
即 从 GC Roots 开始遍历可达对象,不可达的对象会被标记为垃圾,等待回收
GC Roots 是指 JVM 认为始终存活的对象
以下几类对象被认为是 GC Roots:
栈中局部变量、静态变量、JNI 引用public class GCRootDemo { private static GCRootDemo staticRef; private GCRootDemo instanceRef; public static void main(String[] args) { // ① 方法栈中的局部变量 GCRootDemo localVar = new GCRootDemo(); // ② 静态变量 staticRef = new GCRootDemo(); // 被可达对象引用,也存活 localVar.instanceRef = new GCRootDemo(); // 触发 GC System.gc(); } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 2)常见垃圾回收算法
- JVM 的 GC 采用 多种垃圾回收算法,适用于不同场景
| GC 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 标记-清除 | 直接清理垃圾对象,不需要额外空间 | 产生大量内存碎片,影响后续内存分配性能 | 老年代 GC |
| 复制 | 无碎片化,分配速度快,适用于新生代对象 | 需要额外 50% 内存,浪费空间 | 新生代(Eden → Survivor) |
| 标记-整理 | 无碎片化,整理后分配效率高 | 效率比复制算法稍低,但比标记-清除高 | 老年代 GC |
| 分代回收 | 结合不同算法,针对不同对象优化回收 | 需要额外的代管理策略 | Java 堆整体 |
① 标记-清除(Mark-Sweep)- 标记:从 GC Roots 开始标记所有可达对象
- 清除:删除不可达对象,释放空间(但会产生内存碎片)
- 缺点:
碎片化问题,回收后内存不连续,可能导致大对象分配失败
② 复制(Copying)- 将存活对象 复制 到另一块新区域
- 清空旧区域,减少碎片化
- 缺点:
内存浪费:需要 额外 50% 空间 存放对象
③ 标记-整理(Mark-Compact)标记存活对象(和标记-清除一样)。
将所有存活对象移动到内存的一端,使得内存连续,避免碎片化
移动后,清理未使用的空间,使得新的对象可以顺利分配
适用场景:
- 适用于 老年代 GC,因为老年代对象生命周期较长,存活率较高,使用 复制算法成本太高(占用过多内存)
④ 分代回收(Generational GC)- 分代回收 是 JVM 主要的 GC 策略
- 大部分对象生命周期很短,应尽快回收(新生代)
- 少部分对象生命周期较长,应该使用更高效的回收方式(老年代)
# 5、逃逸分析
- 在 Java 中,"逃逸"指的是 对象的作用范围超出了当前方法或线程的控制
- 导致
JVM 无法在栈上分配对象,而必须将其分配到堆,从而增加 GC 负担
Java 中的逃逸(Escape)可以分为两种
① 方法逃逸(Method Escape)对象被 方法返回或赋值给方法外部的变量,JVM 无法确定其生命周期,只能分配到堆中public class EscapeExample { private static Object globalObj; public void setGlobalObj() { globalObj = new Object(); // 对象逃逸到全局作用域 } }1
2
3
4
5
6
7
② 线程逃逸(Thread Escape)对象被多个线程共享,JVM 必须确保数据安全性,通常使用 堆分配 + 线程同步public void escapeToThread() { sharedObject = new Object(); // 逃逸到多线程 new Thread(() -> { System.out.println(sharedObject); // 另一个线程访问 }).start(); }1
2
3
4
5
6
# 6、内存分配策略
JVM 采用 分代回收,不同生命周期的对象采用不同策略
- 短生命周期对象 → 新生代(Eden 分配)
- 长生命周期对象 → 老年代(多次 GC 后晋升)
- 大对象 → 直接进入老年代(避免频繁复制)
📌 JVM 调优:
-XX:NewRatio=2(年轻代和老年代比例)-XX:SurvivorRatio=8(Eden 和 Survivor 比例)
# 03.JVM 线程管理与同步
# 1、线程模型
JVM 的线程模型基于 操作系统原生线程,即 Java 的 Thread 实际上是 映射到操作系统的线程,由 CPU 直接调度,这意味着
- 每个 Java 线程几乎都是一个独立的 OS 线程,需要 内核调度,上下文切换成本较高。
- 适用于计算密集型任务,但 当线程数过多时,性能会下降(如频繁切换导致开销大)。
用户线程 vs. 守护线程
- 用户线程:应用程序主动创建的线程,如
Thread、ExecutorService线程池。 - 守护线程:
- JVM 后台运行的线程,比如 GC 线程、JIT 编译器线程
- 当所有用户线程结束,JVM 也会退出,守护线程不会阻止 JVM 关闭
Golang 线程模型(对比 JVM)
- Goroutine 轻量级:Goroutine 不是 OS 线程,而是 由 Go 运行时管理,调度更加灵活
- M:N 线程调度:多个 Goroutine 可复用少量 OS 线程,避免线程过多导致的上下文
# 2、Java 内存模型(JMM)
Java 内存模型(JMM)主要用于 解决 CPU 缓存一致性问题,确保多线程间的 可见性、有序性、原子性
JMM 的基本结构
主内存:所有共享变量存储的位置,所有线程都能访问。工作内存:- 每个线程 有自己的工作内存,它是寄存器、CPU 缓存等的抽象
- 线程只能直接操作自己的工作内存中的变量
happens-before 规则(核心内存可见性规则)
synchronized释放锁前,变量必须刷新到主存(保证可见性)volatile变量的写入对所有线程立即可见(保证可见性)。- 线程
start()之前的操作对新线程可见(保证启动时能看到正确数据)。
# 3、synchronized、volatile 关键字
synchronized关键字可以修饰 方法 和 代码块,用于保证 线程安全:可见性:线程释放锁前,变量修改必须刷新到主内存
原子性:同一时间只有一个线程能持有锁
有序性:锁保证代码块内的指令不会被重排序
public synchronized void method() { // 线程安全方法 } public void method2() { synchronized (this) { // 线程安全代码块 } }1
2
3
4
5
6
7
8
9
volatile用于保证 变量的可见性 和 禁止指令重排序,但 不保证原子性可见性:写入
volatile变量后,JMM 立即刷新到主存,使其他线程可见禁止指令重排序:防止编译器优化导致执行顺序不符合预期
volatile boolean flag = true; public void stop() { flag = false; // 线程修改,立即对其他线程可见 }1
2
3
4
5
# 4、对象的内存可见性
内存可见性问题:
多线程可能会读取 旧值,因为变量更新可能还在 CPU 缓存,尚未刷新到 主存
volatile可以保证变量每次读取都直接从主存获取,避免缓存一致性问题synchronized通过 锁释放时刷写主存,确保线程间变量可见性关联点:
JMM 定义的 happens-before 规则 影响线程同步方式
锁机制和
volatile结合 可提升可见性和性能
# 5、锁优化
① 偏向锁、轻量级锁、重量级锁JVM 在
synchronized执行时,锁可以 动态升级 以提高性能偏向锁适用于单线程访问,若线程竞争升级为轻量级锁
轻量级锁使用 CAS,适用于短暂竞争,而竞争激烈时会升级为 重量级锁
锁类型 适用场景 优点 缺点 偏向锁 线程独占锁 减少 CAS 操作 竞争时需要升级 轻量级锁 线程短暂竞争 采用 CAS 自旋 竞争激烈时性能下降 重量级锁 多线程竞争激烈 线程安全 线程阻塞,切换开销大
② 自旋锁、锁消除、锁粗化自旋锁:
- 线程等待时,不进入阻塞状态,而是不断尝试获取锁,减少上下文切换开销
- JVM 通过
-XX:+UseSpinning启用自旋锁
锁消除:
- JIT 编译时发现 不可能竞争的锁,会自动去掉,减少开销
- 例如局部变量的
synchronized可能会被 JVM 消除
锁粗化:
如果同一线程 频繁加锁/解锁,JVM 可能会 扩大锁范围,减少加锁次数,提高效率
例如:
synchronized(obj) { a++; } synchronized(obj) { b++; } // 会被优化为: synchronized(obj) { a++; b++; }1
2
3
4
5
6
7
8
③ CAS(Compare And Swap)CAS 是无锁并发的核心技术通过
cmpxchg指令 在多核 CPU 直接操作内存,避免线程阻塞适用于 原子变量(
AtomicInteger、AtomicReference)AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // CAS 方式保证原子性1
2关联点:
CAS 与
volatile结合可实现无锁同步,适用于高并发场景CAS 可能导致 ABA 问题(可用
AtomicStampedReference解决)