Java动态加载与卸载jar包深度思考与全面实战技巧
实战:JAR动态加载与卸载
首先需要准备一个用于验证的 JAR 包。编写一个包含静态代码块、静态方法以及实例方法的简单类,并将其打包为 jartest.jar(假设存放路径为 /Users/tmp/jartest.jar)。
package cn.com.test;
public class JarTest {
static {
System.out.println("I am JarTest's static code");
}
public static void run(){
System.out.println("I am JarTest's static method");
}
public void run1(){
System.out.println("I am JarTest's method>>> " + this);
}
}
接下来编写用于加载与卸载的测试工具。核心思路为:通过 URLClassLoader 加载外部 JAR,调用其内部方法,然后尝试卸载该 JAR。
package jar;
import sun.misc.ClassLoaderUtil;
import ja va.io.File;
import ja va.io.IOException;
import ja va.lang.reflect.InvocationTargetException;
import ja va.lang.reflect.Method;
import ja va.net.MalformedURLException;
import ja va.net.URL;
import ja va.net.URLClassLoader;
public class LoadJar {
static Object jarTestInstance = null;
static ClassLoader myClassLoader1;
static Class> jarTest;
public static void main(String[] args) throws MalformedURLException, InterruptedException {
System.out.println("before load jar");
loadClassAndRun();
Thread.sleep(1000);
System.out.println("load jar");
loadJar();
Thread.sleep(1000);
System.out.println("after load jar");
loadClassAndRun();
Thread.sleep(1000);
System.out.println("start unload jar");
unLoad();
Thread.sleep(1000);
System.out.println("after unload jar");
loadClassAndRun();
System.out.println("load jar");
loadJar();
Thread.sleep(1000);
System.out.println("after load jar");
loadClassAndRun();
}
private static void loadClassAndRun() {
if(myClassLoader1 == null) {
myClassLoader1 = LoadJar.class.getClassLoader();
}
try {
jarTest = myClassLoader1.loadClass("cn.com.test.JarTest");
jarTestInstance = jarTest.newInstance();
for (Method method : jarTest.getMethods()) {
try {
method.invoke(jarTestInstance);
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e){
e.printStackTrace();
}
}
private static void loadJar() throws MalformedURLException {
URL url = new File("/Users/tmp/jartest.jar").toURI().toURL();
myClassLoader1 = new URLClassLoader(new URL[] { url});
}
// 卸载jar包的代码如下:
public static void unLoad() {
if (null != myClassLoader1 && myClassLoader1 instanceof URLClassLoader) {
System.out.println("unload URLClassLoader ");
URLClassLoader loader = (URLClassLoader) myClassLoader1;
try {
// 关注下这里,这里一会儿要改
ClassLoaderUtil.releaseLoader(loader);
loader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:上述代码调用了 ClassLoaderUtil.releaseLoader,这是一个内部 API,后续会采用更标准的方式处理。
运行结果
问题剖析
一个令人困惑的现象:调用 URLClassLoader.close() 释放资源后,之前加载的 class 竟然仍然可以继续使用?官方文档给出的解释如下:
*** 官方文档明确指出,关闭 URLClassLoader 并不会自动回收已加载的类 *** 那么,类究竟在何时才会被真正卸载?回顾类加载机制:只有当类不再有任何引用时,GC 才能回收它。所以问题就清晰了——仅关闭 URLClassLoader 并不等于切断了所有引用链。
因此我们对 unLoad() 方法进行改造,显式将类实例、Class 对象以及 ClassLoader 本身全部置为 null,并主动触发 GC:
// 卸载jar包的代码如下:
public static void unLoad() {
if (null != myClassLoader1 && myClassLoader1 instanceof URLClassLoader) {
System.out.println("unload URLClassLoader ");
URLClassLoader loader = (URLClassLoader) myClassLoader1;
try {
jarTest = null;
jarTestInstance = null;
myClassLoader1 = null;
System.gc();
Thread.sleep(2000);
ClassLoaderUtil.releaseLoader(loader);
loader.close();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
再次执行,得到如下结果:
需要留意的是,System.gc() 并不会立即执行,因此实际测试结果可能与上述截图略有差异。关键结论是:关闭 URLClassLoader 仅仅完成了第一步,确保所有关联对象失去引用才是真正卸载类的前提。


