JVM知识梳理(3)

作者:没有四次元口袋的蓝胖

日期:2026-08-01

标签:Java, JVM, 类加载, 双亲委派


JVM知识梳理(3)

类加载决定"类怎么进入内存",是 JVM 面试的另一大核心。掌握类加载过程、双亲委派原理、如何打破双亲委派。


一、类加载过程

1.1 类的生命周期

复制代码
加载 → 连接(验证 → 准备 → 解析) → 初始化 → 使用 → 卸载

1.2 加载(Loading)

任务

  1. 通过类的全限定名获取二进制字节流
  2. 将字节流代表的静态存储结构转化为方法区的运行时数据结构
  3. 在堆中生成 java.lang.Class 对象,作为方法区数据的访问入口

来源

  • 本地文件系统(.class 文件)
  • JAR 包
  • 网络(Applet)
  • 运行时计算(动态代理)

1.3 连接(Linking)

连接分为三个阶段:验证、准备、解析。

验证(Verification)

确保字节码符合 JVM 规范,不会危害虚拟机。

复制代码
验证内容:
- 文件格式验证(是否以 CAFEBABE 开头)
- 元数据验证(是否有父类、是否继承了 final 类)
- 字节码验证(字节码指令是否正确)
- 符号引用验证(类名、方法名是否存在)
准备(Preparation)

为类的静态变量分配内存并设置默认值(不是代码中的赋值)。

java 复制代码
public class Test {
    public static int a = 10;
    public static final int b = 20;
}
// 准备阶段:a = 0(int 默认值),b = 20(final 常量直接赋值)
// 初始化阶段:a = 10(执行 <clinit>() 方法)

关键区分:准备阶段设默认值(int 是 0),初始化阶段才赋代码中的值(10)。

解析(Resolution)

将常量池中的符号引用替换为直接引用(内存地址)。

复制代码
符号引用:java/lang/String(类名)
直接引用:0x7f8b2c00(内存地址)

1.4 初始化(Initialization)

执行类构造器 <clinit>() 方法,初始化静态变量和静态代码块。

java 复制代码
public class Test {
    public static int a = 10;
    static {
        b = 30;
    }
    public static int b = 20;
}
// 初始化后:a = 10, b = 20(静态代码块和静态变量按顺序执行)

注意<clinit>() 方法是编译器自动收集的静态变量赋值和静态代码块合并而成的。


二、类加载器

2.1 类加载器层次

复制代码
Bootstrap ClassLoader(启动类加载器)
    ↑
Extension ClassLoader(扩展类加载器)
    ↑
Application ClassLoader(应用程序类加载器)
    ↑
Custom ClassLoader(自定义类加载器)
类加载器 加载范围 实现者
Bootstrap JAVA_HOME/lib(如 java.lang.* JVM 实现(C++)
Extension JAVA_HOME/lib/ext Java 代码
Application 用户类路径(classpath Java 代码
Custom 自定义路径 用户代码

三、双亲委派模型

3.1 工作原理

复制代码
加载请求 → Application ClassLoader
              ↓(先委派给父加载器)
           Extension ClassLoader
              ↓(再委派给父加载器)
           Bootstrap ClassLoader
              ↓(父加载器无法加载,才自己加载)
           尝试加载 → 失败则抛出 ClassNotFoundException

核心逻辑

java 复制代码
// ClassLoader.loadClass() 简化版
protected Class<?> loadClass(String name, boolean resolve) {
    // 1. 先检查是否已加载
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        // 2. 委派给父加载器
        if (parent != null) {
            c = parent.loadClass(name);
        } else {
            // 3. 父加载器为空(Bootstrap),自己尝试加载
            c = findClass(name);
        }
    }
    return c;
}

3.2 为什么需要双亲委派?

好处

  1. 避免类重复加载:父加载器已加载的类,子加载器不会再加载
  2. 保护核心类库 :用户自定义的 java.lang.String 不会被加载,防止核心类被篡改
java 复制代码
// 用户代码
package java.lang;
public class String { } // 自定义 String

// 双亲委派保证:Bootstrap 加载的 java.lang.String 不会被覆盖
// 用户代码中的 String 会被委派给 Bootstrap 加载,最终加载的是 JDK 的 String

面试回答模板:双亲委派的好处有两个:一是避免类重复加载,父加载器加载过的类子加载器不会再加载;二是保护核心类库,用户自定义的 java.lang.String 不会被加载,防止核心类被篡改。


四、打破双亲委派

4.1 什么场景需要打破?

场景 原因
Tomcat 每个 Web 应用有独立的类加载器,互不干扰
OSGi 模块化框架,需要灵活的类加载策略
SPI 机制 JDBC、JNDI 等需要加载第三方实现

4.2 如何打破?

方法 :重写 loadClass() 方法。

java 复制代码
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        // 打破双亲委派:自己先尝试加载,失败再委派给父加载器
        try {
            Class<?> c = findClass(name);
            return c;
        } catch (ClassNotFoundException e) {
            return super.loadClass(name, resolve);
        }
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 自定义加载逻辑(如从网络、数据库加载)
        byte[] data = loadClassData(name);
        return defineClass(name, data, 0, data.length);
    }
}

