知识模块
☕ Java 知识模块
四、JVM 深入理解
类加载器

类加载器

类加载器是 JVM 的核心组件,负责将 class 文件加载到 JVM 中。理解类加载机制是深入掌握 Java 的关键。

一、类加载过程

整体流程

┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│    加载      │ -> │    连接      │ -> │    初始化    │
└─────────────┘    └─────────────┘    └─────────────┘

         ┌───────────────┼───────────────┐
         ↓               ↓               ↓
    ┌─────────┐    ┌─────────┐    ┌─────────┐
    │  验证    │    │  准备    │    │  解析    │
    └─────────┘    └─────────┘    └─────────┘

1. 加载 (Loading)

做什么

  • 通过类全限定名获取二进制字节流
  • 将字节流转化为方法区的运行时数据结构
  • 在堆中生成 Class 对象

来源

来源说明
本地文件系统class 文件
JAR 包压缩文件中的 class
网络Applet、RMI
动态代理运行时生成
其他文件JSP 编译后的 class

2. 连接 (Linking)

验证

确保 Class 文件格式正确、语义合法。

验证内容:
├── 文件格式验证(魔数 0xCAFEBABE)
├── 元数据验证(是否有父类、是否实现抽象方法)
├── 字节码验证(数据流分析)
└── 符号引用验证(引用的类/方法是否存在)

准备

为类变量分配内存并设置初始值。

// 准备阶段
public static int value = 123;
// value 在准备阶段 = 0,不是 123
 
public static final int CONST = 100;
// final 常量在准备阶段 = 100(编译期常量)

解析

将符号引用替换为直接引用。

符号引用:字面量描述(如 "java/lang/Object")
直接引用:实际内存地址或偏移量

3. 初始化 (Initialization)

执行类构造器 <clinit>() 方法。

public class InitDemo {
    static int a = 1;           // 赋值
    static {                    // 静态块
        a = 2;
    }
    
    // 编译后生成 <clinit>() 方法:
    // a = 1;
    // a = 2;
}

初始化时机(主动引用):

场景示例
new实例化new MyClass()
访问静态变量MyClass.value
访问静态方法MyClass.method()
反射Class.forName("MyClass")
初始化子类子类初始化触发父类
主类java MyClass

不会初始化(被动引用):

// 1. 通过子类引用父类静态变量
class Parent { static int a = 1; }
class Child extends Parent {}
Child.a;  // 只初始化 Parent,不初始化 Child
 
// 2. 通过数组定义引用类
Parent[] arr = new Parent[10];  // 不初始化 Parent
 
// 3. 引用常量
class Const { static final int A = 1; }
Const.A;  // 不初始化 Const(编译期常量优化)

二、类加载器层次

三层类加载器

┌─────────────────────────────────────────────┐
│           启动类加载器 (Bootstrap)            │
│         加载核心类库 (rt.jar等)               │
│              C++ 实现,无父加载器              │
└────────────────────┬────────────────────────┘

┌────────────────────┴────────────────────────┐
│           扩展类加载器 (Extension)            │
│         加载扩展类库 (ext目录)                │
│              Java 实现,父为 Bootstrap        │
└────────────────────┬────────────────────────┘

┌────────────────────┴────────────────────────┐
│           应用类加载器 (Application)          │
│         加载用户类路径 (classpath)            │
│              Java 实现,父为 Extension        │
└─────────────────────────────────────────────┘

加载器详解

