【Java虚拟机9】类加载器之命名空间详解
前言
前面介绍类加载器的时候,介绍了一下命名空间这个概念。今天就通过一个例子,来详细了解一下【类加载器的命名空间】。然后通过这个例子,我们可以总结一下双亲委托模型的好处与优点。
例1(不删除classpath下的class文件)
首先定义一个MyPerson
package com.jamie.jvmstudy;
public class MyPerson {
private MyPerson myPerson;
public void setMyPerson(Object obj){
this.myPerson = (MyPerson)obj;
}
}
然后是自定义类加载器
package com.jamie.jvmstudy;
import java.io.*;
public class CustomizedClassLoader extends ClassLoader {
private String classLoaderName;
private String path;
private String fileExtension = ".class";
public CustomizedClassLoader(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
public CustomizedClassLoader(ClassLoader parent, String classLoaderName) {
super(parent);
this.classLoaderName = classLoaderName;
}
@Override
public Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("findClass invoked : " + className);
System.out.println("class loader name : " + this.classLoaderName);
byte[] data = this.loadClassData(className);
return this.defineClass(className, data, 0, data.length);
}
private byte[] loadClassData(String className) {
byte[] data = null;
className = className.replace(".", "/");
try(InputStream is = new FileInputStream(new File(this.path + className + this.fileExtension));
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch;
while(-1 != (ch = is.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
public void setPath(String path) {
this.path = path;
}
}
测试客户端类
package com.jamie.jvmstudy;
import java.lang.reflect.Method;
public class TestClassLoaderNameSpace {
public static void main(String[] args) throws Exception {
CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
Class<?> clazz1 = loader1.loadClass("com.jamie.jvmstudy.MyPerson");
Class<?> clazz2 = loader2.loadClass("com.jamie.jvmstudy.MyPerson");
System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
System.out.println( clazz1 == clazz2);
Object object1 = clazz1.newInstance();
Object object2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson", Object.class);
method.invoke(object1, object2);
}
}
结果:
clazz1的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
clazz2的classLoader是sun.misc.Launcher$AppClassLoader@14dad5dc
true
说明:
同一个类加载器(本例是应用类加载器)加载同一个类,得到的class对象是相同的。
例2(基于例1修改,删除classpath下的class文件)
操作
为自定义类加载器设置path,然后编译成功后,删除掉classpath下面的MyPerson.class文件,把编译出的MyPerson.class文件移动到D:/temp文件夹里面。
public class TestClassLoaderNameSpace {
public static void main(String[] args) throws Exception {
CustomizedClassLoader loader1 = new CustomizedClassLoader("loader1");
CustomizedClassLoader loader2 = new CustomizedClassLoader("loader2");
loader1.setPath("D:/temp/");
loader2.setPath("D:/temp/");
Class<?> clazz1 = loader1.loadClass("com.jamie.jvmstudy.MyPerson");
Class<?> clazz2 = loader2.loadClass("com.jamie.jvmstudy.MyPerson");
System.out.println("clazz1的classLoader是" + clazz1.getClassLoader());
System.out.println("clazz2的classLoader是" + clazz2.getClassLoader());
System.out.println( clazz1 == clazz2);
Object object1 = clazz1.newInstance();
Object object2 = clazz2.newInstance();
Method method = clazz1.getMethod("setMyPerson", Object.class);
method.invoke(object1, object2);
}
}
代码执行结果
findClass invoked : com.jamie.jvmstudy.MyPerson
class loader name : loader1
findClass invoked : com.jamie.jvmstudy.MyPerson
class loader name : loader2
clazz1的classLoader是com.jamie.jvmstudy.CustomizedClassLoader@677327b6
clazz2的classLoader是com.jamie.jvmstudy.CustomizedClassLoader@7f31245a
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:497)
at com.intellij.rt.execution.CommandLineWrapper.main(CommandLineWrapper.java:67)
Caused by: 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:497)
at com.jamie.jvmstudy.TestClassLoaderNameSpace.main(TestClassLoaderNameSpace.java:23)
... 5 more
Caused by: java.lang.ClassCastException: com.jamie.jvmstudy.MyPerson cannot be cast to com.jamie.jvmstudy.MyPerson
at com.jamie.jvmstudy.MyPerson.setMyPerson(MyPerson.java:8)
... 10 more
结论
loader1和loader2分别加载了MyPerson.class,分别给MyPerson.class分配了内存空间,如下图:
这2个class对象虽然在文件系统是来自于同一个class文件,但是由于他们是被自定义类加载器加载的,并且这2个自定义类加载器是同级的,没有父子关系。所以双亲委派模型中,他们是看不到对方的命名空间的。
所以clazz1 == clazz2
的结果为false
自己画了一个简图:
下面这张是偶尔在网上看到的:
不同类加载器的命名空间关系
咱们先回顾一下命名空间的概念:
- 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。(请结合下图一起看,想明白)
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
得出命名空间的关系如下:(请结合下图一起看,想明白)
- 同一个命名空间的类是相互可见的。
- 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
- 由父类加载器加载的类不能看见子加载器加载的类。
- 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见。
类的唯一性
在运行期,一个类的唯一性是由以下2点共同决定:
- 该类的完全限定名(binary name)。
- 用于加载该类的[定义类加载器],即defining class loader。
上述2点都一样,才代表该类(可以理解为该类的Class对象)是一样的。
如果同样的名字,不同的类加载器加载,那么这2个类是不一样的。即使.class文件完全一样,.class文件路径一样,这2个类也是不一样的。
双亲委托模型的好处
- 确保Java核心类库的安全:所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object类会被加载到Java虚拟机当中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么可能会在JVM中存在多个版本的java.lang.Object类,而且这些类还是不兼容的、相互不可见的(因为命名空间的原因)。借助双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成的,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是互相兼容的。
- 确保Java核心类库提供的类不会被自定义的类所替代。
- 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加他们即可,不同类加载器所加载的类是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间。