本博客将沿用上篇博客中展示的自定义类加载器代码
复杂类加载情况分析
测试代码一
首先,新建一个类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.<init>(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.<init>(Test14.java:11)
at classloader.Test15.<init>(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);
的运行结果为false
,classLoader1和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类空间。