08. 双亲委派模型
双亲委派模型
在父亲委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器
某一个类加载器想要加载一个特定的类,并不是立刻由自己加载,而是把这个加载的动作委托给自己的父亲完成,父亲上面还有父亲,再委托给父亲的父亲完成,一直往上追溯到最顶层的根类加载器,由根类加载器尝试加载需要的.class文件,如果根类加载器加载不成功,则把流程往下返回给扩展类加载器,从上往下一直返回到开始的类加载器,然而在整个过程中,在任何一个类加载器的层次上成功的加载这个类,就宣告这个加载动作是成功的,流程直接返回开始的类加载器
若有一个类加载器能够成功加载Test类,那么这个类加载器被称为定义类加载器,所有能够成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器
双亲委派模型的好处
-
可以确保Java核心库的类型安全:
所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java.lang.Object这个类会被加载
到Java虚拟中,如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥作用),借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,它们之间是相互兼容的 -
可以确保Java核心类库所提供的类不会被自定义的类所替代
-
不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中得到了实际应用。
通过例子来验证双亲委派模型
看上篇文章我们定义的第二个类加载器,代码如下
public class MyTest16_2 extends ClassLoader{
// 类加载器名字
private String classLoaderName;
// 新增字段path 从什么地方加载 绝对路径
private String path;
// 新增set方法
public void setPath(String path) {
this.path = path;
}
// 字节码文件扩展名
private final String fileExtension = ".class";
public MyTest16_2(String classLoaderName) {
// 使用getSystemClassLoader()返回的类加载器,即AppClassLoader作为该类加载器的父类加载器
super();
this.classLoaderName = classLoaderName;
}
public MyTest16_2(ClassLoader parent, String classLoaderName) {
// 显式指定该类加载器的父类加载器
super(parent);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
// 这两个信息输出 说明findClass()方法被调用
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) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
// windows 系统 替换成\\
className = className.replace(".", "/");
try {
is = new FileInputStream(new File( this.path + className + this.fileExtension));
baos = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())){
baos.write(ch);
}
data = baos.toByteArray();
}catch (Exception e) {
e.printStackTrace();
}finally {
try {
is.close();
baos.close();
}catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
@Override
public String toString() {
return "[" + this.classLoaderName + "]";
}
}
试验1
使用上面的MyTest16_2这个自定义类加载器,我们进行如下代码测试,猜测下输出是什么?
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是classpath
loader1.setPath("/Users/zj/workspace/java/study/jdk8/target/classes");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object = clazz1.newInstance();
System.out.println(object);
}
}
此时,使用的是系统类加载器来加载,使用loader1来加载类,loader1的父加载器是系统类加载器,其委托给系统类加载器去加载,系统类加载器委托扩展类加载器去加载,扩展类委托启动类加载器去加载,启动类加载器发现自己不能加载该类,就让扩展类加载器去加载,扩展类加载器发现自己也不能加载,就让系统类加载器去加载,系统类加载器在classpath找到该类可以加载,就自己加载返回了,所以此时是系统类加载器加载的
试验2
我们修改下代码,并进行如下操作
在桌面递归创建文件夹 mkdir -p ~/Desktop/com/zj/study/jvm/classloader
将MyTest1.class文件 移动到上面创建的文件夹中 cp com/zj/study/jvm/classloader/MyTest1.class ~/Desktop/com/zj/study/jvm/classloader
重新build一下项目
代码如下
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object = clazz1.newInstance();
System.out.println(object);
}
}
此时使用的还是系统类加载器,原因和上面那个例子相似.
试验3
还是同样的代码,进行如下操作
将classpath 下的MyTest1.class删除
代码如下
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object = clazz1.newInstance();
System.out.println(object);
}
}
此时 使用的是自定义类加载器
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
试验4
新增了一个自定义类加载器loader2,加载路径和加载的类一致,重新build一下项目
代码如下
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object1 = clazz1.newInstance();
System.out.println(object1);
MyTest16_2 loader2 = new MyTest16_2("loader2");
loader2.setPath("/Users/zj/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz2.hashCode());
System.out.println("classLoaderName: " + clazz2.getClassLoader());
Object object2 = clazz1.newInstance();
System.out.println(object2);
}
}
此时使用的是系统类加载器,class1 和 class2 是同一个对象
loader1 和 loader2 的父类加载器都是系统类加载器,classpath下有MyTest1.class文件,所以此时系统类加载器此时可以加载MyTest1,
所以loader1 和 loader2 委托系统类加载器加载MyTest1
试验5
同样的代码,将classpath 下的MyTest1.class删除
代码如下
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object1 = clazz1.newInstance();
System.out.println(object1);
MyTest16_2 loader2 = new MyTest16_2("loader2");
loader2.setPath("/Users/zj/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz2.hashCode());
System.out.println("classLoaderName: " + clazz2.getClassLoader());
Object object2 = clazz1.newInstance();
System.out.println(object2);
}
}
此时loader1 和 loader2 分别加载了Mytest1 两个class1 和 class2 不是同一个对象
loader1 和 loader2 的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader1 和 loader2 有各自的命名空间,命名空间概念后面再说
试验7
将loader2的父加载器改为loader1
将classpath 下的MyTest1.class删除
public class Test {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object1 = clazz1.newInstance();
System.out.println(object1);
MyTest16_2 loader2 = new MyTest16_2(loader1, "loader2");
loader2.setPath("/Users/zj/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz2.hashCode());
System.out.println("classLoaderName: " + clazz2.getClassLoader());
Object object2 = clazz1.newInstance();
System.out.println(object2);
}
}
此时使用的loader1 加载类,两个class1 和 class2 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader2的父类加载器是loader1,loader1能加载
loader1 和 loader2 是同一个命名空间
此时同样的代码,重新build一下项目
此时使用的是系统类加载器,两个class1 和 class2 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载
类加载器好像是层次结构,但真实情况是包含关系,子加载器包含父加载器的引用
loader1 和 loader2 虽然都是MyTest16_2的实例,但是loader1可以作为loader2的父加载器
试验8
新增loader3,将classpath 下的MyTest1.class删除
public class Test {
//
// 此时若
//
//
//
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object1 = clazz1.newInstance();
System.out.println(object1);
MyTest16_2 loader2 = new MyTest16_2(loader1, "loader2");
loader2.setPath("/Users/zj/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz2.hashCode());
System.out.println("classLoaderName: " + clazz2.getClassLoader());
Object object2 = clazz1.newInstance();
System.out.println(object2);
MyTest16_2 loader3 = new MyTest16_2("loader3");
loader3.setPath("/Users/zj/Desktop/");
Class<?> clazz3 = loader3.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz3.hashCode());
System.out.println("classLoaderName: " + clazz3.getClassLoader());
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
此时loader1、loader2使用的loader1 加载类,两个class1 和 class2 是同一个对象
loader3 使用loader3加载类,class3 是另外一个对象
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader2的父类加载器是loader1,loader1能加载
loader1 和 loader2 是同一个命名空间
loader3的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader3 和 loader1 不是同一个命名空间
同样的代码,重新build一下项目
此时使用的是系统类加载器,class1、class2、class3 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader3同loader1
试验9
将loader3的父加载器改成loader2,此时若将classpath 下的MyTest1.class删除
public class Test {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
MyTest16_2 loader1 = new MyTest16_2("loader1");
// 这个路径是新的文件夹
loader1.setPath("/Users/zj/Desktop/");
Class<?> clazz1 = loader1.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz1.hashCode());
System.out.println("classLoaderName: " + clazz1.getClassLoader());
Object object1 = clazz1.newInstance();
System.out.println(object1);
MyTest16_2 loader2 = new MyTest16_2(loader1, "loader2");
loader2.setPath("/Users/zj/Desktop/");
Class<?> clazz2 = loader2.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz2.hashCode());
System.out.println("classLoaderName: " + clazz2.getClassLoader());
Object object2 = clazz1.newInstance();
System.out.println(object2);
MyTest16_2 loader3 = new MyTest16_2(loader2,"loader3");
loader3.setPath("/Users/zj/Desktop/");
Class<?> clazz3 = loader3.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz3.hashCode());
System.out.println("classLoaderName: " + clazz3.getClassLoader());
Object object3 = clazz3.newInstance();
System.out.println(object3);
}
}
此时loader1、loader2,loader3使用的loader1 加载类,class1、class2、class3 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器并不能加载,扩展类加载器和根类加载器也不能加载,所以只能自己加载
loader2的父类加载器是loader1,loader1能加载
loader3的父类加载器是loader2,loader2的父类加载器是loader1,loader1能加载
loader1 loader2 loader3 是同一个命名空间
重新build一下项目
此时使用的是系统类加载器,class1、class2、class3 是同一个对象
loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载
loader3的父类加载器是loader2,loader2的父类加载器是loader1,loader1的父类加载器是系统类加载器,而系统类加载器可以加载
通过自定义类加载器来进一步理解各个类加载器负责的范围
自定义类加载器MyClassLoader
public class MyClassLoader extends ClassLoader{
private String classLoaderName;
private final String fileExtension = ".class";
private String path;
public void setPath(String path) {
this.path = path;
}
public MyClassLoader(String classLoaderName) {
super();
this.classLoaderName = classLoaderName;
}
public MyClassLoader(ClassLoader classLoader, String classLoaderName) {
super(classLoader);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
// 这两个信息输出 说明findClass()方法被调用
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) {
InputStream is = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
// windows 系统 替换成\\
className = className.replace(".", "/");
try {
is = new FileInputStream(new File( this.path + className + this.fileExtension));
baos = new ByteArrayOutputStream();
int ch = 0;
while (-1 != (ch = is.read())){
baos.write(ch);
}
data = baos.toByteArray();
}catch (Exception e) {
e.printStackTrace();
}finally {
try {
is.close();
baos.close();
}catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
@Override
public String toString() {
return "[" + this.classLoaderName + "]";
}
}
试验1
看如下代码
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader("loader1");
myClassLoader.setPath("/Users/zj/Desktop/");
Class<?> clazz = myClassLoader.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz.hashCode());
System.out.println("class loader: " + clazz.getClassLoader());
}
}
此时 是由系统类加载器加载的
试验2
把com.zj.study.jvm.classloader.MyTest1 放到 /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes目录
具体操作
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre
sudo mkdir -p classes/com/zj/study/jvm/classloader
cd /Users/zj/workspace/java/study/jdk8/target/classes
sudo cp com/zj/study/jvm/classloader/MyTest1.class /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes/com/zj/study/jvm/classloader
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader("loader1");
myClassLoader.setPath("/Users/zj/Desktop/");
Class<?> clazz = myClassLoader.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz.hashCode());
System.out.println("class loader: " + clazz.getClassLoader());
}
}
此时是由启动类加载器加载的
修改启动类加载器加载的目录,如
java -Dsun.boot.class.path=. com.zj.study.jvm.classloader.MyTest18
会出现如下异常
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object
在Oracle的Hotspot实现中,系统属性sun.boot.class.path如果设置错误,则运行会出错,提示如下错误信息
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object
把/Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes目录 com.zj.study.jvm.classloader.MyTest1删除
试验3
把项目打成jar包 放到 /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/lib/ext看是否由扩展类加载器加载
具体操作
sudo rm -rf /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/classes
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/lib/ext
public class Test {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader myClassLoader = new MyClassLoader("loader1");
myClassLoader.setPath("/Users/zj/Desktop/");
Class<?> clazz = myClassLoader.loadClass("com.zj.study.jvm.classloader.MyTest1");
System.out.println("class: " + clazz.hashCode());
System.out.println("class loader: " + clazz.getClassLoader());
}
}
此时是由扩展类加载器加载
扩展类加载器加载的是jar包,单纯的把.class文件放到扩展类加载器负责的目录是不可以的
然后删除
cd /Library/Java/JavaVirtualMachines/jdk1.8.0_221.jdk/Contents/Home/jre/lib/ext
sudo rm -rf jdk8-1.0.jar