JVM篇-类加载机制与双亲委派机制

2109字

Java 类加载机制与双亲委派机制详解

一、类加载机制

1. 核心概念

类加载机制指 JVM 将.class字节码文件加载到内存,并对数据进行验证、解析、初始化,最终形成可被 JVM 直接使用的 Java 类型的过程。

2. 加载流程

分为三个阶段,共七个步骤:

阶段 步骤 说明
加载 1. 加载 读取字节码文件,创建 Class 对象
链接 2. 验证 验证字节码格式、安全性等
3. 准备 为静态变量分配内存并设置初始值(零值)
4. 解析 将符号引用转换为直接引用
初始化 5. 初始化 执行类构造器<clinit>()方法(静态代码块和静态变量赋值)

  • 第一步:Loading 加载

通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流
将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构
内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

总结:加载二进制数据到内存 → 映射成JVM能识别的结构 → 在内存中生成Class文件

  • 第二步:Linking 链接
    链接是指将上面创建好的 Class 类合并至 Java 虚拟机中,使之能够执行的过程,可分为验证准备解析三个阶段。

① 验证(Verify)

确保 class 文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的 class 类的正确性,不会危害到虚拟机的安全。

② 准备(Prepare)

为类中的静态字段分配内存,并设置默认的初始值,比如 int 类型初始值是 0。被final修饰的 static 字段不会设置,因为 final 在编译的时候就分配了

③ 解析(Resolve)

解析阶段的目的,是将常量池内的符号引用转换为直接引用的过程(将常量池内的符号引用解析成为实际引用)。如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)
事实上,解析器操作往往会伴随着 JVM 在执行完初始化之后再执行。 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《Java 虚拟机规范》的 Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类、接口、字段、类方法、接口方法、方法类型等。对应常量池中的 CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等。

  • 第三步:Initialization 初始化

初始化就是执行类的构造器方法<clinit>()的过程。
这个方法不需要定义,是 javac 编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并来的。
若该类具有父类,JVM会保证父类的<clinit>先执行,然后在执行子类的<clinit>


3. 触发时机

  • 创建类实例(new)
  • 访问静态变量 / 方法
  • 反射调用(Class.forName ())
  • 子类被加载时父类未初始化
  • 主类(包含 main () 方法的类)

二、双亲委派机制

1. 核心原理

  • 工作流程

    1. 类加载请求首先委派给父类加载器
    2. 父加载器无法完成时(在自己的搜索范围内找不到),子加载器才会尝试加载
  • 加载器层次

    1. 启动类加载器(Bootstrap ClassLoader)
    2. 扩展类加载器(Extension ClassLoader)
    3. 应用程序类加载器(Application ClassLoader)
    4. 自定义类加载器(User-Defined ClassLoader)

2. 优势特点

  • 安全性:防止核心 API 被篡改(如自定义 java.lang.Object 类)
  • 唯一性:保证类在各级加载器中只加载一次
  • 高效性:避免重复加载带来的资源浪费

3. 源码实现

java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Class<?> loadClass(String name, boolean resolve) 
    throws ClassNotFoundException {
    
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 2. 委托父类加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}
            
            // 3. 自行加载
            if (c == null) {
                c = findClass(name);
            }
        }
        return c;
    }
}

三、打破双亲委派机制

1. 实现方式

方法 说明 典型应用场景
自定义类加载器 重写loadClass()方法,改变委派逻辑 Tomcat 热部署
线程上下文类加载器 通过Thread.setContextClassLoader()指定加载器 JDBC 驱动加载
OSGi 模块化系统 采用网状委派模型 Eclipse 插件系统
SPI 服务发现机制 使用ServiceLoader加载实现类 Java 日志门面、数据库驱动

2. 代码示例:自定义类加载器

java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class CustomClassLoader extends ClassLoader {
    
    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
        throws ClassNotFoundException {
        
        // 1. 检查自定义路径
        if (name.startsWith("com.myapp")) {
            return findClass(name);
        }
        // 2. 其他类仍遵循双亲委派
        return super.loadClass(name, resolve);
    }
    
    @Override
    protected Class<?> findClass(String name) {
        // 自定义类加载逻辑...
    }
}

3. 典型场景分析

  • Tomcat:为每个 Web 应用创建独立的 WebAppClassLoader

    • 隔离性:不同 Web 应用的类独立加载
    • 热部署:通过重新创建类加载器实现
  • JDBC 驱动加载

    java

    1
    2
    3
    
    // 使用线程上下文类加载器
    Connection conn = DriverManager.getConnection(url);
    // DriverManager在启动类加载器中,通过ContextClassLoader加载具体驱动
    

四、总结对比

机制 优点 缺点 适用场景
双亲委派 安全性高、避免重复加载 灵活性不足 常规 Java 应用开发
自定义类加载 灵活性强、支持热部署 可能引发类冲突 容器化环境、模块热加载
上下文类加载器 解决基础类回调用户代码问题 需要显式设置 SPI 扩展机制、跨类加载器调用

面试要点:理解双亲委派的核心设计思想,掌握主流框架打破委派的实现原理,能结合具体场景说明技术选型依据。

如对内容有异议,请联系关邮箱2285786274@qq.com修改