类加载机制与模块系统(Class Loading & JPMS)

举报
喵手 发表于 2026/01/15 17:26:52 2026/01/15
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

概览(快速看点)

  • Java 类加载分五步:加载 → 验证 → 准备 → 解析 → 初始化
  • ClassLoader 层次:Bootstrap / Platform(Extension) / Application (System)。默认采用 双亲委派(parent-first)
  • 自定义类加载器常用于插件隔离、热替换、动态卸载,但要小心类冲突ClassLoader 泄露(内存泄漏)。
  • JPMS(Java Platform Module System)提供“模块化封装 + 服务声明/发现”的静态保证,但也可以和自定义类加载器结合用于插件化。
  • 热部署通常靠“替换 ClassLoader 并丢弃旧 ClassLoader 的引用”或使用 java.lang.instrument 做类重定义(有局限:不能删字段/方法签名改变等)。

1. 类加载流程(加载、验证、准备、解析、初始化)

简要流程与每步要点:

  1. 加载(Loading)

    • JVM 找到类的二进制字节码(通常来自 .class 文件或 JAR),并由某个 ClassLoader 定位、读取并创建 Class 的内部表示(java.lang.Class)。
  2. 验证(Verification)

    • 检查字节码结构、类型安全、控制流约束等,防止非法或恶意字节码。
  3. 准备(Preparation)

    • 为类的静态变量(static fields)分配内存并设置默认初始值(0/null/false),但不执行静态赋值语句。
  4. 解析(Resolution)

    • 把符号引用解析为直接引用(例如方法、字段、类引用),依赖的类会被触发加载(可能递归)。
  5. 初始化(Initialization)

    • 执行静态初始化块和静态字段的显式初始化(即 <clinit> 方法),在此阶段类才真正“就绪”。

注意:类的初始化是线程安全且按需触发的(首次主动使用时触发,例如 new、调用静态方法或访问静态字段)。

2. 类加载器层次与双亲委派(Parent Delegation)

层次(典型)

  • Bootstrap ClassLoader:用本地代码实现,加载核心 JDK 类(rt.jar 或模块运行时)。
  • Platform(以前是 Extension)ClassLoader:加载 JDK 平台类(JDK 9+ 中划分不同),可包含平台库。
  • Application/System ClassLoader:加载应用类路径(-cp/CLASSPATH)下的类。
  • 自定义 ClassLoader:通常继承 URLClassLoaderClassLoader,用于加载第三方 JAR/插件。

双亲委派(默认)

加载某个类时,ClassLoader 会先把请求交给父 ClassLoader,父若找不到再自己查找。好处:避免重复加载核心类,保证一致性;坏处:不利于插件“重载/隔离”同名类。

子优先(child-first)或自定义策略

插件系统常需要“子优先”以隔离插件依赖。实现方法通常是重写 loadClass,先尝试自身查找再委派父加载(见后面代码示例)。但要小心核心 API/共享接口类必须来自父加载器,否则会出现 ClassCastException(因为同名类来自不同 ClassLoader 被认为是不同类型)。

3. 自定义类加载器与资源隔离(示例 + 要点)

简单的插件 ClassLoader(基于 URLClassLoader,child-first)

public class PluginClassLoader extends URLClassLoader {
    public PluginClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // 1) 已加载则直接返回
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            // 2) 尝试子类加载(避免对核心包做子优先)
            if (!name.startsWith("java.") && !name.startsWith("com.myapp.shared")) {
                try {
                    c = findClass(name);
                } catch (ClassNotFoundException ignored) {
                    // ignore -> delegate to parent
                }
            }
            // 3) 委派给父加载器(系统默认行为)
            if (c == null) {
                c = super.loadClass(name, resolve);
            }
        }
        if (resolve) resolveClass(c);
        return c;
    }
}

