类加载器之间的关系
本次我们主要讨论子类和父类的类加载器之前的关系, 以及包含之前的类加载器关系。
我们自定义一个classloder,设置父加载器为null,这样保证都是自己来加载,指向的路径和AppClassloader一致,这样方便2个都好直接加载
继承关系
1 | class MyAnimal { |
1 | class MyDog extends MyAnimal{ |
1 | public static void main(String[] args) throws Exception { |
输出
1
2
3
4findClass,自定义类加载器加载了指定的类
findClass,自定义类加载器加载了指定的类
MyAnimal by load com.test.classload.MyCustomClassLoader@4b67cf4d
MyDog by Load com.test.classload.MyCustomClassLoader@4b67cf4d可以看到MyDog引发MyAnimal的类加载,同时也用了MyDog的加载器
从调用栈来看,在MyDog类被加载的时候,native defineClass1 阶段,native code直接回调到Dog的加载器 MyCustomClassLoader来尝试加载MyDog的父类
1
2
3
4
5
6
7
8
9
10
11findClass:34, MyCustomClassLoader (com.test.classload) [2]//MyCustomClassLoader加载父类MyAnimal
loadClass:424, ClassLoader (java.lang)
loadClass:357, ClassLoader (java.lang)
//native define 引发加载父类MyAnimal的加载,从这里放回的就是MyCustomClassLoader了
defineClass1:-1, ClassLoader (java.lang)
defineClass:763, ClassLoader (java.lang)
defineClass:642, ClassLoader (java.lang)
findClass:36, MyCustomClassLoader (com.test.classload) [1]//首先加载Dog
loadClass:424, ClassLoader (java.lang)
loadClass:357, ClassLoader (java.lang)
main:15,
疑问1,假如MyAnimal提前被AppClassloder加载了呢?
1 | public static void main(String[] args) throws Exception { |
查看输出,这里可以看到我们提前用了AppClassLoader 加载了一次 MyAnimal ,但是后面还是得被MyCustomClassLoader 加载了一次
1
2
3
4
5findClass,自定义类加载器加载了指定的类
findClass,自定义类加载器加载了指定的类
MyAnimal by load sun.misc.Launcher$AppClassLoader@135fbaa4
MyAnimal by load com.test.classload.MyCustomClassLoader@4b67cf4d
MyDog by Load com.test.classload.MyCustomClassLoader@4b67cf4d疑问2 ,假如MyAnimal提前被MyCustomClassLoader加载了呢?
我们修改都用MyCustomClassLoader加载一次
1
2Class<?> myDog = loader.loadClass("com.test.classload.MyDog");
Class<?> myAnimal = loader.loadClass("com.test.classload.MyAnimal");//只是声明,并没有引发类的加载;看输出,都是只会加载一次的
1
2
3
4findClass,自定义类加载器加载了指定的类
findClass,自定义类加载器加载了指定的类
MyAnimal by load com.test.classload.MyCustomClassLoader@4b67cf4d
MyDog by Load com.test.classload.MyCustomClassLoader@4b67cf4dJVM认为类的不同,是根据加载器和类全名来判断的,类之间的加载器不同,包名同,是属于不同的命名空间里,所以一个Anmial类可以被同时被不同的类加载器加载一次。而不同命名空间里的类在使用某个类的时候,发现并没有加载,就会使用该命名空间的类加载器去加载。
包含关系
如果我们把MyDog改成这样
1
2
3
4
5
6class MyDog {
public MyDog() {
System.out.println("MyDog by Load " + MyDog.class.getClassLoader());
new MyAnimal();
}
}同理,在我们实例化MyAnimal()时候,都是先用调用方MyDog的类加载器去加载,MyDog我们设置的父加载器不为空的,继续交给父加载器去加载。通常情况下我们写的都是由AppClassloder去加载的,都在一个命名空间下面。
父加载器会反过来用子加载器
比如我们在父加载器中去访问子加载器加载的类
让Dog 包含 Anmial
1
2
3
4
5
6public class MyDog {
public MyDog() {
System.out.println("MyDog by Load " + MyDog.class.getClassLoader());
new MyAnimal();
}
}让Anmial去访问一下Dog
1
2
3
4
5
6public class MyAnimal {
public MyAnimal() {
System.out.println("MyAnimal by load " + MyAnimal.class.getClassLoader());
System.out.println("MyAnimal:MyDog lode by "+MyDog.class.getClassLoader());
}
}这样在我们直接初始化Dog的时候,会触发MyAnmial去访问Dog类
我们直接移动一下编译好的Dog.class 让Dog使用自定义的类加载器,让Anmial使用使用AppClassLoder
1
2
3
4
5
6
7
8public static void main(String[] args) throws Exception {
MyCustomClassLoader loader = new MyCustomClassLoader();
loader.setPath("/Users/kenchan/GitHub/jvm_study/target/out/");
Class<?> myDog = loader.loadClass("com.test.classload.MyDog");
Constructor<?> constructor = myDog.getConstructor();//实列化
constructor.setAccessible(true);
Object o = constructor.newInstance();
}我们可以看到明明加载了MyDog类,却在Anmail打印
System.out.println("MyAnimal:MyDog lode by "+MyDog.class.getClassLoader());
是抛错说找不到类
实际上的错误是由于AppClassLoader是访问不到它的子加载器加载的类的,属于不同的命名空间。AppClassLoder尝试自己去加载MyDog,结果没有找到。
举个继承的例子就是如果,子类是自定义加载器才能加载,父类通过AppClassloder才能加载,那么如果父类又引用了之类的话,父类的加载器AppClassloder是加载不了子类的。
结论
类加载器,及其父加载器共同构成一个命名空间,但父类命名空间不能反过来访问子加载器加载的类,子加载器是可以访问父加载器加载的类,子加载器的父加载器是一个,但是父加载器的子加载器是可能有多个的,所以由谁触发的加载,谁可以一直想上追,追不到就自己,但是上面的却不能向下追,因为下面有多个,不知道追谁。当加载某一个类的时候,会调用当前的命名空间的子加载器,然后采用双亲委派机制上交给父加载器加载。我们也是可以做到 MyDog 是自定义加载器,MyAnimal是AppClassloder的,只要不打破双亲委派机制。
在不打破双亲委派基础下,各个加载器之前的关系如下:优先逐个交给最里面的加载器来做
里面的做不了,外面再来做
外面可以访问里面的加载的类
里面却不能访问外面的加载器加载的类