JVM字节码和类加载
字节码的组成
+——————————+
| 魔数(Magic Number) |
+——————————+
| 版本信息(Version) |
+——————————+
| 常量池(Constant Pool) |
+——————————+
| 访问标志(Access Flags)|
+——————————+
| 类信息(This/ Super) |
+——————————+
| 接口(Interfaces) |
+——————————+
| 字段(Fields) |
+——————————+
| 方法(Methods) |
+——————————+
| 属性(Attributes) |
+——————————+
- 一个简单的HelloWorld程序
1
2
3
4
5
6public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
} - 打开class文件

- 0-3字节(ca fe ba be),表示它是否是class类型的文件
在Java中,所有的.class文件都以魔数ca fe ba be开头,这个魔数的前4个字节用于识别该文件是否为Java类文件,如果这个魔数不匹配,那么Java虚拟机将无法加载该文件。
- 类文件结构

- 主版本号-44 = JDK版本
常量池
- 常量池的组成
当你编写 Java 代码后,编译器会将常量(如字面量、类名、字段名、方法签名等)存入 .class 文件中的常量池表(Constant Pool Table)。
| Tag | 类型 | 描述 |
|---|---|---|
| 1 | CONSTANT_Utf8 | UTF-8编码的字符串 |
| 3 | CONSTANT_Integer | 整型常量 |
| 4 | CONSTANT_Float | 浮点型常量 |
| 5 | CONSTANT_Long | 长整型常量(占两个槽) |
| 6 | CONSTANT_Double | 双精度浮点常量(占两个槽) |
| 7 | CONSTANT_Class | 类或接口的符号引用 |
| 8 | CONSTANT_String | 字符串字面量的引用 |
| 9 | CONSTANT_Fieldref | 字段的符号引用 |
| 10 | CONSTANT_Methodref | 方法的符号引用 |
| 11 | CONSTANT_InterfaceMethodref | 接口方法的引用 |
| 12 | CONSTANT_NameAndType | 字段或方法的名称与描述符 |
| 15 | CONSTANT_MethodHandle | 方法句柄 |
| 16 | CONSTANT_MethodType | 方法类型 |
| 18 | CONSTANT_InvokeDynamic | 动态调用信息 |
- 常量池的特点
- 字符串常量池是运行时常量池的一部分,存储字符串字面量,并支持字符串常量共享。
1
2
3
4
5
6
7public class Test {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "Hello";
System.out.println(s1 == s2); // true,因为指向相同的字符串常量池对象
}
}s1和s2会指向同一地址
- 字符串常量池是运行时常量池的一部分,存储字符串字面量,并支持字符串常量共享。
字节码指令
- 解析 i=i++
1
2
3
4
5
6
7public class HelloWorld {
public static void main(String[] args) {
int i=0;
i=i++;
System.out.println(i);
}
} - 这是一个简单的i=i++,我们来看这个程序的字节码指令
当这个程序开始运行后:1
2
3
4
5
6
7
8
90 iconst_0
1 istore_1
2 iload_1
3 iinc 1 by 1
6 istore_1
7 getstatic #7 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #13 <java/io/PrintStream.println : (I)V>
14 return- 在栈中开辟内存
会开启两个空间,数组[0] 是 main函数的参数 args
- iconst_0
将0放入操作数栈中 - istore_1
将操作数栈中的值放入数组[1]中 - iload_1
数组[1]中的值放入操作数栈中 - iinc 1 by 1
将 数组[1]中的值 加 1 - istore_1
将操作数栈中的值放入数组[1]中 - getstatic #7
从类 System 中获取静态字段 out - iload_1
数组[1]中的值放入操作数栈中 - invokevirtual #13
调用对象的实例方法。
- 在栈中开辟内存
- return
方法结束
类的生命周期
加载
│
▼
连接(验证 → 准备 → 解析)
│
▼
初始化(执行
│
▼
使用
│
▼
卸载(由 GC 回收 Class 对象)
加载
- JVM 通过类的全限定名查找 .class 文件 ,由类加载器将字节码读入内存(即方法区),创建 Class 对象
- 在堆中创建一个 java.lang.Class 对象用于封装方法区中的类信息,程序中我们使用 MyClass.class 或 Class.forName() 得到的就是这个对象
连接
连接是 JVM 将类的二进制表示转换为可以被虚拟机使用的状态的过程,包括验证、准备和解析三个子阶段。
🧪 验证
✅ 目的:
确保字节码文件的合法性、安全性、符合 JVM 规范,避免崩溃或恶意代码入侵。
🔍 主要验证内容:
文件格式验证:字节码文件的魔数、版本号等是否合法。
元数据验证:类的父类是否存在,字段类型是否正确。
字节码验证:方法中指令是否合法,操作数栈使用是否正确。
符号引用验证:常量池中的类名、字段、方法是否合法。
🧱 准备
✅ 目的:
为类中的 静态变量(static 字段)分配内存,并赋予默认值。
🔍 特点:
赋默认值,不是编写代码中设置的值(final除外)。
🌰 示例:
1 | |
准备阶段时:
a = 0(默认值)
b 会直接在编译期被放入常量池(JVM 优化,可能不参与连接)
| 数据类型 | 默认值 |
|---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | \u0000(空字符) |
boolean | false |
| 引用类型(如 String) | null |
🧩 解析
✅ 目的:
将常量池中的符号引用(如类名、字段名、方法名)转为 直接引用(指向内存地址的指针、偏移量等)。
| 类型 | 举例 |
|---|---|
| 符号引用 | “java/lang/String” |
| 直接引用 | 指向 String.class 的地址 |
✅ 初始化
✅ 目的:
初始化阶段指执行类的初始化方法
- 初始化顺序示例初始化过程:
1
2
3
4
5
6
7
8
9
10
11public class Example {
static int a = 10;
static {
System.out.println("静态代码块执行");
a = 20;
}
public static void main(String[] args) {
System.out.println("a = " + a);
}
}
准备阶段时,a 被赋默认值 0。
初始化阶段执行
: a = 10;
执行静态代码块,a = 20;,同时打印输出。
main 方法输出:a = 20
📋 何时触发初始化:
首次主动使用类时触发,如:
- 创建类实例(new)
- 访问类的静态字段(非 final 常量)
- 调用静态方法
- 使用反射 Class.forName()
- 初始化子类时,先初始化父类
类加载器
常见的类加载器:
| 类加载器 | 简称 | 是否 Java 实现 | 加载内容 | 类加载路径 |
|---|---|---|---|---|
| 启动类加载器 | Bootstrap | ❌(C/C++ 实现) | JDK 核心类库,如 java.lang.*、java.util.* | $JAVA_HOME/lib/rt.jar(JDK8及以下) |
| 扩展类加载器 | Extension | ✅ | JDK 扩展类库 | $JAVA_HOME/lib/ext/ 或 -Djava.ext.dirs |
| 应用类加载器 | AppClassLoader | ✅ | 应用程序类(用户编写的类) | classpath 环境变量指定的路径 |
| 自定义类加载器 | Custom | ✅ | 根据用户定义的逻辑加载类 | 自定义路径、加密文件、网络等 |
双亲委派机制
双亲委派机制的核心是解决一个类应该由哪个类加载器去加载
工作原理
双亲委派机制的执行流程如下:
- 当某个类加载器收到类加载请求时,它不会立刻去尝试加载这个类;
- 它会先将这个请求委托给父加载器去加载;
- 如果父加载器还有父加载器,则继续向上委托,直到顶层的启动类加载器(Bootstrap ClassLoader);
- 如果父加载器能找到这个类,就直接返回类的Class对象;
- 如果父加载器找不到,才由当前类加载器尝试自己去加载这个类。
类加载器的层次结构
Bootstrap ClassLoader(启动类加载器)
↑
ExtClassLoader(扩展类加载器)
↑
AppClassLoader(应用程序类加载器)
↑
自定义ClassLoader
为什么使用双亲委派机制
安全性:防止核心 Java 类被恶意覆盖或篡改。例如自定义一个 java.lang.String 类。
避免重复加载:保证同一个类只会被一个类加载器加载一次,确保类的唯一性。
模块化和可扩展性:通过自定义类加载器,实现模块隔离(例如 OSGi、Tomcat 的 WebAppClassLoader)。
打破双亲委派的情况
有些场景中需要打破双亲委派,例如:
热部署(如 Tomcat 加载 Web 应用);
插件化开发(如 IDEA 插件机制);
某些框架需要优先加载自己的类版本(比如 Spring Boot 的 LaunchedURLClassLoader)。
方法:
自定义类加载器