关键点

  • findLoadedClass + findClass + super.loadClass 的顺序决定 child-first / parent-first 行为。
  • 必须 避免java.* 或应用共享接口包做子优先(否则会产生类型不兼容)。
  • URLClassLoader 自 Java 9 起实现了 close(),加载的 JAR 的文件句柄可在 close() 后释放(有利于卸载/替换)。

插件资源(Properties、静态单例、Thread)

  • 插件应尽量避免创建全局静态单例或非守护线程(daemon=false),否则卸载时这些资源会引用 ClassLoader,阻止卸载。
  • 任何注册到 JVM 全局表(例如 JDBC DriverManager、MBeanServer)都应在卸载前显式反注册。

4. 模块系统(JPMS)速览:module-info.java、可见性、服务(uses/provides)

JPMS 在 Java 9 引入,目的是为 Java 提供“编译时与运行时的模块化”。它与 ClassLoader 并不完全替代关系,而是叠加在类加载之上。

module-info.java 示例

module com.example.app {
    requires java.logging;
    requires com.example.api;   // 依赖另一个模块
    exports com.example.app.api; // 向外暴露包
    uses com.example.spi.Plugin; // 声明使用某个服务接口
}

服务(Service Loader)

模块可以声明 provides ... with ... 来实现服务,调用方使用 ServiceLoader 发现实现:

// provider module
module com.example.plugin {
    requires com.example.api;
    provides com.example.spi.Plugin with com.example.plugin.impl.PluginImpl;
}

// consumer code
ServiceLoader<Plugin> loader = ServiceLoader.load(Plugin.class);
for (Plugin p : loader) { p.start(); }

模块可见性与封装

  • exports:包向其它模块可见(但导出是按包的,不按类)。
  • opens:对反射开放(运行时反射/框架如 Jackson、Spring 需要)。
  • JPMS 会在模块层面限制反射访问(默认更严格),这影响依赖注入/框架需要 opens 的情形。

动态模块加载(ModuleLayer)

JPMS 支持创建新的 ModuleLayer 以加载模块(可用于插件),示例代码见下文“JPMS 插件框架”部分。注意:模块层可被丢弃以允许卸载(条件复杂),但通常比基于 ClassLoader 的方案更复杂。

5. 热部署 / 重新加载策略

常见做法(优缺点)

  1. ClassLoader 替换(最常用)

    • 每次 reload 都创建一个新的 ClassLoader,加载新版本类,切换引用到新实例,丢弃旧 ClassLoader 的强引用并触发 GC。
    • 优点:简单、可卸载(理论上),支持完全替换类。
    • 缺点:必须清理资源(线程、注册、静态引用),且类实例间不能直接共享非接口类型(要通过 shared 接口或反射/Adapter)。
  2. Instrumentation(java.lang.instrument)

    • 可以在运行时重定义类(redefineClassesretransformClasses)。
    • 优点:无需替换 ClassLoader,能修改方法实现。
    • 缺点:不能改变类的结构(不能新增/删除字段或方法签名),使用门槛和限制高。
  3. 框架支持(Spring DevTools / JRebel)

    • Spring Boot 的 DevTools 使用两个 ClassLoader 机制(重启应用上下文),且能自动检测类文件变动。JRebel 为商业产品,能更细粒度热替换。

推荐策略(生产环境)

  • 开发环境:用 DevTools / JRebel 提高效率。
  • 生产环境:推荐使用“灰度替换”或蓝绿部署,而不是频繁热替换;若必须热替换,采用 ClassLoader 替换 + 严格的资源清理 + 完整测试。

6. 实战练习:实现一个简单插件框架(可卸载的模块)

我给你两种实现思路与代码片段:A. 传统 ClassLoader 插件(轻量、实战常用)与 B. JPMS 模块化插件(更现代、严格封装)。

