一文学会Java类加载
字数 2352 2025-08-30 06:50:35

Java类加载机制详解

1. 类加载过程概述

Java类加载过程分为三个主要阶段:加载、链接和初始化。这三个阶段共同完成了将.class文件中的二进制数据转换为JVM中的Class对象的过程。

1.1 加载阶段

加载阶段是类加载的第一个阶段,主要完成以下工作:

  • 通过类的全限定名获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

2. 链接阶段

链接阶段又分为三个子阶段:验证、准备和解析。

2.1 验证

验证阶段确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证主要包括:

  • 文件格式验证:验证字节流是否符合Class文件格式规范
  • 元数据验证:对类的元数据信息进行语义校验
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的
  • 符号引用验证:验证符号引用能否正确解析

2.2 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。需要注意:

  • 这时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量
  • 初始值通常是数据类型的零值(如0、0L、null、false等)
  • 如果类字段是常量(static final),则会被初始化为ConstantValue属性所指定的值

2.3 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析主要针对以下七类符号引用:

  • 类或接口
  • 字段
  • 类方法
  • 接口方法
  • 方法类型
  • 方法句柄
  • 调用点限定符

符号引用 vs 直接引用

  • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量
  • 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄

3. 初始化阶段

3.1 初始化阶段的核心任务

初始化阶段是执行类构造器<clinit>()方法的过程,该方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生。

3.2 触发初始化的条件

以下情况会触发类的初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时
  2. 使用java.lang.reflect包的方法对类进行反射调用时
  3. 当初始化一个类时,发现其父类还未初始化,需先触发其父类的初始化
  4. 虚拟机启动时,用户指定的主类(包含main()方法的类)会被初始化
  5. 当使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时

4. 类加载器

Java虚拟机使用类加载器来实现类的加载动作,主要有以下几种:

4.1 Bootstrap ClassLoader(启动类加载器)

  • 负责加载JAVA_HOME/lib目录下的核心类库
  • 由C++实现,是虚拟机自身的一部分
  • 不继承java.lang.ClassLoader

4.2 Extension ClassLoader(扩展类加载器)

  • 负责加载JAVA_HOME/lib/ext目录下的扩展类库
  • 由sun.misc.Launcher$ExtClassLoader实现
  • 是Java语言实现的

4.3 Application ClassLoader(应用类加载器)

  • 负责加载用户类路径(ClassPath)上的类库
  • 由sun.misc.Launcher$AppClassLoader实现
  • 是程序中默认的类加载器

4.4 类加载器的核心方法

loadClass - 类加载的入口

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException

findClass

protected Class<?> findClass(String name) throws ClassNotFoundException

defineClass - 将字节码转换为Class对象

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

resolveClass - 完成类的解析

protected final void resolveClass(Class<?> c)

5. 双亲委派模型

双亲委派模型的工作过程:

  1. 当一个类加载器收到类加载请求时,首先不会自己尝试加载,而是委派给父类加载器
  2. 只有当父类加载器反馈无法完成加载请求时,子加载器才会尝试自己加载

5.1 特别情况

Tomcat的Web应用类加载器(WebAppClassLoader)

  • 每个Web应用都有自己的类加载器
  • 打破了双亲委派模型,优先加载自己路径下的类

SPI机制

SPI(Service Provider Interface)是JDK内置的服务发现机制,典型应用场景包括:

  • JDBC
  • JNDI
  • JCE
  • JAXP
  • JBI

6. 自定义解密类加载器实现

6.1 编写一个HelloWorld类

public class HelloWorld {
    public void say() {
        System.out.println("Hello World");
    }
}

6.2 编写Class文件加密代码

public class FileEncoder {
    public static void encode(String src, String dest) throws IOException {
        FileInputStream fis = new FileInputStream(src);
        FileOutputStream fos = new FileOutputStream(dest);
        
        int b;
        while((b = fis.read()) != -1) {
            fos.write(b ^ 0xFF); // 简单异或加密
        }
        
        fis.close();
        fos.close();
    }
}

6.3 编写自定义类加载器

public class MyClassLoader extends ClassLoader {
    private String classPath;
    
    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] classBytes = getClassBytes(name);
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException(name);
        }
    }
    
    private byte[] getClassBytes(String name) throws Exception {
        String path = classPath + File.separator + 
                     name.replace('.', File.separatorChar) + ".class";
        FileInputStream fis = new FileInputStream(path);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        
        int b;
        while((b = fis.read()) != -1) {
            baos.write(b ^ 0xFF); // 解密
        }
        
        fis.close();
        return baos.toByteArray();
    }
}

6.4 使用自定义类加载器

public class Main {
    public static void main(String[] args) throws Exception {
        MyClassLoader loader = new MyClassLoader("encrypted_classes");
        Class<?> clazz = loader.loadClass("com.example.HelloWorld");
        Object obj = clazz.newInstance();
        Method method = clazz.getMethod("say");
        method.invoke(obj);
    }
}

7. 注意事项

7.1 类的唯一性

  • 同一个类被不同的类加载器加载会被JVM视为不同的类
  • 可能导致类型转换异常(ClassCastException)

7.2 被动引用

