类加载器之间的关系

本次我们主要讨论子类和父类的类加载器之前的关系, 以及包含之前的类加载器关系。
我们自定义一个classloder,设置父加载器为null,这样保证都是自己来加载,指向的路径和AppClassloader一致,这样方便2个都好直接加载

继承关系

1
2
3
4
5
class MyAnimal {
public MyAnimal() {
System.out.println("MyAnimal by load " + MyAnimal.class.getClassLoader());
}
}
1
2
3
4
5
class MyDog extends MyAnimal{
public MyDog() {
System.out.println("MyDog by Load " + MyDog.class.getClassLoader());
}
}
1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
MyCustomClassLoader loader = new MyCustomClassLoader();//自定加载类
loader.setPath("/Users/kenchan/GitHub/jvm_study/target/classes/");

Class<?> myDog = loader.loadClass("com.test.classload.MyDog");//加载
Constructor<?> constructor = myDog.getConstructor();//实列化
constructor.setAccessible(true);
Object o = constructor.newInstance();
}
  • 输出

    1
    2
    3
    4
    findClass,自定义类加载器加载了指定的类
    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
    11
    findClass: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
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
MyCustomClassLoader loader = new MyCustomClassLoader();
loader.setPath("/Users/kenchan/GitHub/jvm_study/target/classes/");

Class<?> myDog = loader.loadClass("com.test.classload.MyDog");
//提前触发一下 MyAnimal类的加载,这里默认是用的AppClassloder
Class<?> myAnimal = Class.forName("com.test.classload.MyAnimal");//只是声明,并没有引发类的加载;
myAnimal.newInstance();//引发类的加载
Constructor<?> constructor = myDog.getConstructor();//实列化
constructor.setAccessible(true);
Object o = constructor.newInstance();
}
  • 查看输出,这里可以看到我们提前用了AppClassLoader 加载了一次 MyAnimal ,但是后面还是得被MyCustomClassLoader 加载了一次

    1
    2
    3
    4
    5
    findClass,自定义类加载器加载了指定的类
    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
    2
    Class<?> myDog = loader.loadClass("com.test.classload.MyDog");
    Class<?> myAnimal = loader.loadClass("com.test.classload.MyAnimal");//只是声明,并没有引发类的加载;
  • 看输出,都是只会加载一次的

    1
    2
    3
    4
    findClass,自定义类加载器加载了指定的类
    findClass,自定义类加载器加载了指定的类
    MyAnimal by load com.test.classload.MyCustomClassLoader@4b67cf4d
    MyDog by Load com.test.classload.MyCustomClassLoader@4b67cf4d
  • JVM认为类的不同,是根据加载器和类全名来判断的,类之间的加载器不同,包名同,是属于不同的命名空间里,所以一个Anmial类可以被同时被不同的类加载器加载一次。而不同命名空间里的类在使用某个类的时候,发现并没有加载,就会使用该命名空间的类加载器去加载。

    包含关系

  • 如果我们把MyDog改成这样

    1
    2
    3
    4
    5
    6
    class 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
    6
    public class MyDog {
    public MyDog() {
    System.out.println("MyDog by Load " + MyDog.class.getClassLoader());
    new MyAnimal();
    }
    }
  • 让Anmial去访问一下Dog

    1
    2
    3
    4
    5
    6
    public 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
    8
    public 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());是抛错说找不到类

image.png

  • 实际上的错误是由于AppClassLoader是访问不到它的子加载器加载的类的,属于不同的命名空间。AppClassLoder尝试自己去加载MyDog,结果没有找到。

  • 举个继承的例子就是如果,子类是自定义加载器才能加载,父类通过AppClassloder才能加载,那么如果父类又引用了之类的话,父类的加载器AppClassloder是加载不了子类的。

    结论

    类加载器,及其父加载器共同构成一个命名空间,但父类命名空间不能反过来访问子加载器加载的类,子加载器是可以访问父加载器加载的类,子加载器的父加载器是一个,但是父加载器的子加载器是可能有多个的,所以由谁触发的加载,谁可以一直想上追,追不到就自己,但是上面的却不能向下追,因为下面有多个,不知道追谁。当加载某一个类的时候,会调用当前的命名空间的子加载器,然后采用双亲委派机制上交给父加载器加载。我们也是可以做到 MyDog 是自定义加载器,MyAnimal是AppClassloder的,只要不打破双亲委派机制。
    在不打破双亲委派基础下,各个加载器之前的关系如下:

  • 优先逐个交给最里面的加载器来做

  • 里面的做不了,外面再来做

  • 外面可以访问里面的加载的类

  • 里面却不能访问外面的加载器加载的类

image.png