Java中ClassLoader类加载学习总结
双亲委派模型
类加载这个概念应该算是java语言的一种创新,目的是为了将类的加载过程与虚拟机解耦,达到”通过类的全限定名来获取描述此类的二进制字节流“的目的。实现这个功能的代码模块就是类加载器。类加载器的基本模型就是大名鼎鼎的双亲委派模型(parents delegation model)。听上去很牛掰,其实逻辑很简单,在需要加载一个类的时候,我们首先判断该类是否已被加载,如果没有就判断是否已被父加载器加载,如果还没有再调用自己的findclass方法尝试加载。基本的模型就是这样(盗图侵删):
实现起来也很简单,重点就是classloader类的loadclass方法,源码如下:
protected class<?> loadclass(string name, boolean resolve) throws classnotfoundexception { synchronized (getclassloadinglock(name)) { // first, check if the class has already been loaded class<?> c = findloadedclass(name); if (c == null) { long t0 = system.nanotime(); try { if (parent != null) { c = parent.loadclass(name, false); } else { c = findbootstrapclassornull(name); } } catch (classnotfoundexception e) { // classnotfoundexception thrown if class not found // from the non-null parent class loader } if (c == null) { // if still not found, then invoke findclass in order // to find the class. long t1 = system.nanotime(); c = findclass(name); // this is the defining class loader; record the stats sun.misc.perfcounter.getparentdelegationtime().addtime(t1 - t0); sun.misc.perfcounter.getfindclasstime().addelapsedtimefrom(t1); sun.misc.perfcounter.getfindclasses().increment(); } } if (resolve) { class(c); } return c; } }
突然感觉被逗了,怎么默认直接就抛了异常呢?其实是因为classloader这个类是一个抽象类,实际在使用时候会写个子类,这个方法会按照需要被重写,来完成业务需要的加载过程。
自定义classloader
在自定义classloader的子类时候,我们常见的会有两种做法,一种是重写loadclass方法,另一种是重写findclass方法。其实这两种方法本质上差不多,毕竟loadclass也会调用findclass,但是从逻辑上讲我们最好不要直接修改loadclass的内部逻辑。
个人认为比较好的做法其实是只在findclass里重写自定义类的加载方法。
为啥说这种比较好呢,因为前面我也说道,loadclass这个方法是实现双亲委托模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委托模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadclass方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
当然,如果是刻意要破坏双亲委托模型就另说。
破坏双亲委托模型
为什么要破坏双亲委托模型呢?
其实在某些情况下,我们可能需要加载两个不同的类,但是不巧的是这两个类的名字完全一样,这时候双亲委托模型就无法满足我们的要求了,我们就要重写loadclass方法破坏双亲委托模型,让同一个类名加载多次。当然,这里说的破坏只是局部意义上的破坏。
但是类名相同了,jvm怎么区别这两个类呢?显然,这并不会造成什么世界观的崩塌,其实类在jvm里并不仅是通过类名来限定的,他还属于加载他的classloader。由不同classloader加载的类其实是互不影响的。
做一个实验。
我们先写两个类:
package com.mythsman.test; public class hello { public void say() { system.out.println("this is from hello v1"); } }
package com.mythsman.test; public class hello { public void say() { system.out.println("this is from hello v2"); } }
两个类名字一样,唯一的区别是方法的实现不一样。我们先分别编译,然后把生成的class文件重命名为hello.class.1和hello.class.2。
我们的目的是希望能在测试类里分别创建这两个类的实例。
接着我们新建一个测试类com.mythsman.test.main,在主函数里创建两个自定义的classloader:
classloader classloader1=new classloader() { @override public class<?> loadclass(string s) throws classnotfoundexception { try { if (s.equals("com.mythsman.test.hello")) { byte[] classbytes = files.readallbytes(paths.get("/home/myths/desktop/test/hello.class.1")); return defineclass(s, classbytes, 0, classbytes.length); }else{ return super.loadclass(s); } }catch (ioexception e) { throw new classnotfoundexception(s); } } }; classloader classloader2=new classloader() { @override public class<?> loadclass(string s) throws classnotfoundexception { try { if (s.equals("com.mythsman.test.hello")) { byte[] classbytes = files.readallbytes(paths.get("/home/myths/desktop/test/hello.class.2")); return defineclass(s, classbytes, 0, classbytes.length); }else{ return super.loadclass(s); } }catch (ioexception e) { throw new classnotfoundexception(s); } } };
这两个classloader的用途就是分别关联hello类的两种不同字节码,我们需要读取字节码文件并通过defineclass方法加载成class。注意我们重载的是loadclass方法,如果是重载findclass方法那么由于loadclass方法的双亲委托处理机制,第二个classloader的findclass方法其实并不会被调用。
那我们怎么生成实例呢?显然我们不能直接用类名来引用(名称冲突),那就只能用反射了:
object hellov1=classloader1.loadclass("com.mythsman.test.hello").newinstance(); object hellov2=classloader2.loadclass("com.mythsman.test.hello").newinstance(); hellov1.getclass().getmethod("say").invoke(hellov1); hellov2.getclass().getmethod("say").invoke(hellov2);
输出:
this is from hello v1 this is from hello v2
ok,这样就算是完成了两次加载,但是还有几个注意点需要关注下。
两个类的关系是什么
显然这两个类并不是同一个类,但是他们的名字一样,那么类似isinstance of之类的操作符结果是什么样的呢:
system.out.println("class:"+hellov1.getclass()); system.out.println("class:"+hellov2.getclass()); system.out.println("hashcode:"+hellov1.getclass().hashcode()); system.out.println("hashcode:"+hellov2.getclass().hashcode()); system.out.println("classloader:"+hellov1.getclass().getclassloader()); system.out.println("classloader:"+hellov2.getclass().getclassloader());
输出:
class:class com.mythsman.test.hello class:class com.mythsman.test.hello hashcode:1581781576 hashcode:1725154839 classloader:com.mythsman.test.main$1@5e2de80c classloader:com.mythsman.test.main$2@266474c2
他们的类名的确是一样的,但是类的hashcode不一样,也就意味着这两个本质不是一个类,而且他们的类加载器也不同(其实就是main的两个内部类)。
这两个类加载器跟系统的三层类加载器是什么关系
以第一个自定义的类加载器为例:
system.out.println(classloader1.getparent().getparent().getparent()); system.out.println(classloader1.getparent().getparent()); system.out.println(classloader1.getparent()); system.out.println(classloader1 ); system.out.println(classloader.getsystemclassloader());
输出:
null sun.misc.launcher$extclassloader@60e53b93 sun.misc.launcher$appclassloader@18b4aac2 com.mythsman.test.main$1@5e2de80c sun.misc.launcher$appclassloader@18b4aac2
我们可以看到,第四行就是这个自定义的classloader,他的父亲是appclassloader,爷爷是extclassloader,太爷爷是null,其实就是用c写的bootstrapclassloader。而当前系统的classloader就是这个appclassloader。
当然,这里说的父子关系并不是继承关系,而是组合关系,子classloader保存了父classloader的一个引用(parent)。
推荐阅读