bigywater.tech
Java · 199

URLClassLoader:插件系统如何动态加载 JAR

插件系统怎么用 URLClassLoader 动态加载 JAR、替换版本、卸载?这篇用两个版本的插件 jar 逐项验证加载、类身份、卸载与泄漏(类加载器的双亲委派基础见前面的篇目,Module Layer 在后面)。

ClassCastException
jar 自带接口且父加载器为 null 时转型的结果
c1 == c2 为 false
两个 URLClassLoader 加载同一个 jar 的同名类
close() 后 0 个
plugin_v1.jar 的文件描述符个数(加载后是 1 个)

加载插件

实测 · 本机执行输出
  从 jar 里加载 Impl 并转成 Plugin 接口成功:impl-v1 → v1:HELLO;Impl 的类加载器 = URLClassLoader,Plugin 接口的类加载器 = AppClassLoader
  Impl.class.getProtectionDomain().getCodeSource() = …/plugin_v1.jar
  用 Class.forName("Impl")(用调用者的类加载器)→ ClassNotFoundException(插件类只在 URLClassLoader 里)

类的身份

实测 · 本机执行输出
  jar 里自带了一份 Plugin 接口,父加载器设为 null(启动类加载器):Impl 的接口 Plugin 来自 URLClassLoader;与应用里的 Plugin.class 是同一个类吗:false
  (Plugin) 转型 → ClassCastException: class Impl cannot be cast to class Plugin (Impl is in unnamed module of loader java.net.URLClassLoader @…; Plugin is in unnamed module of loader 'app')
  同一个 jar(也带着 Plugin 接口),但父加载器是应用类加载器:先问父加载器,接口用的是应用的那一份,转型成功:v1x:x;接口的加载器 = AppClassLoader

多版本

实测 · 本机执行输出
  plugin_v1.jar → impl-v1:v1:HELLO
  plugin_v2.jar → impl-v2:v2:olleh
  两个 URLClassLoader 各自加载同一个 jar 的 Impl:c1 == c2 吗?false,c1.getName().equals(c2.getName()) = true

卸载

实测 · 本机执行输出
  加载后进程打开了 plugin_v1.jar 的文件描述符个数(lsof):1
  close() 之后:0 个(关闭了 jar 文件)
  close() 之后还有强引用(pl 与 cl 变量)时,GC 能回收这个类加载器吗:false
  把 pl 与 cl 都置为 null 之后,GC 后类加载器被回收:true,插件的 Class 被回收:true

卸载不掉

实测 · 本机执行输出
  插件里启动了一个后台线程(类里有 static 块起线程),并持有 8 MB 的静态数组:卸载插件(close、丢弃引用、GC)之后,类加载器被回收了吗:false
  plugin-thread 线程还在运行吗:true(线程还活着时类加载器没有被回收;具体是哪条引用链留住它,本篇没有查)

分步演示

URLClassLoader 插件
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12,macOS(lsof 统计文件描述符)
    • 没有验证:Windows 上 jar 被占用无法替换的问题、ServiceLoader 与 URLClassLoader 的配合、parent-last(子优先)加载器的写法、Metaspace 的实际释放量、线程上下文类加载器的设置;卸载不掉的原因没有查引用链
    京ICP备2026067502号-1京公网安备11010502063487号