【Java虚拟机9】类加载器之命名空间详解

时间:2022-05-01 16:33:02

前言

前面介绍类加载器的时候,介绍了一下命名空间这个概念。今天就通过一个例子,来详细了解一下【类加载器的命名空间】。然后通过这个例子,我们可以总结一下双亲委托模型的好处与优点。

例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分配了内存空间,如下图:

【Java虚拟机9】类加载器之命名空间详解

这2个class对象虽然在文件系统是来自于同一个class文件,但是由于他们是被自定义类加载器加载的,并且这2个自定义类加载器是同级的,没有父子关系。所以双亲委派模型中,他们是看不到对方的命名空间的。

所以clazz1 == clazz2的结果为false

自己画了一个简图:

【Java虚拟机9】类加载器之命名空间详解

下面这张是偶尔在网上看到的:

【Java虚拟机9】类加载器之命名空间详解

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

咱们先回顾一下命名空间的概念:

  • 每个类加载器都有自己的命名空间。命名空间由该加载器和所有父加载器所加载的类组成。(请结合下图一起看,想明白)
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

得出命名空间的关系如下:(请结合下图一起看,想明白)

  • 同一个命名空间的类是相互可见的。
  • 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器能看见根类加载器加载的类。
  • 由父类加载器加载的类不能看见子加载器加载的类。
  • 如果两个加载器没有父子关系,那么他们自己加载的类互相不可见。

    【Java虚拟机9】类加载器之命名空间详解

类的唯一性

在运行期,一个类的唯一性是由以下2点共同决定:

  1. 该类的完全限定名(binary name)。
  2. 用于加载该类的[定义类加载器],即defining class loader。

    上述2点都一样,才代表该类(可以理解为该类的Class对象)是一样的。

    如果同样的名字,不同的类加载器加载,那么这2个类是不一样的。即使.class文件完全一样,.class文件路径一样,这2个类也是不一样的。

双亲委托模型的好处

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