• 类加载器分类以及著名的双亲委派机制


    类加载器的分类

    JVM 支持的类加载器是分成两类的:

    1、引导类加载器(BootstrapClassLoader)

    2、自定义类加载器(User-Defined ClassLoader)

    但在上面图示中,我们发现了加载阶段涉及:BootstrapClassLoader、ExtClassLoader、ApplicationClassLoader 所以这个是不是觉得有点矛盾呢?

    从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是 Java虚拟机规范却没有这么定义

    而是将所有 派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器(也就是继承于 ClassLoader 的类加载器)

    无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示:

    虚拟机自带的类加载器

    启动类加载器(引导类加载器,Boostrap ClassLoader)

    • 这个类加载使用 C / C++ 语言实现的,嵌套在 JVM 内部(JVM 的一部分)
    • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar 或 sun.boot.class.path 路径下的内容),用于提供JVM自身需要的类
    • 并不继承自 java.lang.ClassLoader(肯定不继承啦,因为是 C++ 实现的,怎么可能继承 Java的类呢)没有父加载器
    • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
    • 出于安全考虑,Bootstrap启动类加载器只加载包名为 java、javax、sun 等开头的类

    扩展类加载器(Extension ClassLoader)

    • Java语言编写,由 sun.misc.Launcher$ExtClassLoader 实现
    • 派生于 ClassLoader 类
    • 父类加载器为启动类加载器
    • java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 的安装目录的 jre/lib/ext 子目录下(扩展目录)加载类库。如果用户创建的 JAR 放在此目录下,也会自动由扩展类加载器加载。(扩展类加载器,也就是加载核心包之外的扩展包)

    应用程序类加载器(系统类加载器,AppClassLoader)

    • Java语言编写,由 sun.misc.Launcher$AppcClassLoader 实现
    • 派生于 ClassLoader 类
    • 父类加载器为扩展类加载器
    • 它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
    • 该类加载是程序中默认的类加载器,一般来说,Java 应用的类都是由它来完成加载
    • 通过 ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器

    如何获取类加载器?

    此外,我们还可以在代码里面去获取到一些类加载器:

    1. /**
    2. * Date:   2022/10/26 20:23
    3. * Github: https://github.com/chenjjiaa
    4. */
    5. public class ClassLoaderTest {
    6.    public static void main(String[] args) {
    7.        // 获取系统类加载器
    8.        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    9.        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
    10.        // 获取上层
    11.        ClassLoader extClassLoader = systemClassLoader.getParent();
    12.        System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586
    13.        // 试图获取上层,会发现获取不到引导类加载器
    14.        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
    15.        System.out.println(bootstrapClassLoader); // null
    16.        // 用户自定义的类是使用哪个ClassLoader?答:AppClassLoader
    17.        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
    18.        System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
    19.        // String类 是通过引导类加载器进行加载的 ----> Java的核心类库都是通过BootstrapClassLoader进行加载的
    20.        ClassLoader classLoader1 = String.class.getClassLoader();
    21.        System.out.println(classLoader1); // null,获取不到,进而证明了String是通过引导类加载器进行加载的
    22.   }
    23. }
    24. 复制代码

    类加载器可以加载哪些目录?

    1. /**
    2. * Date:   2022/10/27 13:11
    3. * Github: https://github.com/chenjjiaa
    4. */
    5. public class BootstrapURLTest {
    6.    public static void main(String[] args) {
    7.        System.out.println("*********** BootstrapClassLoader ***********");
    8.        // 获取BootstrapClassLoader能够加载哪些路径下的jar
    9.        URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
    10.        for (URL urL: urLs) {
    11.            System.out.println(urL);
    12.       }
    13.        System.out.println("*********** ExtClassLoader ***********");
    14.        String paths = System.getProperty("java.ext.dirs");
    15.        for (String path : paths.split(";")) {
    16.            System.out.println(path);
    17.       }
    18.        // 从上面的路径中随意选择一个jar包中的,来看看他的类加载器是什么 ----> ExtClassLoader
    19.        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
    20.        System.out.println(classLoader1); // sun.misc.Launcher$ExtClassLoader@4b67cf4d
    21.   }
    22. }
    23. 复制代码

    用户自定义类加载器

    在这里不会过多说明,具体在下一个大模块:《字节码与类加载》会详细解读

    在 Java 的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式

    为什么要自定义类加载器?

    • 隔离加载类

      在某些框架当中需要使用中间件,中间件和应用模块是隔离的,所以就要把这些类加载到不同的环境当中,确保我们引用的框架、jar包,与中间件使用的 jar包是不冲突的

      主要是由于中间件都有自己依赖的 jar包,而在同一个工程里面引入多个框架有可能会出现某些类路径一样、类名相同的情况。在这种情况下就会出现类的冲突。那么此时就要进行类的仲裁。像现在主流的容器类框架都会自定义一些类的加载器,从而实现不同的中间件相互之间是隔离的,避免类的冲突

    • 修改类加载的方式

      BootstrapClassLoader 是一定需要的,因为要加载系统所需要的一些核心 api。但除了 BootstrapClassLoader 之外的其他类加载器,那就不一定需要了。可以通过实际需求来引入。所以可以通过自定义类加载器,在需要的时候动态加载

    • 扩展加载源

      也就是扩展类加载进 JVM 的来源。一般来说,类的加载可以通过 .zip.war.jar 获取、通过网络、通过本地系统直接加载。但还有可能要通过一些特殊的途径来加载:比如通过数据库,又或者通过机顶盒,这时我们就要扩展加载源了

    • 防止源码泄漏。因为 Java 文件很容易被反编译,为了防止代码泄露或篡改,可以在字节码文件上加密,然后通过自定义的类加载器进行解密、加载

    用户自定义类加载器的实现步骤

    具体细节在下个大模块里说明

    • 开发人员可以通过继承抽象类 java.lang.ClassLoader 类的方式,实现自己的类加载器,以满足一些特殊的需求
    • 在 JDK1.2 之前,在自定义类加载器时,总会去继承 ClassLoader 类并重写 loadClass() 方法(这个方式有点复杂,就是需要指定的结构非常多),从而实现自定义的类加载类,但是在 JDK1.2 之后已不再建议用户去覆盖 loadClass () 方法,而是建议把自定义的类加载逻辑写在 findClass()方法中
    • 在编写自定义类加载器时,如果没有太过于复杂的需求(加解密之类),可以直接继承 URLClassLoader 类这样就可以避免自己去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁

    关于 ClassLoader

    ClassLoader 类是抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)

    sun.misc.Launcher 是一个JVM 的入口应用。扩展类加载器和应用类加载器都是 Launcher 里定义的内部类

    获取 ClassLoader 的途径

    方式途径
    方式1:获取当前类ClassLoaderXXX类.class.getClassLoader();
    方式2:获取当前线程上下文ClassLoaderThread.currentThread().getContextClassLoader();
    方式3:获取系统的ClassLoaderClassLoader.getSystemClassLoader();
    方式4:获取调用者的ClassLoader (待证实)DriverManager.getCallerClassLoader(); (待证实)

    至于方式4,现在在代码层面暂时敲不出来,只看到了反射类有涉及

    双亲委派机制

    Java 虚拟机对 class file 采用的是按需加载的方式,也就是说,当需要使用该类才会将他的 calss file 加载到内存中生成 class 对象。而且加载某个类的 class file 时,JVM 采取双亲委派模式,即把请求交由父类处理,他是一种任务委派模式

    工作原理

    • 如果一个类加载器收到了类加载的请求,他并不会自己先去加载,而是把这个请求委托给父类加载器去执行
    • 如果一个父类加载器还存在其他父类加载器,则进一步向上委托,依次递归,请求最终达到顶层的 BootstrapClassLoader(引导类加载器)
    • 如果父类加载器可以完成类加载任务,就成功返回,若父类加载器无法完成加载此任务,子加载器就会尝试自己去加载,这就是双亲委派模式

    例1

    先随便写一个测试类

    1. public class StringTest {
    2.    public static void main(String[] args) {
    3.        // 这里会不会输出:"这是我们自定义的 String 类" 呢?
    4.        String string = new java.lang.String();
    5.        System.out.println("hello!");
    6.   }
    7. }
    8. 复制代码

    再在根目录下新建包 java.lang,在此包下新建一个类,叫 String

    1. package java.lang;
    2. public class String {
    3.    static {
    4.        System.out.println("这是我们自定义的 String 类");
    5.   }
    6. }
    7. 复制代码

    那么此时,StringTest 类中的 main() 方法运行时,会不会打印出自定义 String 里的无参构造器中的内容呢?

    答案是:没输出自定义 String 里的无参构造器中的内容

    说明了此时使用的 String 还是 Java 核心 API 里面的 String 类

    这种机制就蕴含了双亲委派机制。主要是为了防止我们的代码/项目受到恶意攻击(如果没有这个机制,那么这个 String 从网络上加载下来的,可能含有恶意代码,然后让整个项目挂掉)

    BootstrapClassLoader 一看这个 String 是 java. 包下的,就说:好,这事儿归我管。 BootstrapClassLoader 能加载哪些包下的类?

    例2

    在自定义 String 类下面运行 main() 方法

    1. /**
    2. * Date:   2022/10/31 22:37
    3. * Github: https://github.com/chenjjiaa
    4. */
    5. public class String {
    6.    static {
    7.        System.out.println("这是我们自定义的 String 类");
    8.   }
    9.    // 此时调用 main 方法会出现问题
    10.    public static void main(String[] args) {
    11.        System.out.println("hello,这是自定义String的main方法~");
    12.   }
    13. }
    14. /** =================== 报错 ===================
    15. * 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
    16. *   public static void main(String[] args)
    17. * 否则 JavaFX 应用程序类必须扩展javafx.application.Application
    18. */
    19. 复制代码

    这就说明了:使用的 String 是 Bootstrap ClassLoader 加载的 Java 原生 api 中的 String 了,而核心包下的 String 没有 main() 。正好印证了双亲委派机制

    例3(反向委托)

    例如我们在项目中加载 JDBC 的有关类

    当我们加载 jdbc.jar 用于实现数据库连接的时候,首先我们需要知道的是 jdbc.jar 是基于 SPI 接口进行实现的,所以在加载的时候,会进行双亲委派机制,接口是由 BootstrapClassLoader 加载的,而具体接口的第三方实现类是由线程上下文类加载器加载的,也就是我们系统类加载器,来完成 jdbc.jar 的加载

    扩展:Java 的 SPI 机制是什么?。SPI 机制的实际应用就是如:JDBC包的加载、SpringBoot 自动装配原理等,作用是极大的减轻了框架之间的耦合度,使得扩展性大大增强,能更加便捷的引入外部包

    例4

    在自定义的 java.lang包下新建自定义类,跑一遍

    1. package java.lang; // 禁止自定义类用 java 核心包的名字
    2. public class ChenjjiaHello {
    3.    public static void main(String[] args) {
    4.        System.out.println("hello!");
    5.   }
    6. }
    7. 复制代码

    会出现以下错误:

    因为 java.lang 是引导类加载器负责管的,这个类试图用引导类加载器去加载,会失败。也是为了防止核心 API 被篡改,同时防止引导类加载器被破坏

    双亲委派机制的优势

    • 避免类的重复加载
    • 保护应用程序安全,防止核心 API 被篡改

    沙箱安全机制

    自定义 String 类,但是在加载自定义 String 类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载 JDK 自带的文件(rt.jar包中 java\lang\String.class),报错信息说没有 main 方法,就是因为加载的是 rt.jar 包中的 String 类。这样可以保证对 Java 核心源代码的保护,这就是沙箱安全机制

    其它

    判断两个 Class 对象是否是同一个类

    • 类的全限定类名必须一致

    • 加载这两个类的 ClassLoader 必须相同

      比如刚刚的两个 String 类:自定义的 String 和 系统自带的 String 不是相同的类。即使他们包名一样,但不是使用同一个 ClassLoader 加载,故不是统一个类

    • 换句话说,在 JVM 中,即使这两个类对象(Class对象)来源同一个 class 文件,被同一个虚拟机所加载,但只要加载它们的 ClassLoader 实例对象不同,那么这两个类对象也是不相等的

    对类加载器的引用

    JVM 必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。(很正常,通过这个类调用 getClassLoader() 就可以知道)

    如果一个类型是由用户类加载器加载的,那么 JVM 会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中

    当解析一个类型到另一个类型的引用的时候,JVM 需要保证这两个类型的类加载器是相同的( 动态链接部分讲解

    类的主动使用和被动使用

    Java 程序对类的使用方式分为:主动使用和被动使用

    主动使用,又分为七种情况:

    • 创建类的实例
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射。比如: Class.forName("com.chenjjia.Test")
    • 初始化一个类的子类
    • Java 虚拟机启动时被标明为启动类的类
    • JDK7 开始提供的动态语言支持:

    java.lang.invoke.MethodHandle 实例的解析结果 REF_getStaticREF_putStaticREF_invokeStatic 句柄对应的类没有初始化,则初始化

    除了以上七种情况,其他使用 Java 类的方式都被看作是对类的被动使用,都不会导致类的初始化

  • 相关阅读:
    LeetCode-66-加一
    CMakeList整理大全
    web期末大作业:基于html+css+js制作 学校班级网页制作----校园运动会 4页
    前端进击笔记第十四节 单页应用与前端路由库设计原理
    C语言之数组练习题
    Surface area
    常用的软件架构设计模式
    跟运维学 Linux - 02
    038-JTable控件应用案例讲解
    荧光素标记氨基酸,异硫氰酸荧光素FITC标记D-天冬氨酸;FITC-D-Aspartic acid
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/127668497