实现
首先需要准备一个用于测试的 JAR 包。编写一个简单的 Java 类,包含静态代码块、静态方法和实例方法,并将其打包成 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() 释放资源后,之前加载的类竟然仍然可以被使用?官方文档对此解释如下:
官方文档说明截图如下:
*** 从官方解释来看,确实如此 *** 那么,类究竟在何时才会被真正回收?回顾类加载机制可知:只有当类没有任何引用时,才会被垃圾回收器(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 只是第一步,让所有相关对象失去引用才是实现卸载的前提。