A. 传统 ClassLoader 插件框架(核心思路)

  • 每个插件打成独立 JAR,必须暴露一个工厂类或实现共享接口 com.myapp.Plugin
  • 插件由 PluginManager 管理:加载(创建 PluginClassLoader),激活(newInstance 并调用 start),卸载(stop -> 关闭 classloader -> 清理资源 -> 丢弃引用)。

接口定义

package com.myapp.plugin;

public interface Plugin {
    void start();
    void stop();
}

PluginDescriptor(元数据)

public class PluginDescriptor {
    private final String id;
    private final Path jarPath;
    private final URL[] urls;
    private final String mainClass; // fully-qualified plugin impl class
    // constructors + getters
}

PluginManager(关键方法)

public class PluginManager {
    private final Map<String, PluginClassLoader> loaders = new ConcurrentHashMap<>();
    private final Map<String, Plugin> instances = new ConcurrentHashMap<>();

    public void load(PluginDescriptor desc) throws Exception {
        URL[] urls = new URL[]{desc.getJarPath().toUri().toURL()};
        PluginClassLoader loader = new PluginClassLoader(urls, getClass().getClassLoader());
        loaders.put(desc.getId(), loader);

        Class<?> clazz = loader.loadClass(desc.getMainClass());
        Plugin plugin = (Plugin) clazz.getDeclaredConstructor().newInstance();
        instances.put(desc.getId(), plugin);
        plugin.start();
    }

    public void unload(String id) throws Exception {
        Plugin plugin = instances.remove(id);
        if (plugin != null) {
            plugin.stop();
        }
        PluginClassLoader loader = loaders.remove(id);
        if (loader != null) {
            // 必须先确保 plugin.stop() 中释放所有全局注册
            loader.close(); // Java 7+ URLClassLoader 有 close()
        }
        // 进一步:清理 Thread locals、关闭线程池、解除 JDBC Driver
        System.gc(); // 提示 JVM 回收(不能保证)
    }
}

插件实现要点(开发者约束)

  • 不要持有对系统类或应用类的可变静态引用(会导致泄露)。
  • stop() 中释放资源:停止线程、取消定时任务、反注册 JDBC 驱动与 MBeans。
  • 使用共享的接口包(例如 com.myapp.plugin.api)由主应用加载(父加载器),插件通过父加载器可见该接口。

B. JPMS(ModuleLayer)方式:模块化插件(示例)

适用于需要模块化封装和服务发现时。基本步骤:

  1. 每个插件是一个模块 JAR(含 module-info.class)。
  2. 主应用使用 ModuleFinder 找到插件模块并在新的 ModuleLayer 中解析与定义。
  3. 通过 ModuleLayer 的类加载器或 ServiceLoader 加载服务实现。
  4. 卸载时丢弃 ModuleLayer 的引用并清理全局注册。

核心代码(加载模块)

Path pluginPath = Paths.get("/path/to/plugin.jar");
ModuleFinder finder = ModuleFinder.of(pluginPath);

ModuleLayer parent = ModuleLayer.boot();
Configuration parentConfig = parent.configuration();

// root module names (插件 module 名称)
Set<String> roots = Set.of("com.example.pluginmodule");

Configuration cf = parentConfig.resolveAndBind(finder, ModuleFinder.of(), roots);

// 使用同一个 ClassLoader(也可以自定义)
ClassLoader scl = ClassLoader.getSystemClassLoader();

ModuleLayer layer = parent.defineModulesWithOneLoader(cf, scl);

// 获取模块类加载器
ClassLoader pluginLoader = layer.findLoader("com.example.pluginmodule");

// 通过 ServiceLoader 从该 module layer 加载服务
ServiceLoader<MyService> loader = ServiceLoader.load(layer, MyService.class);
for (MyService s : loader) {
    s.start();
}

注意:要实现真正卸载,需确保丢弃对 ModuleLayer、module 里的静态引用和由 module 注册的全局对象的引用,使之可被 GC。JPMS 的卸载语义比较复杂,不像简单 ClassLoader 那样直观。

