JVM 类加载
JVM 类加载
一. 类文件结构
一个简单的 HelloWorld.java
package cn.itcast.jvm.t5; // HelloWorld 示例 public class HelloWorld {public static void main(String[] args) {System.out.println("hello world");} }
执行 javac -parameters -d . HelloWorld.java 就能得到字节码文件. (其中 -parameters 表示"让编译器在生成的字节码中记录方法参数的名称信息").
1. 类文件结构规范
ClassFile {u4 magic; # 魔数 (标识文件是否为有效的类文件)u2 minor_version; # 次版本号u2 major_version; # 主版本号 u2 constant_pool_count; # 常量池长度cp_info constant_pool[constant_pool_count-1]; # 常量池u2 access_flags; # 访问标志 (public/private/final/abstract)u2 this_class; # 当前类u2 super_class; # 父类u2 interfaces_count; # 接口数量u2 interfaces[interfaces_count]; # 接口 u2 fields_count; # 字段数量field_info fields[fields_count]; # 字段u2 methods_count; # 方法数量method_info methods[methods_count]; # 方法u2 attributes_count; # 类的附加属性数量attribute_info attributes[attributes_count]; # 类的附加属性 }
2. 部分字段说明
魔数
0~3字节 (共占4个字节) 表示当前文件是否为 class 类型的文件 (不同类型的文件有不同的魔数信息).
ca fe ba be 表示当前文件时 .class 文件.
版本
4~7字节 (共占4个字节) 表示类的版本.
00 00 00 34 (十进制52) 表示 Java8.
例子: 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
常量池
Constant Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
8~9字节 (共占4个字节) 表示常量池长度. 例如: 00 23 (十进制35) 表示常量池中有34项 (#1~#34项).
从10字节开始往后就是常量池.
字节码文件示例:
ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04 69 72 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00 00 00 02 00 14
二. 类加载阶段
1. 加载
(1) 核心任务
将类的字节码文件 (.class文件) 加载到 JVM 内存中, 并生成一个代表该类的 java.lang.Class 对象.
(2) 具体操作
通过类的全限定名 (如
com.example.User
) 找到对应的字节码文件.将字节码文件的二进制数据读入 JVM 内存.
在方法区中创建该类的运行时数据结构 (包含 类的版本, 字段, 方法等信息).
在堆中生成一个 Class 类的实例, 作为方法区中该类数据的访问入口.
注意
加载阶段由类加载器 (ClassLoader) 完成.
加载阶段与连接阶段的部分操作可能重叠 (如字节码验证可能在加载未完成时就开始).
2. 连接
2.1 验证
验证字节码是否符合 JVM 规范, 避免不合规范的字节码危害 JVM 安全.
主要验证内容:
文件格式验证 (检查是否符合类文件规范 如 魔数, 版本号 是否正确).
元数据验证 (检查类的元数据信息 如 访问修饰符, 类中字段和方法 是否符合语法规则).
字节码验证 (检查方法体中的字节码指令是否合法).
符号引用验证 (检查常量池中的符号是否能被正确解析).
2.2 准备
为类的 静态变量 (类变量) 分配内存, 并设置默认初始值.
注:
默认初始值并非代码中定义的初始值, 而是 Java 的 "零值" (如 int 为 0, boolean 为 false, 引用类型为 null).
被 final 修饰的 基本类型 或 字符串类型 赋值操作会在准备阶段完成.
2.3 解析
将常量池中的 符号引用 转换为 直接引用.
符号引用: 用字符串描述的引用 (如类名, 方法名, 字段名), 存在于字节码的常量池中 (在编译期生成).
直接引用: 指向内存中实际地址的引用 .
注:
解析阶段并非必须按顺序执行, JVM 可在执行字节码指令时动态触发解析 (延迟解析).
3. 初始化
(1) 核心任务
执行类的初始化代码 (执行 静态变量赋值 和 静态代码块), 将类变量设置为开发者定义的初始值.
(2) 触发时机
初始化阶段是类加载的最后一步, 只有在 主动使用类 时才会触发初始化 (是惰性的).
常见触发时机如下:
创建类的实例 (如
new User()
).调用类的静态方法.
访问类的静态变量 (非
final
).反射调用 (如
Class.forName("com.example.User")
)初始化子类时, 其父类会先被初始化.
(3) 执行顺序
父类的初始化代码先于子类执行.
静态变量的赋值与静态代码块按代码定义顺序执行.
注: 不会触发初始化的情况
访问类的 static final 静态常量 (基本类型和字符串) 不会触发初始化.
访问类对象 .class 不会触发初始化.
创建该类的数组不会触发初始化.
类加载器的 loadClass() 方法.
Class.forName 的参数 2 为 false 时.
三. 类加载器
以 JDK 8 为例, 类加载器的层级关系如下:
名称 | 加载哪里的类 | 说明 |
---|---|---|
Bootstrap ClassLoader (启动类加载器) | JAVA_HOME/jre/lib | 无法直接访问 |
Extension ClassLoader (扩展类加载器) | JAVA_HOME/jre/lib/ext | 上级为 Bootstrap, 显示为 null |
Application ClassLoader (应用程序类加载器) | classpath | 上级为 Extension |
自定义类加载器 | 自定义 | 上级为 Application |
1. 启动类加载器
("Bootstrap ClassLoader")
是 Java 类加载层次结构中的顶层类加载器
加载范围: 负责加载 Java 运行时核心类库, 主要加载 %JAVA_HOME%/jre/lib 目录下的类库文件 (如 java.lang.Object, java.util.List 等). 这些类库是 Java 运行的基础, 提供了最核心的功能.
实现方式: 由 C++ 语言实现, 是JVM的一部分.
在 Java 程序中无法直接访问. 如果在 Java 代码中调用某个类的getClassLoader()方法, 得到的返回值为 null, 就说明这个类是由启动类加载器加载的.
作用: 加载 Java 核心类库, 为 Java 程序提供最基础的类加载支持, 保证 JVM 能够运行最基本的 Java 代码.
2. 扩展类加载器
("Extension ClassLoader")
扩展类加载器 的父加载器是 启动类加载器
加载范围: 负责加载 Java 的扩展类库, 主要加载 %JAVA_HOME%/jre/lib/ext 目录下 和 由 java.ext.dirs 系统属性指定的目录中的类库文件. 这些扩展类库可以用于扩展 Java 的功能, 例如一些第三方提供的与系统交互相关的扩展包等.
实现方式: 由 Java 语言实现, 是 java.lang.ClassLoader类的子类.
如果在 Java 代码中调用某个类的getClassLoader()方法, 得到的返回值为 扩展类加载器的实例, 就说明这个类是由扩展类加载器加载的.
作用: 加载扩展目录下的类库, 为 Java 程序提供额外的功能支持, 且加载的类可以被应用程序类加载器所加载的类访问.
3. 应用程序类加载器
("Application ClassLoader")
应用程序类加载器 的父加载器是 扩展类加载器.
加载范围: 负责加载 应用程序的类路径 (classpath) 下的所有类, 包括开发者自己编写的类以及引用的第三方类库 (比如通过Maven / Gradle 引入的依赖). 开发中, 我们编写的业务代码, 配置文件等都是由应用程序类加载器来加载的.
实现方式: 由 Java 语言实现, 是java.lang.ClassLoader 类的子类.
如果在 Java 代码中调用某个类的getClassLoader()方法, 得到的返回值为 应用程序类加载器的实例, 就说明这个类是由应用程序类加载器加载的.
作用: 是 Java 应用程序中最常用的类加载器, 负责加载应用程序运行所需的各类业务逻辑类和依赖类库, 让应用程序能够正常运行.
4.双亲委派模型
这三种类加载器遵循 双亲委派模型: 即当一个类加载器收到类加载请求时, 它 不会尝试自己去先加载这个类,而是把请求委托给父类加载器去完成, 依次向上, 直到顶层的启动类加载器. 只有当父类加载器无法完成加载任务时 (即: 父类加载器在它的加载范围内找不到需要加载的类), 子类加载器才会尝试自己去加载这个类.
双亲委派模型的优点:
避免类的重复加载: 比如 java.lang.Object 类, 无论由哪个类加载器加载, 最终都是由启动类加载器加载, 不会出现多个不同版本的 Object 类.
保证 Java 核心类库的安全性: 核心类库都是由启动类加载器加载, 不可被篡改和替换. 防止了恶意代码替换 Java 核心类造成安全问题.