欢迎关注本人公众号
概述
之前我写了篇博客: java自定义类加载器实现类隔离 ,里面介绍了如何自定义类加载器实现了类隔离。
通常情况下,在JSP,OSGI及其他一些支持热替换的库,都是需要进行类的卸载回收的,否则类在替换后,老的类就没用了但是还在内存中,就会造成内存泄漏。
我们知道类的卸载需要满足以下三个条件:
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。
所以在自定义类加载器时,就要注意这一点,如果你是希望其使用完成后就被卸载,那么就需要特别留意类加载器及类的作用域了。
代码实现
我这里还是在 java自定义类加载器实现类隔离 的基础上,对代码进行一些调整,使其可以在使用完成后可以被卸载。注意这里的代码只是学习使用的实例,如果用于生产环境还需要慎重。
在D盘的a,b两个目录下准备Hello.class 和 Dog.class 两个文件还是使用 java自定义类加载器实现类隔离 提到的方法来实现。
接下来对通过URLClassLoader实现的test0方法进行改造:
@Test
public void test0() throws Exception {
test4();
System.gc();
TimeUnit.SECONDS.sleep(5);
}
public void test4() throws Exception {
System.out.println(this.getClass().getClassLoader());
URLClassLoader diskLoader = new URLClassLoader(new URL[]{new URL("file:/D:/liubenlong/a/")});//最后面的斜杠需要添加
URLClassLoader diskLoader1 = new URLClassLoader(new URL[]{new URL("file:/D:/liubenlong/b/")});
//加载class文件
Class clz = diskLoader.loadClass("Hello");
Constructor constructor = clz.getConstructor(String.class);
Object obj = constructor.newInstance("tom");
/**
* 类Hello引用了类Dog,类加载器会主动加载被引用的类。
* 注意一般是我们使用 URLClassLoader 实现自定义的类加载器。如果使用classLoader,则需要重写findClass方法来实现类字节码的加载
*/
Method method = clz.getMethod("sayHello", null);
//通过反射调用Test类的say方法
method.invoke(obj, null);
Class clz1 = diskLoader1.loadClass("Hello");
Constructor constructor1 = clz1.getConstructor(String.class);
Object obj1 = constructor1.newInstance("cat");
Method method1 = clz1.getMethod("sayHello", null);
//通过反射调用Test类的say方法
method1.invoke(obj1, null);
}
这里System.gc();
是为了主动触发GC进行类卸载。后面的sleep只是为了等待程序执行完成,输出结果。
然后需要添加启动参数-verbose:class
来打印出类加载及类卸载的日志信息。
运行test0,输出
//省略部分日志
[0.482s][info][class,load] Hello source: file:/D:/liubenlong/a/
[0.483s][info][class,load] Dog source: file:/D:/liubenlong/a/
[0.484s][info][class,load] java.lang.invoke.StringConcatFactory source: jrt:/java.base
//省略部分日志
[0.508s][info][class,load] java.io.DataInputStream source: jrt:/java.base
a hi ... java.net.URLClassLoader@3891771e
//省略部分日志
[0.549s][info][class,load] java.lang.invoke.LambdaForm$MH/0x0000000100101c40 source: java.lang.invoke.LambdaForm
a hello tom java.net.URLClassLoader@3891771e
[0.552s][info][class,load] Hello source: file:/D:/liubenlong/b/
[0.556s][info][class,load] Dog source: file:/D:/liubenlong/b/
b hi ... java.net.URLClassLoader@78ac1102
b hello cat java.net.URLClassLoader@78ac1102
[0.560s][info][class,unload] unloading class Dog 0x0000000100102258
[0.560s][info][class,unload] unloading class Hello 0x0000000100102040
[0.560s][info][class,unload] unloading class Dog 0x00000001000a1a58
[0.560s][info][class,unload] unloading class Hello 0x00000001000a1840
[5.581s][info][class,load ] java.lang.Shutdown source: jrt:/java.base
[5.581s][info][class,load ] java.lang.Shutdown$Lock source: jrt:/java.base
Process finished with exit code 0
日志中可以看出class load 和 unload 的信息,以及Hello和Dog类的类加载器。
注意,我这里时是加了一个test4
方法来进行操作类及处理业务逻辑,test0
调用test4
方法并且调用System.gc();
。如果System.gc();
直接写道test4
方法的末尾,是无法实现类的卸载的。读者可以自己实验。
原因是如果放在一起的话,都是同一个方法内,方法是虚拟机执行的最小单元,调用test4
方法时会生成一个栈帧放到栈顶。test4
方法的局部变量就会存在与栈帧中的局部变量表中,这里就有URLClassLoader类加载器及Hello/Dog类实例的引用,还包括一些动态链接,所以在GC时,由于栈帧中的内容是作为GC ROOT的,所以肯定不会被回收,故而不会进行类的卸载。
注意在实际开发中一定要保证类的实例, 该类的ClassLoader都被回收,并且没该类不可以被反射调用,才可以类卸载。