4.3 SPI 机制打破双亲委派

JDBC 的 SPI 场景:

java 复制代码
// JDBC 核心接口在 Bootstrap 加载(java.sql.*)
// 但实现类(如 MySQL Driver)在 classpath 中,由 Application 加载

// 解决方案:Thread Context ClassLoader
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// 父加载器通过线程上下文加载器调用子加载器加载实现类

流程

复制代码
Bootstrap 加载 java.sql.DriverManager
    ↓
DriverManager 需要加载 com.mysql.cj.jdbc.Driver
    ↓
Bootstrap 无法加载(在 classpath 中)
    ↓
通过 Thread.currentThread().getContextClassLoader() 获取 Application 加载器
    ↓
Application 加载器加载 MySQL Driver

五、类加载器隔离与 Tomcat

5.1 Tomcat 的类加载器

复制代码
Common ClassLoader(加载 Tomcat 通用类)
    ↑
Catalina ClassLoader(加载 Tomcat 内部类)
    ↑
Shared ClassLoader(加载共享类)
    ↑
WebApp ClassLoader(每个 Web 应用独立)
    ├── /webapp1 → WebAppClassLoader1
    └── /webapp2 → WebAppClassLoader2

设计目的

  • 不同 Web 应用的类互相隔离
  • 同一份第三方库可以被多个应用共享

🗺️ 思维导图速览

复制代码
JVM 类加载
├── 类加载过程
│   ├── 加载(获取字节流 → 方法区 → Class 对象)
│   ├── 连接
│   │   ├── 验证(字节码合法性)
│   │   ├── 准备(静态变量设默认值)
│   │   └── 解析(符号引用 → 直接引用)
│   └── 初始化(执行 <clinit>(),赋实际值)
├── 类加载器层次
│   ├── Bootstrap(JDK 核心类)
│   ├── Extension(扩展类)
│   ├── Application(classpath)
│   └── Custom(自定义)
├── 双亲委派
│   ├── 原理:先委派父加载器,父加载器无法加载才自己加载
│   ├── 好处:避免重复加载、保护核心类库
│   └── 打破:重写 loadClass()
└── SPI 机制
    └── Thread Context ClassLoader 反向调用子加载器

📝 写在最后

学习建议

  1. 类加载过程要记顺序:加载→连接(验证→准备→解析)→初始化,准备阶段设默认值,初始化阶段赋实际值
  2. 双亲委派要理解为什么:不是死记硬背,理解"避免重复加载"和"保护核心类库"两个核心好处
  3. 打破双亲委派要知道场景 :Tomcat 的 WebAppClassLoader、SPI 的 Thread.currentThread().getContextClassLoader()
  4. 准备 vs 初始化 :面试经常问"静态变量 int a = 10 什么时候赋值"------准备阶段赋 0,初始化阶段赋 10

面试回答模板

Q:说说类的加载过程?

类的加载过程分为加载、连接、初始化三个阶段。加载阶段获取字节流并在方法区创建运行时数据结构,在堆中生成 Class 对象。连接阶段分为验证(字节码合法性)、准备(静态变量设默认值,如 int 是 0)、解析(符号引用转直接引用)。初始化阶段执行 <clinit>() 方法,为静态变量赋代码中的实际值。

Q:什么是双亲委派?为什么需要?

双亲委派是类加载器之间的委派机制。类加载请求先委派给父加载器处理,父加载器无法加载才自己加载。层次是 Bootstrap → Extension → Application → Custom。好处有两个:一是避免类重复加载,父加载器加载过的类子加载器不会再加载;二是保护核心类库,用户自定义的 java.lang.String 不会被加载,防止核心类被篡改。

Q:如何打破双亲委派?

通过重写 ClassLoader 的 loadClass() 方法,自己先尝试加载,失败再委派给父加载器。典型场景有:Tomcat 的每个 Web 应用有独立的 WebAppClassLoader,保证应用之间类隔离;JDBC 的 SPI 机制通过 Thread.currentThread().getContextClassLoader() 让父加载器调用子加载器加载第三方实现。

相关推荐
磁爆步兵1 天前
内存分区:程序运行的核心秘密
java·开发语言·jvm
讓丄帝愛伱1 天前
JVM线上故障快速排
jvm
落苜蓿蓝1 天前
Java 循环中对象复用导致属性覆盖?从 JVM 内存模型讲解原因
java·jvm·python
一只专注api接口开发的技术猿1 天前
OpenClaw 实战:搭建京东商品监控与简易数据分析脚本
大数据·jvm·数据库·oracle·数据分析
Tim_102 天前
【C++】021、RAII
jvm
格兰芬多呼神护卫2 天前
# Agent 企业评估方案深度分析:从“看答案”到持续质量闭环_码士教育
java·开发语言·jvm
CodeStats2 天前
【Java GC】Java JVM 垃圾回收(GC)完全指南:从内存结构到并发回收的底层原理
java·jvm·gc·垃圾回收·zgc·内存结构
ChaHae-In2 天前
JVM运行时数据区与垃圾回收
jvm
IKUN家族2 天前
Spring JVM(二)
java·jvm·spring