Java类加载器和双亲委派模型


这里讲了类加载器和双亲委派模型。
讲到的很多东西都是很底层的东西,一些源码的实现。
如果只是想简单了解双亲委派模型的,可以看看类加载器的分类和层级关系,然后直接去看最后的总结。最后的图把整个流程都描述了。

类的生命周期

在这里插入图片描述

  • 加载:查找并加载类的二进制数据.class,
  • 通过一个类的全限定名来获取此类的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在java方法区中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。
  • 连接
    • 验证:确保加载的类的正确性,保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且确定不会危害虚拟机自身的安全。
    • 准备:为类的静态变量分配内存,并将其初始化为默认值。
      正式为类变量分配内存并设置类变量初始值(各数据类型的默认值)的阶段,这些内存将在方法区中进行分配。
      但是如果类字段的字段属性表中存在常量属性,那在准备阶段变量值就会初始化为常量属性指定的值。
    • 解析:把类中的符号引用转为直接引用,在虚拟机将常量池内的符号引用替换为直接引用的过程。
  • 初始化:为类的静态变量赋予正确的初始值,执行静态代码块。

类型的加载

类型就是指我们Java源代码通过编译后的class文件,类型的来源有哪些?

  1. 本地磁盘
  2. 网络下载的.class文件
  3. war/jar包下载的.class文件
  4. 从专门的数据库中读取的.class文件(少见)
  5. 将Java源文件动态编译成class文件:
    • 典型的就是动态代理,通过运行期生成class文件。
    • 我们的jsp会被转换成servlet,而我们的servlet是一个java文件,会被编译成class文件。

class文件经过类加载器加载到JVM内存中的方法区的数据区。
方法区:1.7叫方法区,1.8叫元空间。
数据区:用来存储class数据的结构,class文件的数据结构都存储在数据区。

在元空间中生成的一个个class对象都有一个引用指向数据区的数据结构,class对象可以访问到数据区的数据结构。
在这里插入图片描述

在元空间中生成的一个个class对象,比如AClass对象,在1.7中存放在方法区,1.8中没有说明具体放在哪(有的虚拟机可能存放在堆内存),根据不同JVM的规范来实现。
在HotSpot虚拟机中,jdk1.8中Class对象还是存放在方法区。

类加载器的分类、职责以及层级结构

  • 系统级别:
  • 启动类加载器 --> C++语言实现
  • 扩展类加载器
  • 系统类加载器(App类加载器)
    扩展类加载器和App类加载器是通过Java来实现,有统一的父类(ClassLoader)
  • 用户级别的:用户自定义类加载器(继承ClassLoader)。
    在这里插入图片描述
    这里的层级关系并不是说下层的加载器去继承上层的加载器,而是当前类加载器中的parent引用指向父加载器。

类加载器加载的目录

每个加载器的职责不一样,加载的目录也不一样。

  • 启动类加载器(BootStrapClassLoader)
    是java应用体系中最顶层的类加载器,负责加载JVM需要的一些类库。是一个由C++编写的类加载器。
    系统变量"sun.boot.class.path"表示启动类加载器的加载目录:System.getProperty(“sun.boot.class.path”)。
    我们可以把class文件放到D:\java\jdk1.8\jre\classes 目录下,这样我们自己的类也能通过启动类加载器来进行加载,(本地的D:\java\jdk1.8\jre目录下载jdk时就已经存在,classes文件夹需要我们自己创建)。
    在这里插入图片描述
  • 扩展类加载器(ExtClassLoader)
    系统变量"java.ext.dirs"表示扩展类加载器的加载目录:System.getProperty(“java.ext.dirs”)。
    在这里插入图片描述
  • App类加载器
    系统变量"java.class.path"表示App类加载器的加载目录:System.getProperty(“java.class.path”)。
    App类加载器加载classpath目录下的class文件,也就是平常我们自己写的代码/引入的jar都是由App类加载器来加载。
    在这里插入图片描述

Launcher类

sun.misc.Launcher类位于rt.jar包中,由启动类加载器来加载,sun.misc.Launcher类是java程序的的入口,在启动java应用的时候会首先创建Launcher类,创建Launcher类的时候会准备应用程序运行中需要的类加载器。

Laucher类是由JVM创建的,它类加载器是系统类加载器(BootStrapClassLoader)。


查看Laucher类的源码会发现,ExtClassLoader和AppClassLoader是Launcher类的静态内部类。
在Launcher类的构造方法中,会获取AppClassLoader和ExtClassLoader,然后把ExtClassLoader指定为AppClassLoader的父加载器(AppClassLoader类中的parent引用指向ExtClassLoader,)。

