类加载器 - 命名空间

本博客将沿用上篇博客中展示的自定义类加载器代码

复杂类加载情况分析

测试代码一

首先,新建一个类Test14,重写默认的构造方法,打印加载该类的类加载器

public class Test14 {
    public Test14() {
        System.out.println("Test14 is loaded by:" + this.getClass().getClassLoader());
    }
}

然后,在新建一个类Test15,同样重写默认的构造方法,打印加载该类的类加载器,在构造方法中new出Test14的实例

public class Test15 {
    public Test15() {
        System.out.println("Test15 is loaded by:" + this.getClass().getClassLoader());

        new Test14();
    }
}

测试代码

public class Test16 {
    public static void main(String[] args) throws Exception {
        test01();
    }

    private static void test01 () throws Exception {
        ClassLoaderTest classLoader = new ClassLoaderTest("classLoader");
        Class clazz = classLoader.loadClass("classloader.Test15");
        System.out.println("class:" + clazz);
        Object object = clazz.newInstance();
    }
}

猜测一下,首先自定义类加载器classLoader通过反射获取Test15的Class对象,属于主动使用,会加载Test15,classLoader委托它的父加载器AppClassLoader加载Test15;然后我们通过clazz.newInstance();代码获取Test15的实例,调用Test15的构造方法,在Test15的构造方法中创建了Test14的实例,所以同样加载了Test14,并调用了Test14的构造方法。加上-XX:+TraceClassLoading指令执行代码,发现运行结果和我们想的是一样的。

......
[Loaded classloader.Test15 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
class:class classloader.Test15
Test15 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
[Loaded classloader.Test14 from file:/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/]
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
......

测试代码二

在上篇博客中,自定义类加载器ClassLoaderTest是有一个path属性可以自定义类的加载路径的,我们同样测试一下,我们将Test14和Test15的class文件放到桌面的classloader文件夹下,然后删除工程路径下的class文件,执行一下的测试代码

public class Test16 {
    public static void main(String[] args) throws Exception {
        test02();
    }
    private static void test02 () throws Exception {
        ClassLoaderTest classLoader = new ClassLoaderTest("classLoader");
        classLoader.setPath("/home/fanxuan/桌面/");
        Class clazz = classLoader.loadClass("classloader.Test15");
        System.out.println("class:" + clazz);
        Object object = clazz.newInstance();
    }
}

按照上节的结果,应该都是ClassLoaderTest加载器加载了Test14和Test15类

class:class classloader.Test15
Test15 is loaded by:classloader.ClassLoaderTest@6d6f6e28
Test14 is loaded by:classloader.ClassLoaderTest@6d6f6e28

接下来,我们重新编译项目,删除掉工程目录下的Test14的calss文件,再次执行代码

class:class classloader.Test15
Test15 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Test14
    at classloader.Test15.(Test15.java:11)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at classloader.Test16.test02(Test16.java:25)
    at classloader.Test16.main(Test16.java:9)
Caused by: java.lang.ClassNotFoundException: classloader.Test14
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 8 more

我们发现结果报错了,按照我们正常的思维,自定义记载器classLoader委托父加载器AppClassLoader加载Test15,从打印结果可以看出Test15加载成功了,然后创建Test15的实例,加载Test14,因为工程目录下缺少Test14的class文件,所以AppClassLoader无法加载到Test14,由自定义加载器classLoader自身从桌面加载Test14。但是我们发现加载Test14的报了ClassNotFoundException的错误,这是因为在Test15中记载Test14的时候,是以Test15的类加载器AppClassLoader来加载的,AppClassLoader加载不到Test14,它的父加载器扩展类加载器同样加载不到,扩展类加载器的父加载器启动类加载器也加载不到,所以报错ClassNotFoundException

然后,再重新编译项目,删除掉工程目录下的Test15的calss文件,再次执行代码。根据前文分析的代码,我们可以很清晰的得出结论:由自定义记载器classLoader加载了Test15,由系统类记载器AppClassLoader加载了Test14。

class:class classloader.Test15
Test15 is loaded by:classloader.ClassLoaderTest@6d6f6e28
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2

测试代码三

简单修改下Test14类,在Test14的构造方法中引用Test15的Class对象。

public class Test14 {
    public Test14() {
        System.out.println("Test14 is loaded by:" + this.getClass().getClassLoader());

        System.out.println("Test14:" + Test15.class);
    }
}

执行测试代码二中的测试代码Test16,结果如下,没有任何问题。

class:class classloader.Test15
Test15 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Test14:class classloader.Test15

我们同样重新编译项目,删除掉工程目录下的Test15的calss文件,再次执行代码。

class:class classloader.Test15
Test15 is loaded by:classloader.ClassLoaderTest@6d6f6e28
Test14 is loaded by:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: classloader/Test15
    at classloader.Test14.(Test14.java:11)
    at classloader.Test15.(Test15.java:11)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at classloader.Test16.test02(Test16.java:25)
    at classloader.Test16.main(Test16.java:9)
Caused by: java.lang.ClassNotFoundException: classloader.Test15
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 9 more

我们发现加载已经完成了,但是程序还是报错了,是我们刚刚加的System.out.println("Test14:" + Test15.class);代码报的错,依然是ClassNotFoundException错误。

分析:
Test15由自定义记载器classLoader加载,Test14由系统类记载器AppClassLoader加载。导致程序报错的是因为命名空间的问题,我们在上一篇博客的结尾简单介绍了命名空间:每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成。子加载器所加载的类可以看见父加载器加载的类,但是父加载器所加载的类无法看见子加载器加载的类。Test14是由AppClassLoader加载的,在AppClassLoader的命名空间中没有Test15的,所以程序报错了。

命名空间实例分析

测试代码

新建Entity类用于测试

public class Entity {
    private Entity entity;

    public void setEntity(Object entity) {
        this.entity = (Entity)entity;
    }
}

编写测试代码

public class Test17 {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest classLoader1 = new ClassLoaderTest("classLoader1");
        ClassLoaderTest classLoader2 = new ClassLoaderTest("classLoader2");

        Class clazz1 = classLoader1.loadClass("classloader.Entity");
        Class clazz2 = classLoader2.loadClass("classloader.Entity");

        System.out.println(clazz1 == clazz2);

        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setEntity", Object.class);
        method.invoke(object1, object2);
    }
}