7. 常见陷阱与如何避免(实战建议)

  1. 类冲突 / ClassCastException

    • 原因:同一个类名由不同 ClassLoader 加载(类型不兼容)。
    • 规避:把共享 API(接口/抽象类)放到主应用的 classpath(由父加载器加载),插件只实现接口,不复制接口类。
  2. ClassLoader 泄露(内存泄露)

    • 常见原因:线程(非守护线程)未停止;ThreadLocal 未清理;JDBC Driver 未反注册;静态集合持有插件类引用;MBeans 未注销。
    • 规避:要求插件在 stop() 中释放资源;PluginManager 在卸载时强制检查并清理:中断线程、remove ThreadLocal、DriverManager.deregisterDriver、MBeanServer.unregisterMBean。
  3. 资源(文件句柄)被占用

    • JAR 文件被 ClassLoader 占用导致无法替换。Java 7+ 可 URLClassLoader.close() 释放句柄,但需要确实丢弃引用并 GC。
  4. 错误的父/子加载策略

    • 随意采用 child-first 会导致核心类(例如 logging、共享 API)被插件重复加载,出现各种怪异错误。
    • 经验法则:共享 API 放父加载器、插件自身依赖尽量在插件内部;对 java.* 与框架包强制 parent-first。
  5. JPMS 下的反射失败

    • 如果模块没有 opens,反射访问会失败(例如 Jackson)。解决:在 module-info.java 中使用 opens 或采用 --add-opens 启动参数(临时方案)。

8. 调试与诊断工具(列清单)

  • jmap -histo <pid>:查看堆上类与实例分布(找 ClassLoader 泄露)。
  • jcmd <pid> GC.class_histogram:类似 jmap。
  • jstack:线程栈,定位未结束的线程。
  • jcmd <pid> VM.classloader_stats(在支持的 JDK)或 jcmd 的其它诊断命令。
  • VisualVM / JConsole / Java Mission Control (JMC):观察内存、线程、类加载器。
  • jdeps:分析类/模块依赖,有助于判断哪个 JAR 引入了冲突类。
  • jlink:用于构建精简运行时镜像(与模块系统配合)。

9. 何时用 JPMS,何时用 OSGi,差别一瞥

  • JPMS:语言层面的模块化(编译时、运行时更严格的封装),适合模块化应用与封装;但动态性(热替换/复杂插件)不如 OSGi 灵活。
  • OSGi:成熟的动态模块/服务平台,强大但复杂,常用于大型应用或需要高度动态管理的场景(如 Eclipse、部分企业中间件)。
  • 选择建议:如果你需要动态部署/卸载/版本化的复杂插件,OSGi 很强;如果只需要静态模块化和较轻的插件需求,JPMS + ClassLoader 足够。

10. 延伸阅读与学习路线(建议)

  • 官方 JPMS 指南(Java 9+ 模块系统文档)
  • 《Java Platform Module System》系列文章 / 教程
  • OSGi 官方文档与入门教程(比较动态特性)
  • Spring / WildFly / JBoss 在模块化/插件方面的实现实践(学习他们如何清理类加载器、管理模块)
    (若你想,我可以把这些资源的权威链接列出来 — 需要我帮你查最新链接吗?)

11. 小结清单(可执行的 checklist)

开发插件化系统时,部署前务必核对:

  1. 共享 API 放在主应用(父加载器)。
  2. 设计清晰的插件生命周期(init/start/stop/destroy)并强制插件释放资源。
  3. 插件尽量不要直接操作全局注册表(使用主应用提供的桥接服务)。
  4. 使用 URLClassLoader.close() 并丢弃引用后触发 GC(但不可完全依赖 GC,需在 stop 中释放资源)。
  5. 在 JPMS 环境下检查 exports/opensrequires 的声明,避免反射问题。
  6. 充分用 jmap/jstack/VisualVM 做内存与线程泄露检测。

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。