Skip to content

类加载过程:Loading

类加载:把字节码变成可用的 Class 对象

当程序第一次用到某个类时——比如 new Student()Class.forName("Student")、或者访问 Student.class——JVM 需要先把 Student.class 文件加载进内存,变成一个 Class 对象,才能使用它。

这个过程就是类加载(Class Loading)

类加载的完整生命周期

类从加载到卸载,经历以下阶段:

加载(Loading)


  链接(Linking)
   ├─ 验证(Verification)
   ├─ 准备(Preparation)
   └─ 解析(Resolution)


初始化(Initialization)


  使用(Using)


  卸载(Unloading)

其中加载、验证、准备、初始化、卸载是确定的执行顺序,而解析可以在初始化之后再进行(晚期绑定)。

Loading:加载阶段做了什么

加载阶段的核心任务是:通过类的全限定名找到字节码文件,并将其转换为方法区中的运行时数据结构,同时在堆中生成一个 java.lang.Class 对象作为访问入口

三件事

  1. 通过类的全限定名获取字节码

类加载器根据类的全限定名(如 com.example.Student)到特定位置查找 .class 文件。

不同类加载器的查找路径不同:

类加载器加载路径
引导类加载器JAVA_HOME/jre/lib/rt.jar 等核心 JAR
扩展类加载器JAVA_HOME/jre/lib/ext/*.jar
应用类加载器classpathjava -cp-jar 指定)
  1. 将字节流转换为方法区的运行时数据结构

字节码文件(Class 文件)是一个二进制流。加载时,JVM 解析这个二进制流,在方法区中创建类的内部表示结构:

ClassFile 二进制流

解析

方法区中的类结构
├─ 类的元数据(名字、父类、修饰符)
├─ 字段表(有哪些属性)
├─ 方法表(有哪些方法,包含字节码)
└─ 常量池(各种符号引用)
  1. 在堆中生成 Class 对象

这是关键一步:加载完成后,JVM 会在堆内存中创建一个 java.lang.Class 对象。Java 程序通过这个 Class 对象来访问方法区中的类数据。

java
public class LoadingDemo {
    public static void main(String[] args) throws ClassNotFoundException {
        // 三种获取 Class 对象的方式

        // 方式一:通过类的 class 属性
        Class<String> c1 = String.class;

        // 方式二:通过对象的 getClass() 方法
        String s = "hello";
        Class<?> c2 = s.getClass();

        // 方式三:通过 Class.forName() 触发加载
        Class<?> c3 = Class.forName("java.lang.String");

        // 三种方式拿到的是同一个 Class 对象
        System.out.println(c1 == c2);  // true
        System.out.println(c2 == c3);  // true
    }
}

注意:Class 对象在中,类的内部数据(字节码、元数据)在方法区中。

数组类的加载

数组类的加载比较特殊,因为它没有字节码文件。数组类由 JVM 直接在内存中构造。

java
public class ArrayLoading {
    public static void main(String[] args) {
        // 数组类型不需要 ClassLoader 加载
        int[] arr1 = new int[10];
        String[] arr2 = new String[10];

        // 数组类的 Class 对象
        System.out.println(arr1.getClass());  // class [I
        System.out.println(arr2.getClass());  // class [Ljava.lang.String;

        // [I  表示 int[]
        // [Ljava.lang.String; 表示 String[]
        // [[[I 表示 int[][][]
    }
}

数组类的加载规则:

  • 如果数组的组件类型(去掉一层数组后的类型)是引用类型,数组类由加载该组件类型的类加载器加载
  • 如果数组的组件类型是基本类型(如 int[]),数组类不由任何类加载器加载,而是由 JVM 直接创建

加载阶段与命名空间

每个类加载器都有自己独立的命名空间:由该类加载器及其所有父加载器所加载的类的集合。

java
public class NamingSpace {
    public static void main(String[] throws Exception) {
        // 自定义类加载器
        ClassLoader myLoader = new MyClassLoader();

        // 用自定义加载器加载一个类
        Class<?> clazz = myLoader.loadClass("com.example.Test");

        // 用系统类加载器尝试加载同一个类
        Class<?> systemClazz = Class.forName("com.example.Test");

        // 结果:不是同一个类!
        System.out.println(clazz == systemClazz);  // false
    }
}

同一个类(同一个全限定名)可以被不同类加载器加载,它们在 JVM 中是两个不同的类。这在 OSGi、热部署等场景下是核心机制。

加载的结束时机

一个类在以下情况下会被完全加载:

  1. 当字节码被成功读取、解析并创建了 Class 对象
  2. 当所有相关的类变量(static 字段)和静态代码块(static {})都准备好

注意:加载和初始化是不同的阶段。加载完成不代表初始化完成。关于初始化的详细规则,我们会在 类加载过程:Initialization 中深入讲解。

加载与验证的关系

很多人混淆加载和验证。简单说:

  • 加载:把字节码从文件读取到内存
  • 验证:检查读取进来的字节码是否合法

它们是链接(Linking)阶段的第一步,可以理解为:加载过程的后半段会穿插进行验证。

本节小结

加载阶段的核心任务:

  1. 通过全限定名定位字节码文件
  2. 将字节码转换为方法区的运行时数据结构
  3. 在堆中创建 Class 对象,作为程序访问类数据的入口

理解加载阶段的关键是记住:类信息存储在方法区,但 Class 对象存在于堆中

下一节,我们来看加载之后发生的事情——类加载过程:Linking(验证/准备/解析)

基于 VitePress 构建