Java 代码编译后会变成 .class 文件,但 class 文件不会自己跑起来。它必须先被类加载器加载进 JVM,再经过验证、准备、解析、初始化等阶段,最后才能被程序使用。

一句话概括:类加载器负责把 class 字节码加载到 JVM;双亲委派负责保证核心类优先由上层加载器加载,避免重复加载和核心 API 被篡改;类加载过程则描述一个类从加载到可用的完整生命周期。

类加载器是什么

类加载器的核心职责是根据类的全限定名找到字节码,并把它加载进 JVM。

类全限定名

类加载器

查找 class 字节码

读取二进制数据

生成 Class 对象

程序可以使用这个类

JVM 中常见类加载器可以这样看:

Bootstrap ClassLoader

Extension ClassLoader

Application ClassLoader

Custom ClassLoader

类加载器 作用
Bootstrap ClassLoader 启动类加载器,加载 Java 核心类库
Extension ClassLoader 扩展类加载器,JDK 8 中用于加载扩展目录下的类
Application ClassLoader 应用类加载器,加载应用 classpath 下的类
Custom ClassLoader 自定义类加载器,用于热部署、隔离加载、插件化等场景

JDK 9 以后引入模块系统,扩展类加载器相关机制发生变化,很多场景会看到 Platform ClassLoader。面试如果围绕 JDK 8 体系展开,说 Bootstrap、Extension、Application、自定义加载器即可;如果对方追问高版本,再补充 Platform ClassLoader。

什么是双亲委派模型

双亲委派的流程是:一个类加载器收到类加载请求后,先不自己加载,而是先委托父加载器加载;父加载器无法加载时,子加载器才尝试自己加载。

不能

不能

不能

应用需要加载某个类

Application ClassLoader

委托 Extension ClassLoader

委托 Bootstrap ClassLoader

Bootstrap 能否加载

返回 Class 对象

Extension 能否加载

Application 能否加载

抛出 ClassNotFoundException

注意“双亲”不是 Java 继承里的父类,而是类加载器之间的层级关系。

为什么需要双亲委派

双亲委派主要解决两个问题:安全和唯一性。

保证核心类库安全

假设你自己写了一个类:

package java.lang;

public class String {
}

如果没有双亲委派,应用类加载器可能直接加载你写的 java.lang.String,这会破坏 Java 核心类库的安全性。

有了双亲委派后,加载 java.lang.String 会先向上委托,最终由 Bootstrap ClassLoader 加载 JDK 自带的 String,应用自己写的同名核心类不会被优先使用。

加载 java.lang.String

Application ClassLoader

向上委托

Bootstrap ClassLoader

加载 JDK 自带 String

阻止核心 API 被替换

避免类重复加载

同一个类如果被不同类加载器重复加载,在 JVM 里会被认为是不同的类。双亲委派可以让上层已经加载过的类直接复用,减少重复加载带来的混乱。

一个类在 JVM 中的身份,不只由类全限定名决定,还和加载它的类加载器有关。

类全限定名

类身份

类加载器

这也是为什么一些容器、插件化框架、热部署场景会非常关注类加载器隔离。

双亲委派会不会被打破

会,而且有些场景就是故意打破。

双亲委派是一种默认的类加载机制,不是 JVM 永远不能改变的铁律。实际工程里,为了插件隔离、应用隔离、SPI 扩展,经常会出现不完全按双亲委派走的类加载设计。

典型场景有两个。

第一个是 JDBC SPI。DriverManager 属于 JDK 核心类,由上层类加载器加载;但具体数据库驱动通常在应用 classpath 里,比如 MySQL 驱动。上层类加载器看不到应用 classpath 下的驱动类,于是需要借助线程上下文类加载器去反向加载应用侧实现。

不能

DriverManager

需要加载数据库驱动

上层类加载器能否看到驱动

使用线程上下文类加载器

加载应用 classpath 下的驱动实现

第二个是 Tomcat。每个 Web 应用需要隔离自己的依赖,否则一个应用里的 jar 很容易影响另一个应用。Tomcat 会为不同 Web 应用创建不同的 WebAppClassLoader,让应用之间的类尽量隔离。

Tomcat

Web 应用 1 类加载器

Web 应用 2 类加载器

应用 1 的类和依赖

应用 2 的类和依赖

所以回答双亲委派时可以补一句:双亲委派是默认模型,核心目标是安全和避免重复加载;但在 SPI、容器、插件化、热部署等场景中,可能会通过线程上下文类加载器或自定义类加载器有意打破。

类加载的完整过程

类从加载到卸载,大致会经历 7 个阶段:

加载

验证

准备

解析

初始化

使用

卸载

其中,验证、准备、解析三个阶段统称为连接。

连接

验证

准备

解析

加载

连接

初始化

使用

卸载

各阶段职责如下:

阶段 作用
加载 根据类名获取二进制字节流,生成类元数据和 Class 对象
验证 校验 class 文件格式、元数据、字节码、安全性
准备 为类变量分配内存并设置默认初始值
解析 把符号引用转换为直接引用
初始化 执行静态变量赋值和静态代码块
使用 程序正常访问类和对象
卸载 类加载器可回收时,对应类元数据才可能卸载

准备阶段和初始化阶段的区别

这是类加载里最容易被追问的点。

看一段代码:

public class Demo {
    static int a = 10;
    static final int b = 20;
    static final Integer c = 30;
}

在准备阶段,a 会先被设置为默认值 0,真正赋值为 10 发生在初始化阶段。

static final int b = 20 这种编译期就能确定的常量,可能在准备阶段就完成赋值。

static final Integer c = 30 是引用类型,不是简单的编译期基本类型常量,通常仍要在初始化阶段完成。

准备阶段

为 static 变量分配内存

设置默认值

特殊编译期常量可直接赋值

初始化阶段

执行静态变量显式赋值

执行静态代码块

类什么时候会初始化

类加载不等于类一定初始化。初始化通常发生在类被主动使用时。

常见主动使用场景包括:

场景 示例
创建对象 new User()
访问类静态变量 User.count
调用类静态方法 User.init()
反射调用 Class.forName("com.demo.User")
初始化子类 父类未初始化时会先初始化父类
启动主类 包含 main 方法的入口类

父类和子类初始化顺序也很常见:

class Animal {
    static int num = 1;
    static {
        System.out.println("Animal init");
    }
}

class Cat extends Animal {
    static int age = 2;
    static {
        System.out.println("Cat init");
    }
}

如果主动使用 Cat.age,会先初始化 Animal,再初始化 Cat。如果只是通过 Cat.num 访问父类静态变量,通常只会触发父类初始化。

面试回答模板

可以这样回答:

类加载器负责把 .class 字节码加载到 JVM 中,常见的有启动类加载器、扩展类加载器、应用类加载器和自定义类加载器。类加载采用双亲委派模型,一个类加载器收到加载请求后,会先委托父加载器加载,父加载器加载不了,子加载器才自己加载。这样可以保证 Java 核心类库优先由上层加载器加载,避免核心 API 被篡改,也能减少类重复加载。类加载过程包括加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析属于连接。准备阶段主要给类变量分配内存并设置默认值,初始化阶段才执行静态变量赋值和静态代码块。

小结

类加载这块抓住三句话就够稳:

类加载器负责把字节码带进 JVM;双亲委派保证核心类优先、类加载更安全;类从加载到使用要经历加载、连接、初始化等阶段。

能把这三句话展开,类加载相关面试题基本不会乱。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