Java虚拟机(JVM)的类加载机制是其核心组成部分之一,它负责将字节码文件从磁盘加载到内存,并使其变成可以在JVM中执行的对象。理解类加载机制对于开发人员优化应用程序的启动时间、内存使用以及解决类加载相关问题非常重要。本文将深入解析Java虚拟机的类加载过程、验证步骤、初始化过程,以及类加载器的工作原理。
1. 类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。

关于在什么情况下需要开始类加载过程的第一个阶段“加载”,Java虚拟机规范中并没有进行强制约束。 但是对于初始化阶段,严格规范了只有下面六种情况必须立即对类进行“初始化”(加载、验证、准备自然需要在此之前开始):
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能生成这四条指令的典型Java代码场景:
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入调用处的那个类的常量池的静态字段除外)的时候
- 调用一个类型的静态方法的时候
- 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为
REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化 - 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
2. 类加载的过程
类加载过程通常分为以下五个阶段:加载、验证、准备、解析和初始化。
2.1 加载
加载阶段包括以下几个关键步骤:
- 获取字节流:通过类的全限定名获取类的二进制字节流。
- 数据转换:将字节流转换为JVM内部的类数据结构。
- 生成Class对象:在内存中生成一个
java.lang.Class对象,作为访问类数据的入口。
2.2 验证
验证是连接过程中的第一步,旨在确保字节码文件符合Java虚拟机规范,保证类不会对虚拟机造成安全威胁。验证包括:
2.2.1 文件格式验证
确保字节码符合class文件格式的规范。包括检查文件是否以0xCAFEBABE开头、类版本是否支持等。
2.2.2 元数据验证
对字节码描述的信息进行语义检查,确保符合Java语言规范。比如,类是否具有父类,字段和方法是否符合规范等。
2.2.3 字节码验证
通过数据流和控制流分析,确保字节码在执行时不会危害虚拟机的安全。JDK6之后,字节码验证得到优化,虚拟机通过StackMapTable属性进行类型检查,减少了验证的复杂度。
2.2.4 符号引用验证
符号引用验证发生在解析阶段,将常量池中的符号引用转换为直接引用时,确保类、字段、方法等的访问权限和存在性。
2.3 准备
准备阶段为类的静态变量分配内存并设置初始值。所有静态变量会被初始化为默认值,例如0、null等。对于final修饰的静态常量,虚拟机会直接在此阶段赋予其编译时已知的值。
2.4 解析
解析阶段是将常量池中的符号引用替换为直接引用的过程。解析过程包括类、字段、方法、方法句柄等的解析。符号引用与虚拟机内存布局无关,可以在运行时动态解析。
2.5 初始化
初始化阶段执行类中的静态初始化代码(即<clinit>()方法)。<clinit>()方法由编译器自动生成,包含静态字段赋值和静态代码块的执行。此阶段的关键点是:
- 静态字段赋值和静态代码块的执行顺序按源代码中的顺序执行。
- 在多线程环境中,JVM会确保一个类的初始化操作只会由一个线程执行,避免多线程争用。
3. 类加载器
类加载器是JVM中负责加载类的组件。它的工作流程通常是通过类的全限定名获取字节流,解析后加载到内存中。类加载器可以根据不同的需求进行扩展或定制。
3.1 双亲委派模型
Java采用了双亲委派模型来管理类加载器的层次结构。所有的类加载请求首先会被委派给父类加载器处理,只有当父加载器无法加载时,子加载器才会尝试加载。JVM默认有以下几种类加载器:
- 启动类加载器(Bootstrap ClassLoader):负责加载核心类库(如
rt.jar中的类)。 - 扩展类加载器(Extension ClassLoader):加载JVM扩展库(如
JAVA_HOME/lib/ext目录中的类)。 - 应用类加载器(Application ClassLoader):加载用户指定的类路径中的类。
类加载器双亲委派模型:

通过双亲委派模型,Java虚拟机确保了类加载的稳定性和层次性。如果没有双亲委派模型,程序中可能会出现多个java.lang.Object类等问题,导致系统的不稳定,双亲委派模型对于保证Java程序的稳定运作极为重要,但它的实现非常简单。用以实现双亲委派模型的代码只有10行左右,全部在java.lang.ClassLoader的loadClass()方法之中。
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
//首先,检查请求的类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//如果父类加载器抛出ClassNotFoundException说明父类加载器无法完成加载请求
}
if (c == null) {
//在父类加载器无法加载时,再调用本身的findClass()方法来进行类加载
c = findClass(name);
}
}
return c;
}
核心逻辑:先检查请求加载的类型是否已经被加载过,如果没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常的话,才调用自己的findClass()方法尝试进行加载。
4. 总结
Java虚拟机的类加载机制是一个复杂且高效的过程,包括加载、验证、准备、解析和初始化等多个阶段。每个阶段都有严格的规范,确保程序在不同环境中稳定运行。理解类加载的时机、过程和类加载器的工作原理,有助于开发者更好地掌握JVM,优化应用程序的性能,解决类加载相关的问题。掌握双亲委派模型和类加载器的实现机制,对深入理解JVM和进行系统优化尤为重要。
🌟 关注我的CSDN博客,收获更多技术干货! 🌟

751

被折叠的 条评论
为什么被折叠?



