当前位置:首页 > 技术 > 正文内容

JVM类加载体系与双亲委派模型深度解析

访客 技术 2026年5月30日 1

类加载的本质

Java程序的执行始于源码编译,终于虚拟机运行。编译阶段将.java文件转换为字节码.class文件,而真正赋予这些字节码生命的是JVM的类加载子系统。类加载并非一次性将所有类装入内存,而是遵循按需加载、只加载一次的原则——当执行路径首次触及某个类时,JVM才会触发其加载流程。

类加载器的层级架构

JVM通过不同的加载器分工协作,处理来自不同源的字节码:

加载器职责范围实现方式
引导类加载器 (Bootstrap)$JAVA_HOME/jre/lib 下的核心类库JVM原生实现,非Java类
扩展类加载器 (Extension)$JAVA_HOME/jre/lib/ext 目录及扩展包sun.misc.Launcher$ExtClassLoader
应用类加载器 (Application)用户类路径(CLASSPATH)下的类sun.misc.Launcher$AppClassLoader
自定义类加载器网络、数据库、加密文件等非标准来源继承java.lang.ClassLoader

双亲委派机制的工作逻辑

假设需要加载一个名为com.example.Demo的类,加载流程如下:

  1. 应用类加载器收到请求,不立即处理,委托给父加载器
  2. 扩展类加载器继续向上委托至引导类加载器
  3. 引导类加载器尝试加载,若失败则向下返回
  4. 扩展类加载器尝试加载,失败再向下返回
  5. 应用类加载器最终尝试自身加载

这种自下而上委托、自上而下加载的模式,核心目的在于保障核心类库的安全性——防止用户自定义的java.lang.String等类替换系统核心类。

源码层面的实现

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 查询缓存
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    // 递归委托父加载器
                    c = parent.loadClass(name, false);
                } else {
                    // 已达顶层,调用原生方法
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器无法完成加载
            }
            if (c == null) {
                // 父加载器均失败,自身尝试查找
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

验证实验

自定义测试类验证加载器层级:

public class LoaderHierarchy {
    public static void main(String[] args) {
        ClassLoader appLoader = LoaderHierarchy.class.getClassLoader();
        System.out.println("当前加载器: " + appLoader.getClass().getSimpleName());
        
        ClassLoader extLoader = appLoader.getParent();
        System.out.println("父加载器: " + extLoader.getClass().getSimpleName());
        
        ClassLoader bootLoader = extLoader.getParent();
        System.out.println("引导加载器: " + bootLoader); // 输出 null
    }
}

运行结果中引导加载器显示为null,印证其由JVM内部实现的特性。

类加载的完整生命周期

阶段一:加载 (Loading)

通过全限定名获取二进制字节流,将其转化为方法区的运行时数据结构,并在堆中生成java.lang.Class对象作为访问入口。字节流来源包括:本地文件系统、Jar压缩包、网络传输、运行时动态生成(如代理类)等。

阶段二:链接 (Linking)

验证 (Verification)

确保字节流符合Class文件格式规范,包含:

  • 文件格式校验:魔数0xCAFEBABE、版本号、常量池合法性
  • 元数据校验:继承关系合法性、抽象方法实现完整性
  • 字节码校验:操作数栈平衡、类型转换有效性
  • 符号引用校验:全限定名能否定位到目标、访问权限是否合规

准备 (Preparation)

为类变量(static修饰)分配内存并设置默认初始值。注意此时仅分配零值,真正的赋值在初始化阶段完成。

public class PrepareDemo {
    private static int counter = 10;  // 准备阶段: counter = 0
    private static final int MAX = 100; // 准备阶段: MAX = 100 (编译期常量直接赋值)
}

解析 (Resolution)

将常量池中的符号引用转换为直接引用:

引用类型符号引用示例直接引用形式
类/接口com/service/UserService方法区中的内存地址
字段UserService::dao偏移量
方法UserService::findById方法表索引或入口地址

阶段三:初始化 (Initialization)

执行<clinit>方法(由编译器自动收集类中所有静态变量的赋值动作和静态代码块合并产生)。执行顺序严格遵循源码中的声明顺序:

public class StaticInitOrder {
    static {
        System.out.println("静态块1执行, value=" + value); // 输出0
    }
    
    private static int value = initValue();
    
    static {
        System.out.println("静态块2执行, value=" + value); // 输出100
    }
    
    private static int initValue() {
        System.out.println("initValue()被调用");
        return 100;
    }
    
    public static void main(String[] args) {
        // 触发初始化时输出:
        // 静态块1执行, value=0
        // initValue()被调用
        // 静态块2执行, value=100
    }
}

打破常规的自定义加载器

网络类加载器实现

public class RemoteClassLoader extends ClassLoader {
    private final String serverBase;
    
    public RemoteClassLoader(String baseUrl) {
        this.serverBase = baseUrl.endsWith("/") ? baseUrl : baseUrl + "/";
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] bytes = fetchFromRemote(name);
        if (bytes == null) {
            throw new ClassNotFoundException("无法从远程获取: " + name);
        }
        return defineClass(name, bytes, 0, bytes.length);
    }
    
    private byte[] fetchFromRemote(String className) {
        String path = serverBase + className.replace('.', '/') + ".class";
        try (InputStream is = new URL(path).openStream();
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            
            byte[] buf = new byte[8192];
            int len;
            while ((len = is.read(buf)) != -1) {
                bos.write(buf, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            return null;
        }
    }
}

使用方式

public class DynamicLoadDemo {
    public static void main(String[] args) throws Exception {
        RemoteClassLoader loader = new RemoteClassLoader("http://192.168.1.100/classes/");
        Class<?> clazz = loader.loadClass("plugin.ReportGenerator");
        
        Object instance = clazz.getDeclaredConstructor().newInstance();
        // 通过反射调用方法...
    }
}

双亲派派的变体场景

某些框架需要打破标准委派模型:

  • Tomcat的Web应用隔离:不同Web应用可能依赖同一库的不同版本,通过自定义加载器实现类空间隔离
  • OSGi模块化:基于网状加载器结构,模块间按需导入导出包
  • JDBC的SPI机制:DriverManager由引导加载器加载,但需要加载厂商实现的驱动类,此时需要线程上下文类加载器Thread.currentThread().getContextClassLoader())完成反向委托

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。