运行程序,System.out.println(clazz1 == clazz2);返回结果为true,都是AppClassLoader加载的,classLoader1加载之后会在AppClassLoader的命名空间中形成缓存,classLoader2加载的时候直接返回命名空间已经存在的Class对象,所以clazz1与clazz2相同。

改造下代码,将Entity类的class文件copy到桌面文件夹下,删除工程下的class文件,执行如下代码

public class Test18 {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest classLoader1 = new ClassLoaderTest("classLoader1");
        ClassLoaderTest classLoader2 = new ClassLoaderTest("classLoader2");

        classLoader1.setPath("/home/fanxuan/桌面/");
        classLoader2.setPath("/home/fanxuan/桌面/");

        Class clazz1 = classLoader1.loadClass("classloader.Entity");
        Class clazz2 = classLoader2.loadClass("classloader.Entity");

        System.out.println(clazz1 == clazz2);

        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setEntity", Object.class);
        method.invoke(object1, object2);
    }
}

根据前文的介绍,不难推断System.out.println(clazz1 == clazz2);的运行结果为falseclassLoader1和classLoader2分别加载了Entity类,就是其自身加载的(定义类加载器),在jvm的内存中形成了完全独立的两个命名空间,所以clazz1与clazz2不同。而且因为clazz1和clazz2相互不可见,调用了classLoader1命名空间中的方法,传入了classLoader2命名空间的对象,导致程序抛出了异常。

false
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at classloader.Test18.main(Test18.java:26)
Caused by: java.lang.ClassCastException: classloader.Entity cannot be cast to classloader.Entity
    at classloader.Entity.setEntity(Entity.java:11)
    ... 5 more

不同类加载器的命名空间关系

  • 同一命名空间内的类是相互可见的
  • 子加载器的命名空间包含所有父加载器的命名空间,由子加载器所加载的类可以看见父加载器加载的类
  • 由父加载器所加载的类无法看见子加载器加载的类
  • 如果两个加载器之间没有任何直接或间接的父子关系,那么它们各自加载的类相互不可见

父亲委托机制的好处

在上篇博客的2.1章节简单介绍了一下类加载器的父亲委托机制,这里面来总结一下好处

  • 确保Java核心类库的安全:所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object类会被记载到Java虚拟机当中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么可能会在JVM中存在多个版本的java.lang.Object类,而且这些类还是不兼容的、相互不可见的(因为命名空间的原因)。借助父亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是互相兼容的。
  • 确保Java核心类库提供的类不会被自定义的类所替代。
  • 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加他们即可,不同类加载器所加载的类是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间。

你可能感兴趣的:(类加载器 - 命名空间)