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
解决)