类加载机制和双亲委派模型是什么?

0 阅读7分钟

一、什么是类加载机制

Java 程序里的类,只有在 JVM 把它加载到内存后,才能被使用。

所谓类加载机制,就是 JVM 把类从字节码文件加载到内存,并转换成可以运行的 Class 对象的过程。

一个类从加载到可用,通常会经历这几个阶段:

  • 加载(Loading)
  • 验证(Verification)
  • 准备(Preparation)
  • 解析(Resolution)
  • 初始化(Initialization)

有时还会补充:

  • 使用(Using)
  • 卸载(Unloading)

二、类加载的生命周期

1. 加载(Loading)

这一阶段主要做三件事:

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

通俗讲:
就是把 .class 文件内容读进来,并在 JVM 里生成这个类的“身份证”。


2. 验证(Verification)

验证字节码是否符合 JVM 规范,防止非法字节码破坏虚拟机。

常见验证包括:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

作用就是保证:
这个类是安全的、合法的、能被 JVM 正常执行。


3. 准备(Preparation)

给类变量,也就是 static 变量 分配内存,并设置默认初始值

例如:

public static int a = 10;

在准备阶段,a 先被赋值为默认值 0,不是 10

注意:

  • 这里只处理 类变量
  • 实例变量不会在这一步处理,实例变量跟对象一起分配在堆中

4. 解析(Resolution)

把常量池中的符号引用替换成直接引用

比如:

  • 类名
  • 方法名
  • 字段名

原来只是字符串形式的“符号”,这一步会变成 JVM 能直接定位到的内存地址或偏移量。


5. 初始化(Initialization)

这一步才真正执行类中写的初始化逻辑。

主要就是执行类构造器 <clinit>() 方法。

<clinit>() 是由编译器自动收集下面这些内容组合生成的:

  • 静态变量的显式赋值
  • 静态代码块

例如:

public class Test {
    static int a = 10;
    static {
        System.out.println("init");
    }
}

在初始化阶段才会真正执行:

  • a = 10
  • 静态代码块

三、类什么时候会初始化

并不是类一加载就立刻初始化。

只有在首次主动使用类时,才会触发初始化。

常见主动使用场景:

  • new 一个对象
  • 访问类的静态变量(非 final 常量)
  • 调用类的静态方法
  • 反射调用
  • 初始化子类时,父类先初始化
  • JVM 启动时加载主类

例如:

Class.forName("com.test.User");

这通常也会触发类初始化。


四、什么是双亲委派模型

双亲委派模型,本质上是 类加载器的加载规则

当一个类加载器收到类加载请求时,它不会先自己加载,而是先把请求委托给父加载器 去完成。
只有当父加载器无法完成时,子加载器才会自己尝试加载。

这就是“双亲委派”。


五、Java 中常见类加载器

1. 启动类加载器(Bootstrap ClassLoader)

  • 最顶层加载器

  • 负责加载 JAVA_HOME/lib 核心类库

  • 比如 rt.jar(旧版本)、核心基础类等

  • 例如:

    • java.lang.String
    • java.util.*

它不是 Java 对象实现的,通常由 C/C++ 实现。


2. 扩展类加载器(Extension ClassLoader)

JDK 9 之前常这样叫,后面是平台类加载器的概念更常见。

  • 负责加载扩展类库
  • 比如 jre/lib/ext 下的类库(旧版本理解)

3. 应用程序类加载器(Application ClassLoader)

也叫系统类加载器。

  • 负责加载应用程序 classpath 下的类
  • 我们自己写的大部分类默认都是它加载的

4. 自定义类加载器

开发中也可以自己继承 ClassLoader 实现自定义加载逻辑,比如:

  • 热部署
  • 模块隔离
  • OSGi
  • Tomcat 多应用隔离
  • 加密类加载

六、双亲委派的工作过程

比如现在应用类加载器要加载 java.lang.String

第一步

应用类加载器先不自己加载,而是把请求交给父加载器。

第二步

父加载器继续向上委托,最终交给启动类加载器。

第三步

启动类加载器发现 java.lang.String 是核心类,能够加载,于是就完成加载。

第四步

如果父加载器找不到这个类,才会逐层往下,由子加载器自己尝试加载。


七、双亲委派的优点

1. 避免类重复加载

如果每个加载器都自己加载,可能同一个类被加载多次。

双亲委派能尽量保证:
一个类优先由最上层合适的加载器统一加载。


2. 保证 Java 核心类库安全

比如 java.lang.String 这种类,只能由启动类加载器优先加载。

这样就防止有人自己写一个假的 java.lang.String 放到 classpath 里,篡改核心类。

这是双亲委派非常重要的意义。


八、双亲委派是不是父子继承关系

不是。

这里的“父子”是组合关系/委托关系,不是 Java 类继承关系。

也就是说:

  • 类加载器之间不是面向对象里的父类子类概念
  • 而是“收到请求先往上委托”的层级关系

九、为什么会有“破坏双亲委派”

有些场景下,必须打破双亲委派,否则做不到隔离或灵活加载。

典型场景:

1. Tomcat

Tomcat 里不同 Web 应用可能依赖同名不同版本的类。

如果严格双亲委派,所有应用都走同一个上层加载器,就无法做到应用隔离。

所以 Tomcat 会使用更复杂的类加载机制。


2. SPI 机制

比如 JDBC 驱动。

接口在 rt.jar 或核心类库中,由启动类加载器加载;
但具体实现类在应用的 classpath 中,由应用类加载器加载。

这时就需要通过 线程上下文类加载器 来反向加载,从而“突破”双亲委派。


十、“类加载机制”

Java 的类加载机制是指 JVM 把类的字节码文件加载到内存,并转换为可使用的 Class 对象的过程。类加载主要经历加载、验证、准备、解析、初始化几个阶段。其中加载是读取类字节码并生成 Class 对象,验证是保证字节码合法安全,准备阶段为静态变量分配内存并赋默认值,解析阶段把符号引用转成直接引用,初始化阶段执行静态变量赋值和静态代码块,也就是执行 <clinit> 方法。类通常在首次主动使用时才会被初始化。


十一、“双亲委派模型”

双亲委派模型是 Java 类加载器的一种工作机制。当一个类加载器收到类加载请求时,它不会先自己尝试加载,而是先把请求委托给父加载器;只有父加载器无法完成加载时,子加载器才会自己加载。这样做的好处是避免类被重复加载,同时保证 Java 核心类库的安全性,防止核心类被应用程序篡改。


十二、背诵版

Java 类加载机制是指 JVM 将类的字节码加载到内存并转化为 Class 对象的过程,主要包括加载、验证、准备、解析和初始化五个阶段。加载阶段负责读取字节码并生成 Class 对象,准备阶段为静态变量分配内存并赋默认值,初始化阶段执行静态变量显式赋值和静态代码块。双亲委派模型是类加载器的加载规则,即类加载请求先委托给父加载器处理,只有父加载器无法加载时,子加载器才自己加载。这样可以避免重复加载,并保证核心类库的安全。


十三、面试高频追问

1. static final 常量会触发类初始化吗?

如果是编译期常量,一般不会触发,因为值已经放到调用方常量池里了。

2. Class.forName()ClassLoader.loadClass() 区别

  • Class.forName():通常会触发初始化
  • loadClass():一般只加载,不一定初始化

3. 为什么自定义的 java.lang.String 不会生效

因为双亲委派下,启动类加载器会优先加载核心类库里的 String

4. 两个类相等的条件是什么

不仅类的全限定名要相同,还必须由同一个类加载器加载
否则 JVM 会认为它们是两个不同的类。