以下情况不会触发类的初始化:

  1. 通过子类引用父类的静态字段
  2. 通过数组定义来引用类
  3. 引用类的常量(final static)

7.3 接口初始化

  • 接口的初始化不需要先初始化其父接口
  • 只有当真正使用父接口时才会初始化

7.4 类卸载

满足以下条件时,类可以被卸载:

  1. 该类的所有实例都已被回收
  2. 加载该类的ClassLoader已被回收
  3. 该类对应的java.lang.Class对象没有被引用

总结

Java类加载机制是JVM的核心组成部分,理解类加载过程、类加载器的工作原理以及双亲委派模型对于深入理解Java运行机制至关重要。通过自定义类加载器可以实现许多高级功能,如热部署、代码加密等,但同时也需要注意类加载带来的潜在问题。

Java类加载机制详解 1. 类加载过程概述 Java类加载过程分为三个主要阶段:加载、链接和初始化。这三个阶段共同完成了将.class文件中的二进制数据转换为JVM中的Class对象的过程。 1.1 加载阶段 加载阶段是类加载的第一个阶段,主要完成以下工作: 通过类的全限定名获取定义此类的二进制字节流 将字节流所代表的静态存储结构转化为方法区的运行时数据结构 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口 2. 链接阶段 链接阶段又分为三个子阶段:验证、准备和解析。 2.1 验证 验证阶段确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证主要包括: 文件格式验证:验证字节流是否符合Class文件格式规范 元数据验证:对类的元数据信息进行语义校验 字节码验证:通过数据流和控制流分析,确定程序语义是合法的 符号引用验证:验证符号引用能否正确解析 2.2 准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。需要注意: 这时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量 初始值通常是数据类型的零值(如0、0L、null、false等) 如果类字段是常量(static final),则会被初始化为ConstantValue属性所指定的值 2.3 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析主要针对以下七类符号引用: 类或接口 字段 类方法 接口方法 方法类型 方法句柄 调用点限定符 符号引用 vs 直接引用 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量 直接引用:可以是直接指向目标的指针、相对偏移量或能间接定位到目标的句柄 3. 初始化阶段 3.1 初始化阶段的核心任务 初始化阶段是执行类构造器 <clinit>() 方法的过程,该方法由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生。 3.2 触发初始化的条件 以下情况会触发类的初始化: 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时 使用java.lang.reflect包的方法对类进行反射调用时 当初始化一个类时,发现其父类还未初始化,需先触发其父类的初始化 虚拟机启动时,用户指定的主类(包含main()方法的类)会被初始化 当使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_ getStatic、REF_ putStatic、REF_ invokeStatic的方法句柄时 4. 类加载器 Java虚拟机使用类加载器来实现类的加载动作,主要有以下几种: 4.1 Bootstrap ClassLoader(启动类加载器) 负责加载JAVA_ HOME/lib目录下的核心类库 由C++实现,是虚拟机自身的一部分 不继承java.lang.ClassLoader 4.2 Extension ClassLoader(扩展类加载器) 负责加载JAVA_ HOME/lib/ext目录下的扩展类库 由sun.misc.Launcher$ExtClassLoader实现 是Java语言实现的 4.3 Application ClassLoader(应用类加载器) 负责加载用户类路径(ClassPath)上的类库 由sun.misc.Launcher$AppClassLoader实现 是程序中默认的类加载器 4.4 类加载器的核心方法 loadClass - 类加载的入口 findClass defineClass - 将字节码转换为Class对象 resolveClass - 完成类的解析 5. 双亲委派模型 双亲委派模型的工作过程: 当一个类加载器收到类加载请求时,首先不会自己尝试加载,而是委派给父类加载器 只有当父类加载器反馈无法完成加载请求时,子加载器才会尝试自己加载 5.1 特别情况 Tomcat的Web应用类加载器(WebAppClassLoader) 每个Web应用都有自己的类加载器 打破了双亲委派模型,优先加载自己路径下的类 SPI机制 SPI(Service Provider Interface)是JDK内置的服务发现机制,典型应用场景包括: JDBC JNDI JCE JAXP JBI 6. 自定义解密类加载器实现 6.1 编写一个HelloWorld类 6.2 编写Class文件加密代码 6.3 编写自定义类加载器 6.4 使用自定义类加载器 7. 注意事项 7.1 类的唯一性 同一个类被不同的类加载器加载会被JVM视为不同的类 可能导致类型转换异常(ClassCastException) 7.2 被动引用 以下情况不会触发类的初始化: 通过子类引用父类的静态字段 通过数组定义来引用类 引用类的常量(final static) 7.3 接口初始化 接口的初始化不需要先初始化其父接口 只有当真正使用父接口时才会初始化 7.4 类卸载 满足以下条件时,类可以被卸载: 该类的所有实例都已被回收 加载该类的ClassLoader已被回收 该类对应的java.lang.Class对象没有被引用 总结 Java类加载机制是JVM的核心组成部分,理解类加载过程、类加载器的工作原理以及双亲委派模型对于深入理解Java运行机制至关重要。通过自定义类加载器可以实现许多高级功能,如热部署、代码加密等,但同时也需要注意类加载带来的潜在问题。