在Launcher类的构造方法中,还会把AppClassLoader设置为当前线程上下文的ClassLoader,Thread.currentThread().setContextClassLoader(this.loader);
然后可以在线程中随时获取线程上下文的类加载器:Thread.currentThread().getContextClassLoader();

ClassLoader和loadClass方法

AppClassLoader和ExtClassLoader继承了URLClassLoader,URLClassLoader继承了SecureClassLoader,SecureClassLoader继承了ClassLoader,通过调用ClassLoader类中的loadClass方法来加载类。
在这里插入图片描述
当getClassLoader()为null就表示加载器是BootStrapClassLoader了。
在这里插入图片描述

loadClass方法

在loadClass中,会先去得到当前类加载器的父加载器,如果parent不为null,则交给父加载器来进行类加载,parent.loadClass()。如果父类加载器为null,则说明父类加载器为启动类加载器,则使用启动类加载器来进行类加载,c = findBootstrapClassOrNull(name);



if(c == null) 则说明这个类的.class文件不在启动类加载器加载的目录下,启动类加载器不能加载。则会让子加载器自己去加载。

下面是ClassLoader类的loadClass源码:
在这里插入图片描述
在这里插入图片描述
c = findClass(name); 真正的去加载class,调用的是其直接父类的findClass方法,也就是URLClassLoader类的findClass方法。
findClass方法中就是去该加载器的加载路径下查找有没有该类的class文件,方法中有这么一行代码:Resource res = ucp.getResource(Path, false);

比如我们自己写了一个Teacher类,在ExtClassLoader的加载路径下肯定是找不到Teacher.class文件的,Resource
为null。父加载器ExtClassLoader加载不了,则会交给AppClassLoader自己来加载,在classpath下能找到Teacher.class,所以AppClassLoader能加载成功。

在这里插入图片描述
在这里插入图片描述

什么时候会触发类加载器去加载类?

类的主动使用会触发类的初始化,初始化一定会有类的加载,类加载不一定会初始化。

类的主动使用:

  1. 调用类的静态字段
  2. 调用类的静态方法
  3. 执行main方法(执行A类中的main方法就是主动使用A类,会触发A类的初始化)
  4. new指定 :new一个类的对象
  5. Class.forname("") 反射,输入一个类的全限定名。
  6. 子类初始化一定会触发父类初始化

自定义类加载器

我们自定义的类加载的父类一般都是AppClassLoader,自定义类加载器时会指定该类加载器的加载路径。
ClassLoader的源码中的文档给出了自定义加载器的例子
在这里插入图片描述
自定义加载器需要去继承ClassLoader类,然后重写findClass方法。还需要再写一个loadClassData方法:给定一个class文件的全类名,把.class文件读取成一个byte[]。然后通过一个JVM底层的方法defineClass()把byte[]转成一个Class对象。

defineClass方法:本地的native方法,c++实现。

package com.zlin.jvm.myClassLoader;

import java.io.*;

/**
 * 自定义的类加载器
 */
public class MyClassLoader extends ClassLoader{
    private final static String fileSuffixExt = ".class";

    // 加载器的名称
    private String classLoaderName;

    // 加载器的加载路径
    private String loadPath;

    public void setLoadPath(String loadPath) {
        this.loadPath = loadPath;
    }

    public MyClassLoader(ClassLoader parent, String classLoaderName) {
        // 指定当前类加载器的父类加载器
        super(parent);
        this.classLoaderName = classLoaderName;
    }


    public MyClassLoader(String classLoaderName) {
        // 不指定父类加载器,则默认使用AppClassLoader加载器作为父类加载器
        super();
        this.classLoaderName = classLoaderName;
    }

    public MyClassLoader(ClassLoader classLoader) {
        super(classLoader);
    }

