JVM 为什么需要类加载机制?深入浅出 JVM 类加载原理
类加载机制是什么?
大家好,我是码哥,《Redis 高手心法》作者、InfoQ 签约作者、51CTO Top 红人。
在 Java 中,类加载机制是 Java 虚拟机(JVM)将 .class
文件加载到内存并转化为可以运行的 Class 对象的过程。简单来说,类加载机制是让“代码变为现实”的第一步!
你可能会问,为什么需要类加载机制? 因为 Java 是一门 动态语言,类可以在运行时加载、链接和初始化,这种灵活性让 Java 能够实现跨平台运行、高效的内存管理和模块化架构。
介绍下我的《Java 面试高手心法 58 讲》专栏内容涵盖 Java 基础、Java 高级进阶、Redis、MySQL、消息中间件、微服务架构设计等面试必考点、面试高频点。
丢掉你收藏的那些所谓的「面试宝典」,因为它们大多数深度不够,甚至内容还有错误,你只会看完就忘,还浪费时间。这也是为何每次面试你都回答不好的原因,找不到好工作的原因。
类加载的三个阶段
根据《Java 虚拟机规范》,类的生命周期包括以下三个主要阶段:加载、链接 和 初始化。
而其中链接又分为三个子阶段:验证(Verification)、准备(Preparation)、解析(Resolution)。
我们逐一拆解这些阶段的工作原理和流程。
加载(Loading)
Chaya:类加载阶段作用是什么?非要加载吗?
主要是使用 "类加载器" 将本地或者远程网络中的字节码文件,通过读字节流的方式加载到 Java 虚拟机内存中。在加载阶段中 Java 虚拟机主要完成以下三件事情:
① 通过一个类的全限定名称来获取定义此类的二进制字节流。
② 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
③ 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区中这个类的各种数据的访问入口。
加载是类加载的第一步,JVM 需要完成以下任务:
读取 Class 文件:通过类的全限定名找到对应的
.class
文件。转换为 JVM 可识别的结构:将 Class 文件的二进制数据转换为 JVM 的运行时数据结构。
创建 Class 对象:在内存中创建
java.lang.Class
对象,作为该类的入口。
示例。
Class<?> clazz = Class.forName("com.example.MyClass");
这段代码会触发 MyClass
的加载,将其 .class
文件读取到内存中,并生成 Class
对象。
链接(Linking)
链接 是将 Class 文件中的符号引用解析为直接引用的过程,分为以下三个子阶段:
验证(Verification)确保 Class 文件的字节码格式和内容符合 JVM 的规范。
验证文件格式:Class 文件是否以
0xCAFEBABE
开头。验证字节码:指令是否符合 JVM 规范,数据类型是否匹配。
准备(Preparation)为类的静态变量分配内存,并设置默认值。
例如:
static int a = 10;
在准备阶段,a
的初始值是0
。
解析(Resolution)将符号引用替换为内存地址的直接引用。
符号引用:
java.lang.String
直接引用:指向
String
类在内存中的地址。
验证阶段 (Verification)
验证阶段的主要目的是对字节码字节流进行校验,判断其内容是否符合当前虚拟机的规范,以确保被加载的代码运行后不会对虚拟机造成损害。
大多数虚拟机大致都会对 文件格式
、元数据
、字节码
、符号引用
几项内容进行校验。
文件格式验证
文件格式验证主要是对 字节流格式
进行校验,判断其是否符合字节码文件格式规范,并且还要判断其是否可以运行在当前版本的虚拟机中。比如:
序号 | 描述 |
---|---|
1 | 验证是否以 0XCAFEBABE 开头 |
2 | 验证主、次版本号,是否包含在当前虚拟机支持的版本范围内 |
3 | 验证字节码常量池中的常量类型,是否都被虚拟机所支持 |
4 | 验证指向常量的各种索引值,是否有指向不存在的常量或不符合类型的常量 |
5 | 验证 CONSTANT_Utf8_info 类型常量中,是否有不符合 UTF-8 编码的数据 |
6 | 验证字节码文件中各个部分及文件本身,是否有被删除或附加的其他信息 |
文件格式验证的主要目的其实就是为了保证加载的字节码可以被正确地解析并存储在方法区内。
元数据验证
元数据验证主要是对 字节码
中的 元数据信息
进行语法校验,避免存在不符合 Java 语法规范的元数据信息。比如:
序号 | 描述 |
---|---|
1 | 验证当前类的父类是否继承了不允许被继承的类,比如被 final 修饰的类 |
2 | 验证当前类是否有父类,一般情况下除了 java.lang.Object 外,所有的类都应当有父类 |
3 | 验证如果当前类不是抽象类,则当前类是否实现了其父类或接口之中要求实现的所有方法 |
4 | 验证当前类中的字段或方法是否与父类有冲突,比如当前类覆盖了父类的 final 字段,或者当前类实现的方法参数都一致,但返回值的类型却不同,导致不符合方法重载规则等情况 |
字节码验证
字节码验证主要是对 数据流
和 控制流
进行分析,以确保其语法合规且符合逻辑。
符号引用验证
符号引用验证主要对 字节码常量池
中 常量
的各种 符号引用
进行校验,确保当前类引用到的其它类或者方法是真实存在且有权限访问的。如果符号引用中关联的类无法在系统中查找到,就会抛出 NoClassDefFoundError
错误,如果符号引用中关联的方法无法找到,则会抛出 NoSuchMethodError
错误。
准备阶段 (Preparation)
准备阶段主要是用于对类或接口中的 "静态变量" 分配内存空间,以及对变量设置默认的初始值。
准备阶段和初始化阶段,这两个阶段都是用于对静态变量设置值,概念上容易混淆,所以这里需要特别说明一下,准备阶段只是对静态变量设置初始默认值,而真正赋值操作是在初始化阶段完成的。
例如,下面示例代码在执行时:
public class A {
static int test = 999;
}
准备阶段会对变量 test 设置默认值
0
;初始化阶段会对变量 test 赋予初始值
999
;
解析阶段 (Resolution)
解析阶段主要是用于将 字节码常量池
中的 符号引用
替换为 直接引用
的过程。
符号引用 (Symbolic References): 符号引用就是用于描述引用目标的一组符号,它可以是任何形式的字面量 (只要符合 Java 虚拟机规范)。
直接引用 (Direct References): 直接引用可以是直接指向目标的指针、相对偏移量,或者是一个能间接定位到目标的句柄。
初始化(Initialization)
初始化阶段是类加载的最后一步,也是最重要的阶段。此阶段会执行静态变量的赋值操作和静态代码块。
初始化的触发条件:
类的初始化顺序
先初始化父类。
再初始化当前类的静态变量和静态代码块。
使用
new
关键字实例化对象时。访问类的静态字段或静态方法时。
使用反射调用类时。
唐二婷:初始化阶段有啥用?可以谈恋爱吗?
初始化阶段主要是执行 类构造器
方法 <clinit>()
,该方法不需要定义,代码在经过 Javac 编译器编译时,会自动收集类中的所有 类变量
的赋值动作和 静态代码块
中的语句,对这些代码进行合并,形成类构造器 <clinit>()
。
在执行类构造器 <clinit>()
时,会对类中的 类变量
和 静态代码块
进行初始化赋值操作,如果该类存在父类,则会先执行父类中的类构造器 <clinit>()
,对父类中的 类变量
和 静态代码块
进行初始化。
示例如下。
public class FatherCLass {
public static int number;
static {
System.out.println(number);
System.out.println("父类 static{} 初始化");
}
}
子类:
public class SubInitialization extends FatherCLass {
static{
// number 属于父类的属性,这里要能执行成功,说明父类已经加载
number = 100;
System.out.println("子类 static{} 初始化");
}
public static void main(String[] args) {
System.out.println(number);
}
}
执行时输出如下:
0
父类 static{} 初始化
子类 static{} 初始化
100
最后(Ending)
最后,顺带给大家介绍下,我的新书写《Redis 高手心法》,本书基于 Redis 7.0 版本,拟人故事化方式和诙谐幽默的言语与各路“神仙”对话。
作者简介
往期推荐
Java 对象到底是如何创建的?类加载机制是什么?对象的内存布局和访问方式有哪些?
堆、栈、方法区到底是什么?一文带你搞懂 JVM 运行时数据区内存模型!
什么是 JVM?JVM 为什么是开发者必须了解的核心技术?
重生之从零设计 MySQL 架构
小米面试:什么是线程池?工作原理是什么?线程池可以动态修改吗?