JVM 的类加载机制是指 JVM 将 .class
文件(包含 Java 字节码)加载到内存,并对其进行校验、解析、初始化,最终转换为 JVM 可以直接使用的 Java 类型的过程。
类加载过程 (5 个阶段):
-
加载 (Loading):
- 查找并加载类的二进制数据:
- 通过类的全限定名(Fully Qualified Name)查找
.class
文件。 - 类加载器(ClassLoader)负责查找和加载
.class
文件。类加载器有多种,包括启动类加载器、扩展类加载器、应用程序类加载器,以及自定义类加载器。 - 类加载器遵循双亲委派模型(Parent Delegation Model)。
- 将找到的
.class
文件中的二进制数据读取到内存中。
- 通过类的全限定名(Fully Qualified Name)查找
- 创建类的 Class 对象:
- 在方法区中创建一个
java.lang.Class
对象,作为该类的访问入口。 - 这个
Class
对象封装了类的结构信息(类名、父类、接口、字段、方法、注解等)。
- 在方法区中创建一个
- 查找并加载类的二进制数据:
-
链接 (Linking):
- 验证 (Verification):
- 目的: 确保加载的类文件符合 JVM 规范,并且不会危害 JVM 的安全。
- 检查内容:
- 文件格式验证: 检查
.class
文件是否符合 Java 类文件规范(魔数、版本号等)。 - 元数据验证: 检查类的元数据是否正确(例如,是否有父类、是否实现了接口、字段和方法的描述符是否合法等)。
- 字节码验证: 检查字节码指令是否合法、安全(例如,类型检查、控制流检查、操作数栈检查等)。这是最复杂的一个阶段。
- 符号引用验证: 检查符号引用是否能够找到对应的类、字段或方法。
- 文件格式验证: 检查
- 准备 (Preparation):
- 目的: 为类的静态变量(static variables)分配内存,并设置默认初始值(零值)。
- 注意:
- 这里分配内存的仅包括类变量(static 修饰的变量),不包括实例变量(实例变量会在对象实例化时随着对象一起分配在 Java 堆中)。
- 这里设置的初始值是“零值”(例如,int 类型的零值是 0,boolean 类型的零值是 false,引用类型的零值是 null),而不是代码中显式赋予的值(显式赋值是在初始化阶段进行的)。
- 对于
static final
修饰的基本类型或字符串常量, 会直接在准备阶段赋值.
- 解析 (Resolution):
- 目的: 将类、接口、字段和方法的符号引用(symbolic references)解析为直接引用(direct references)。
- 符号引用: 以符号(例如,类的全限定名、方法的名称和描述符)来表示目标。
- 直接引用: 可以直接定位到目标的指针、偏移量或句柄。
- 解析的时机:
- Java 虚拟机规范并没有强制规定解析阶段的具体时间,可以延迟到运行时(用到的时候再解析)。
- HotSpot VM 中,解析通常是延迟进行的。
- 解析的内容:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
- 验证 (Verification):
-
初始化 (Initialization):
- 目的: 执行类的初始化代码,为静态变量赋初始值(代码中显式指定的值),并执行静态代码块。
- 触发时机:
- 遇到
new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时(创建对象、访问静态字段、调用静态方法)。 - 使用
java.lang.reflect
包的方法对类进行反射调用时。 - 初始化一个类的子类时(会先初始化其父类)。
- 虚拟机启动时,用户需要指定一个要执行的主类(包含
main
方法的类),虚拟机会先初始化这个主类。 - 使用JDK 1.7 的动态语言支持时(invokedynamic指令).
- 接口的初始化: 当一个接口中定义了 default 方法时, 如果这个接口的实现类发生了初始化, 则该接口要在实现类之前被初始化.
- 遇到
- 执行内容:
- 执行
<clinit>()
方法(类构造器)。<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。<clinit>()
方法的执行顺序是按照语句在源文件中出现的顺序决定的。<clinit>()
方法与类的构造函数(<init>()
方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。- 如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成
<clinit>()
方法。 - 接口中不能使用静态代码块, 但接口也需要通过
<clinit>()
方法为接口中定义的静态成员变量显示初始化. - 接口的
<clinit>()
方法不需要先执行父接口的<clinit>()
方法. - 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。
- 执行
类加载器 (ClassLoader):
-
类型:
- 启动类加载器 (Bootstrap Class Loader): C++ 实现, 负责加载 Java 核心类库(
<JAVA_HOME>/jre/lib
)。 - 扩展类加载器 (Extension Class Loader): Java 实现(sun.misc.Launcher$ExtClassLoader), 负责加载 Java 扩展类库(
<JAVA_HOME>/jre/lib/ext
或java.ext.dirs
指定的目录)。 - 应用程序类加载器 (Application Class Loader): Java 实现(sun.misc.Launcher$AppClassLoader), 负责加载应用程序的类(classpath)。
- 自定义类加载器: 继承
java.lang.ClassLoader
,实现自定义的类加载逻辑。
- 启动类加载器 (Bootstrap Class Loader): C++ 实现, 负责加载 Java 核心类库(
-
双亲委派模型 (Parent Delegation Model):
- 除了启动类加载器,每个类加载器都有一个父类加载器。
- 当一个类加载器需要加载类时,它首先会委托给它的父类加载器去加载。
- 只有当父类加载器无法加载该类时(在其搜索范围内找不到该类),才由子类加载器尝试加载。
- 优点:
- 避免类的重复加载。
- 保证 Java 核心类库的安全性(防止用户自定义的类替换核心类)。
类加载机制的特点:
- 动态加载: 类加载是在程序运行时进行的,而不是在编译时。这使得 Java 具有动态性和灵活性。
- 按需加载: 类加载器只在需要时才加载类,而不是一次性加载所有类。这可以节省内存空间。
- 双亲委派: 双亲委派模型保证了类加载的顺序和安全性。
- 缓存机制: 类加载器会缓存已加载的类,避免重复加载。
代码示例 (自定义类加载器):
import java.io.*;public class MyClassLoader extends ClassLoader {private String classpath;public MyClassLoader(String classpath) {this.classpath = classpath;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {byte[] classData = loadClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}} catch (IOException e) {throw new ClassNotFoundException();}}private byte[] loadClassData(String className) throws IOException {String fileName = classpath + File.separatorChar+ className.replace('.', File.separatorChar) + ".class";try (InputStream ins = new FileInputStream(fileName);ByteArrayOutputStream baos = new ByteArrayOutputStream()) {int bufferSize = 1024;byte[] buffer = new byte[bufferSize];int length = 0;while ((length = ins.read(buffer)) != -1) {baos.write(buffer, 0, length);}return baos.toByteArray();}}public static void main(String[] args) throws Exception{//测试自定义类加载器MyClassLoader myClassLoader = new MyClassLoader("./myclasses"); // 指定类路径Class<?> clazz = myClassLoader.loadClass("com.example.MyClass"); // 加载类Object obj = clazz.newInstance(); //实例化System.out.println(obj.getClass().getClassLoader());//打印类加载器}
}
总结:
JVM 的类加载机制负责将 .class
文件加载到内存中,并将其转换为 JVM 可以使用的 Java 类型。类加载过程包括加载、链接(验证、准备、解析)和初始化几个阶段。类加载器遵循双亲委派模型,保证了类加载的顺序和安全性。