    /**
     * 方法实现说明
     * @param name: 类的二进制名称
     */
    private byte[] loadClassData(String name) {
        byte[] data = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        InputStream inputStream = null;

        try {
            name = name.replace(".", "\\");
            String fileName = loadPath + name + fileSuffixExt;
            File file = new File(fileName);
            inputStream = new FileInputStream(file);

            byteArrayOutputStream = new ByteArrayOutputStream();
            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }
            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
                if (inputStream != null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    protected Class<?> findClass(String name) {
        byte[] data = loadClassData(name);
        System.out.println("MyClassLoader加载的类:" + name);
        return defineClass(name, data, 0, data.length);
    }
}

E:\zlin\路径下存在Student.class, AppClassLoader的加载路径下也存在,根据双亲委托模型,会使用AppClassLoader来加载Student类。当把classpath下的Student.class文件删掉后, AppClassLoader的加载路径下找不到Student.class ,才会使用MyClassLoader来进行加载。
在这里插入图片描述
在这里插入图片描述

类加载器的命名空间

类加载器的命名空间:是由类加载器本身以及所有父加载器所加载出来的binary name(full class name) 组成。

  1. 在同一个命名空间里,不允许出现两个完全一样的binary name。
  2. 在不同的命名空间中,可以出现两个相同的binary name。
  3. 子加载器的命名空间中的binary name对应的类中可以访问父加载器命名空间中的binary name对应的类,反之不行。

子加载器加载的类能访问父加载器加载的类,父加载器加载的类不能访问子加载器加载的类。比如:我们自定义一个Person类(使用AppClassLoader加载),
在Person类中可以使用String/Object类(使用启动类加载器加载),但是在String/Object类中不能访问Person类。

同一个Person.class文件被不同的类加载器加载,则我们的JVM内存中会生成多个Person的Class对象,而且这两个对应的Class对象是相互不可见的(通过Class对象反射创建的实例对象相互是不能兼容的,不能相互转型)。
在这里插入图片描述

下面看一个例子:
Student类

package com.zlin.jvm.myClassLoader;

public class Student {
    private Student student;

    public Student getStudent() {
        return student;
    }

    public void setStudent(Object student) {
        this.student = (Student) student;
    }
}

package com.zlin.jvm.myClassLoader;

import java.lang.reflect.Method;

public class DifferentClassLoaderNameSpaceTest {
    public static void main(String[] args) throws Exception{
        MyClassLoader myClassLoader1 = new MyClassLoader("myClassLoader1");
        myClassLoader1.setLoadPath("E:\\zlin\\");

        MyClassLoader myClassLoader2 = new MyClassLoader("myClassLoader2");
        myClassLoader2.setLoadPath("E:\\zlin\\");

        // 通过myClassLoader1加载Student
        Class<?> clazz1 = myClassLoader1.loadClass("com.zlin.jvm.myClassLoader.Student");
        System.out.println("class1的类加载器" + clazz1.getClassLoader());

        // 通过myClassLoader2加载Student
        Class<?> clazz2 = myClassLoader2.loadClass("com.zlin.jvm.myClassLoader.Student");
        System.out.println("class2的类加载器" + clazz2.getClassLoader());

        System.out.println("class1 == class2 :" + (clazz1 == clazz2));

        // 当classpath目录下Student.class存在时,clazz1和clazz2都是AppClassLoader来进行加载
        // 当把classpath目录下的Student.class删除后,clazz1使用myClassLoader1进行加载,class2使用myClassLoader2进行加载
        // 两个类的命名空间不同
        // java.lang.ClassCastException: com.zlin.jvm.myClassLoader.Student cannot be cast to com.zlin.jvm.myClassLoader.Student

        // 模拟问题
        Object student1 = clazz1.newInstance();

        Object student2 = clazz2.newInstance();


        Method method = clazz1.getMethod("setStudent", Object.class);

        method.invoke(student1, student2);

    }
}

当classpath目录下Student.class存在时,clazz1和clazz2都是AppClassLoader来进行加载,setStudent方法能正常执行。

当把classpath目录下的Student.class删除后,clazz1使用myClassLoader1进行加载,class2使用myClassLoader2进行加载,两个类的命名空间不同,执行setStudent抛出java.lang.ClassCastException: com.zlin.jvm.myClassLoader.Student cannot be cast to com.zlin.jvm.myClassLoader.Student
在这里插入图片描述
当你在生产环境中遇见这种问题, java.lang.ClassCastException: com.zlin.jvm.myClassLoader.Student cannot be cast to com.zlin.jvm.myClassLoader.Student,相同的两个类不能相互转换,则可能就是使用不同的类加载器加载到不同的命名空间中了。

双亲委派模型的好处

双亲委派模型的好处:核心包下的类不能使用我们自定义的类加载器去加载。
如果没有双亲委派模型,比如我们能自定义类加载器去加载Object类,则JVM中会存在多个Object的Class对象且它们之间不可见,不能相互转型(这是非常危险的)。

打破双亲委派模型

举一个例子:我们的规范类会使用到数据库厂商jar包中的类,比如DriverManager需要注册具体数据库厂商的驱动。
但是根据全盘委托模型,启动类加载器无法加载classpath目录下的jar包。
在这里插入图片描述
– 怎么解决?
JVM中规定了,在启动类加载器(DriverManager)中可以访问到当前线程上下文类加载器所加载的类,我们的线程上下文类加载器是AppClassLoader。
还记得上面提到的在Launcher类的构造方法中会把AppClassLoader设置为当前线程上下文的类加载器。

这里会涉及到SPI(服务扩展机制)的一些东西,如果不懂SPI可能看不懂,有兴趣的小伙伴可以看看这篇博客SPI服务扩展机制

  • Class.forName(“com.mysql.jdbc.Driver”):
    只会注册一个驱动com.mysql.jdbc.Driver,在Driver类的静态代码块中: DriverManager.registerDriver(new Driver());来注册驱动。
  • Connection connection = DriverManager.getConnection(“jdbc:mysql://localhost:3306/test”, “root”, “admin”);
    DriverManager的构造方法中的loadInitialDrivers()会通过SPI服务把mysql-connector-java-8.0.18.jar下META-INF/services/java.sql.Driver文件中的所有驱动都注册(包含"com.mysql.jdbc.Driver")。
    通过SPI服务扩展机制:Iterator遍历将mysql-connector-java-8.0.18.jar下META-INF/services/java.sql.Driver文件中的类 来进行Class.forName("");
    查看源码会发现,在getConnection()中会去获取线程上下文的类加载器,然后能读取和使用获取线程上下文的类加载器加载的类。
    在这里插入图片描述

在这里插入图片描述

当把当前线程上下文的ClassLoader改为ExtClassLoader后

  • Class.forName(“com.mysql.jdbc.Driver”);任能注册一个驱动com.mysql.jdbc.Driver (反射 ==> 会触发类的初始化 ==> 执行静态代码块)
    因为在Driver类的静态代码块中: DriverManager.registerDriver(new Driver());
  • 但是Connection connection = DriverManager.getConnection(“jdbc:mysql://localhost:3306/test”, “root”, “admin”);
    不能加载mysql-connector-java-8.0.18.jar下META-INF/services/java.sql.Driver文件中的类,因为ExtClassLoader的加载路径不包含classpath/第三方jar, classpath/引入的第三方jar属于AppClassLoader的加载路径。不能注册驱动。
package com.zlin.jvm.classLoadNameSpace;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class TestDemo {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {

        Class.forName("com.mysql.jdbc.Driver"); // 只会注册一个驱动com.mysql.jdbc.Driver
   
		Thread.currentThread().setContextClassLoader(TestDemo.class.getClassLoader().getParent());

		// 任然能注册Driver驱动
        Class.forName("com.mysql.jdbc.Driver");

        // 不能注册驱动
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "admin");
    }
}

这里可能不是很好理解,最好自己能去看看源码,把各个类,执行流程打断点走一遍,就很好理解,印象也会很深。

这里总的来说就是:在启动类加载器中可以访问到当前线程上下文类加载器所加载的类。DriverManager由启动类加载器加载,“com.mysql.jdbc.Driver"mysql驱动的jar由AppClassLoader加载,Launcher类的构造方法中把AppClassLoader设置为当前线程上下文的类加载器,在getConnection方法中通过获取当前线程上下文的类加载器来使用"com.mysql.jdbc.Driver”。
当把当前线程上下文的类加载器修改为ExtClassLoader时,mysql驱动的jar的路径不在ExtClassLoader的加载目录,ExtClassLoader不会加载mysql驱动的jar,获取当前线程上下文的类加载器不能获取"com.mysql.jdbc.Driver"。所以不能加载驱动。

总结

双亲委派模型也叫双亲委托模型/ 双亲委任模型。
双亲委派模型:类加载时,会由下向上依次判断是否加载过,加载过则直接返回,如果到顶层还没有加载。则启动类加载器会尝试加载,当class文件不在启动类加载器的加载目录下,则启动类加载器不能加载,再交给ExrClassLoader来加载,这样依次下去,如果到最底层也不能加载成功,则抛出异常ClassNotFoundException。
在这里插入图片描述
下面的图很详细了
在这里插入图片描述
上面涉及到的代码地址:https://github.com/zhonglinliu123/MyBlogCode/tree/master/JVM/classLoader%E7%B1%BB%E5%8A%A0%E8%BD%BD

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值