加载器路径父加载器
Bootstrap$JAVA_HOME/jre/lib/rt.jar等核心库null
Extension$JAVA_HOME/jre/lib/ext/*.jarBootstrap
ApplicationclasspathExtension

代码验证

public class ClassLoaderDemo {
    public static void main(String[] args) {
        // 获取类加载器
        ClassLoader appLoader = ClassLoaderDemo.class.getClassLoader();
        System.out.println("应用类加载器: " + appLoader);
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        
        ClassLoader extLoader = appLoader.getParent();
        System.out.println("扩展类加载器: " + extLoader);
        // sun.misc.Launcher$ExtClassLoader@1540e19d
        
        ClassLoader bootLoader = extLoader.getParent();
        System.out.println("启动类加载器: " + bootLoader);
        // null(C++实现,Java中无对应对象)
        
        // 核心类由 Bootstrap 加载
        ClassLoader stringLoader = String.class.getClassLoader();
        System.out.println("String的加载器: " + stringLoader);
        // null
    }
}

三、双亲委派模型

工作原理

加载请求

┌─────────────────────────────────────────────────┐
│              应用类加载器                         │
│                                                  │
│  1. 先委托父加载器加载                            │
│  2. 父加载器无法加载,自己再尝试加载              │
└─────────────────────────────────────────────────┘
    ↓ (委托)
┌─────────────────────────────────────────────────┐
│              扩展类加载器                         │
│                                                  │
│  1. 先委托父加载器加载                            │
│  2. 父加载器无法加载,自己再尝试加载              │
└─────────────────────────────────────────────────┘
    ↓ (委托)
┌─────────────────────────────────────────────────┐
│              启动类加载器                         │
│                                                  │
│  1. 尝试加载核心类库                              │
│  2. 找到则返回,找不到返回 null                   │
└─────────────────────────────────────────────────┘

源码分析

// ClassLoader.loadClass() 核心逻辑
protected Class<?> loadClass(String name, boolean resolve) {
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 2. 委托父加载器
                    c = parent.loadClass(name, false);
                } else {
                    // 3. 父为null,委托 Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器找不到,忽略
            }
            
            if (c == null) {
                // 4. 父加载器都找不到,自己加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派的好处

1. 安全性

防止核心类被篡改:

// 自定义 java.lang.String
package java.lang;
 
public class String {
    public String() {
        // 恶意代码
    }
}
 
// 由于双亲委派,Bootstrap 会加载 rt.jar 中的 String
// 自定义的 String 永远不会被加载

2. 唯一性

保证类的全局唯一性:

无论哪个加载器加载 java.lang.Object
最终都由 Bootstrap 加载
JVM 中只有一份 Object.class

四、打破双亲委派

何时需要打破?

场景说明
SPI 机制JDBC 等接口由 Bootstrap 加载,实现类在 classpath
热部署需要卸载和重新加载类
隔离容器Tomcat 等容器需要隔离不同应用

方式一:重写 loadClass

public class CustomClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 打破双亲委派:先自己加载,失败再委托父加载器
        try {
            return findClass(name);
        } catch (ClassNotFoundException e) {
            return super.loadClass(name);
        }
    }
}

方式二:线程上下文加载器

JDBC 的实现方式:

// DriverManager 在 rt.jar,由 Bootstrap 加载
// Driver 实现类(如 MySQL Driver)在 classpath
 
public class DriverManager {
    static {
        // 使用线程上下文类加载器
        // 默认是 AppClassLoader
        AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // ...
            return null;
        });
    }
}
 
// ServiceLoader.load() 使用上下文类加载器
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

方式三:OSGi 模块化

OSGi 类加载机制:
├── 每个模块(Bundle)有自己的类加载器
├── 加载顺序:本模块 → 依赖模块 → 父加载器
└── 实现了类加载器的网状结构

五、Tomcat 类加载机制

Tomcat 类加载器层次

┌─────────────────────────────────────────────────┐
│              Bootstrap (启动类加载器)             │
└────────────────────┬────────────────────────────┘

┌────────────────────┴────────────────────────────┐
│              System (系统类加载器)               │
│           加载 Tomcat 自身类                      │
└────────────────────┬────────────────────────────┘

┌────────────────────┴────────────────────────────┐
│              Common (公共类加载器)               │
│         加载 Tomcat 和应用共享的类               │
└────────────────────┬────────────────────────────┘

        ┌────────────┴────────────┐
        ↓                         ↓
┌───────────────┐         ┌───────────────┐
│   Catalina    │         │   Shared      │
│ 类加载器       │         │ 类加载器       │
│ (Tomcat内部)   │         │ (应用共享)     │
└───────────────┘         └───────┬───────┘

                     ┌────────────┴────────────┐
                     ↓                         ↓
              ┌─────────────┐          ┌─────────────┐
              │  WebApp1    │          │  WebApp2    │
              │ 类加载器     │          │ 类加载器     │
              └─────────────┘          └─────────────┘

隔离原理

每个 WebApp 有独立的类加载器:

  • 不同应用可以使用不同版本的类库
  • 应用之间互不影响
  • 实现了 Web 应用的隔离

六、常见面试题

Q1: 类加载器为什么采用双亲委派?

安全性

  • 防止核心类库被篡改
  • 自定义的 java.lang.String 无法被加载

唯一性

  • 保证类的全局唯一性
  • 不同加载器加载同一个类,得到的是不同的 Class 对象

Q2: 如何自定义类加载器?

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[] data = loadClassData(name);
            return defineClass(name, data, 0, data.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name);
        }
    }
    
    private byte[] loadClassData(String name) throws IOException {
        String fileName = classPath + name.replace('.', '/') + ".class";
        FileInputStream fis = new FileInputStream(fileName);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }
        fis.close();
        return bos.toByteArray();
    }
}
 
// 使用
MyClassLoader loader = new MyClassLoader("/custom/path/");
Class<?> clazz = loader.loadClass("com.example.MyClass");

Q3: JDBC 如何打破双亲委派?

问题:
- DriverManager 由 Bootstrap 加载
- Driver 实现类(如 MySQL Driver)在 classpath
- Bootstrap 无法加载 classpath 下的类

解决方案:
- 使用线程上下文类加载器
- ServiceLoader.load() 使用 AppClassLoader 加载实现类

Q4: 为什么不同加载器加载同一个类是不同的类?

// 同一个类文件,不同加载器加载
MyClassLoader loader1 = new MyClassLoader("/path/");
MyClassLoader loader2 = new MyClassLoader("/path/");
 
Class<?> c1 = loader1.loadClass("com.example.Test");
Class<?> c2 = loader2.loadClass("com.example.Test");
 
System.out.println(c1 == c2);  // false
 
// 在 JVM 中,类的唯一标识 = 全限定名 + 类加载器
// c1 和 c2 虽然全限定名相同,但加载器不同,所以是不同的类

Q5: 如何判断两个类是否相同?

JVM 判断两个类相同的条件:

  1. 全限定名相同
  2. 类加载器相同
// 即使是同一个 class 文件
// 不同类加载器加载后,instanceof 返回 false
 
Object obj = c1.newInstance();
System.out.println(obj instanceof c2);  // false

小结

概念要点
类加载过程加载 → 连接(验证、准备、解析) → 初始化
三层加载器Bootstrap → Extension → Application
双亲委派先委托父加载器,父加载不了再自己加载
打破双亲委派重写 loadClass / 上下文加载器 / OSGi
Tomcat 隔离每个 WebApp 独立的类加载器

核心原则

  • 双亲委派保证安全性和唯一性
  • 必要时可打破双亲委派(SPI、热部署、容器隔离)
  • 类的唯一性由全限定名 + 类加载器共同决定