JVM第二篇-类的加载

类的加载

一、类的加载过程(生命周期)

1.1 说说类加载分几步?

在这里插入图片描述

1.1.1 谁需要加载?

在Java中数据类型分为基本数据类型和引用数据类型。基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载。

1.2 过程一: Loading(装载)阶段

1.2.1 做了什么事?

将Java类的字节码文件加载到机器内存中,并在内存中构建出Java类的原型–类模板对象。

1.2.2 什么是类模板对象

是Java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期间便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。

反射机制即基于这一基础。如果jvm没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。

1.2.3 Class实例的位置在哪

堆中。

1.2.4 数组类的加载有什么不同?

数组类本身并不是由类加载器负责创建,而是由JVM在运行时根据需要创建的,但数组的元素类型仍然需要依靠类加载器去创建。

1.3 过程二: Linking(链接)阶段

链接过程之验证阶段(Verification)

验证阶段目的是保证加载的字节码是合法、合理并符合规范的。
验证过程大致如下图
在这里插入图片描述
如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说这个类是完全没有问题的。

链接过程之准备阶段

简言之,为类的静态变量分配内存,并将其初始化为默认值。
注意:

  1. 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。
  2. 注意这里不会为实例变量分配初始化,实例变量是会随着对象一起分配到Java堆中。
  3. 在这个阶段并不会向初始化阶段中那样会有初始化或者代码被执行。

链接过程之解析阶段

简言之,将类、接口、字段和方法的符号引用转为直接引用。
通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。

小结

所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。

1.4 过程三: Initialization(初始化)阶段

简言之,为类的静态变量赋予正确的初始值。(显示初始化)
到了初始化阶段,才真正开始执行类中定义的Java程序代码。
初始化阶段的重要工作是执行类的初始化方法:()方法。

  • (): 只有在给类中的static的变量显示赋值或在静态代码块中赋值了。才会生成此方法。
  • (): 一定会出现在Class的method表中。

使用static+final修饰的成员变量(全局变量)的赋值时机

  • 给全局变量赋的值为字面量或常量。不涉及到方法或构造器的调用。会在链接的准备阶段赋值
  • 其它情况都是在初始化阶段赋值。

<clinit>的调用会死锁吗?

会,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步
正因为函数<clinit>()带锁线程安全的,所以可能会引发死锁。

类的初始化情况: 主动使用vs被动使用

主动使用

主动使用的说明:
Class只有在必须要首次使用的时候才会被装载,Java虚拟机不会无条件的装载Class类型。Java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。这里指的"使用",是指主动使用。

  • 创建一个类的实例时,比如使用new关键字,反射,克隆,反序列化
  • 调用类的静态方法时,即使用了字节码invokestatic指令。
  • 当使用了类、接口的静态字段时(final修饰特殊考虑),比如,使用getstatic或者putstatic指令。
  • 当使用java.lang.reflect包中的反射类的方法时。比如Class.forName("Java.lang.String);
  • 当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其弗雷德初始化。
  • 如果一个接口定义了default方法,那么直接实现或者间接实现接口的类的初始化,该接口要在其之前被初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

被动使用

被动使用不会引起类的初始化。
并不是在代码中出现的类,就一定会被加载或者初始化。如果不符合主动使用的条件,类就不会初始化。

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
    • 当通过子类引用父类的静态变量,不会导致子类初始化
  2. 通过数组定义类引用,不会触发此类的初始化
  3. 引用常量不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式赋值了。
  4. 调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。

被动的使用,意味着不需要执行初始化环节,意味着没有<clinit>()的调用。

1.5 过程四:类的使用

1.6 过程五: 类的卸载

二、类的加载器

2.1 作用

类加载器是JVM执行类加载机制的前提。

ClassLoader的作用
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化操作。

因此,ClassLoader只在类的装载阶段起作用。

2.2 类加载的显式加载和隐式加载

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式

  • 显示加载: 指的是在代码中通过调用ClassLoader记载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载Class对象。
  • 隐式加载: 是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

2.3 类加载机制的必要性

  • 避免在开发中遇到java.lang.ClassNotFoundException异常或Java.lang.NoClassDefFoundError异常时,手足无措。只有了解类加载器的加载机制才能够在出现异常的时候快速地更具错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。

2.4 记载的类是唯一的吗?

2.4.1 何为类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

2.4.2 命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

在大型应用中,我们往往借助这一特性,来运行一个类的不同版本。

三、类的加载器分类与测试

3.1 类加载器的分类说明

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)自定义类记载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。

无论类加载器的类型如何划分,在程序中我们常见的类加载结构主要是如下情况:
在这里插入图片描述

3.2 子父类加载器的关系?

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。
  • 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用。

在这里插入图片描述

3.3 具体类的类加载器介绍

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

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

使用-XX:+TraceClassLoading参数得到。
在这里插入图片描述

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所制定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下记载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
    在这里插入图片描述

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

  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 他负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载起。
  • 它是用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器

3.4 用户自定义类加载器

用户自定义类加载器

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以使本地的JAR包,也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  • 同时,自定有加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。
  • 所有用户自定义类加载器通常需要继承于抽象类java.lang.ClassLoader。

3.5 测试不同的类加载器

public class ClassLoaderTest {
    
    
    public static void main(String[] args) {
    
    
        //获取系统该类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);
        //获取扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);
        //试图获取引导类加载器:失败
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);

        //###########################
        try {
    
    
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader);
            //自定义的类默认使用系统类加载器
            ClassLoader classLoader1 = Class.forName("com.atguigu.java3.ClassLoaderTest").getClassLoader();
            System.out.println(classLoader1);

            //关于数组类型的加载:使用的类的加载器与数组元素的类的加载器相同
            String[] arrStr = new String[10];
            System.out.println(arrStr.getClass().getClassLoader());//null:表示使用的是引导类加载器

            ClassLoaderTest[] arr1 = new ClassLoaderTest[10];
            System.out.println(arr1.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2

            int[] arr2 = new int[10];
            System.out.println(arr2.getClass().getClassLoader());//null:不需要类的加载器


            System.out.println(Thread.currentThread().getContextClassLoader());

        } catch (ClassNotFoundException e) {
    
    
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

四、ClassLoader源码分析

4.1 定义与本质

定义

如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

本质

规定了类加载的顺序是: 引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
在这里插入图片描述

4.2 源码分析

子加载器包含父加载器的引用

loadClass()

在这里插入图片描述

findClass()

  • 由自定义类加载器去重写此方法
    在这里插入图片描述

defineClass()

在这里插入图片描述

五、自定义类的加载器

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

  • 隔离加载类
    在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如: 阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如: Tomcat这类web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。 (类的仲裁–>类冲突)
  • 修改类加载的方式
    类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
  • 扩展加载源
  • 比如从数据库、网络、甚至是电视机机顶盒进行加载
  • 防止源码泄漏
  • Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

5.2 自定义类的加载器

public class MyClassLoader extends ClassLoader{
    
    

    String rootPath;

    public MyClassLoader(String rootPath) {
    
    
        this.rootPath = rootPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    
    
        byte[] bytes = getClassByName(name);
        if (bytes == null) {
    
    
            throw new ClassNotFoundException();
        }
        return defineClass(name, bytes, 0 , bytes.length);
    }

    private byte[] getClassByName(String name){
    
    
        String path = getPath(name);
        path = rootPath + path;
        try (InputStream ins = new FileInputStream(path);
             ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
    
    
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = ins.read(bytes)) != -1) {
    
    
                bos.write(bytes, 0, len);
            }
            return bos.toByteArray();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
        return null;
    }

    String getPath(String name) {
    
    
        return name.replace(".", "\\") + ".class";
    }

    public static void main(String[] args) throws ClassNotFoundException {
    
    
        String rootPath = "D:\\IdeaWorkspace\\JVMdachang210416\\chapter02_classload\\src\\";
        MyClassLoader loader1 = new MyClassLoader(rootPath);
        Class clazz1 = loader1.findClass("com.atguigu.java3.User");
        MyClassLoader loader2 = new MyClassLoader(rootPath);
        Class clazz2 = loader2.findClass("com.atguigu.java3.User");
        System.out.println(clazz2 == clazz1);
        System.out.println(clazz1.getClassLoader());
        System.out.println(clazz2.getClassLoader());

        Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.atguigu.java3.User");
        System.out.println(clazz3.getClassLoader());
        System.out.println(clazz1.getClassLoader().getParent());
    }
}

在这里插入图片描述

六、相关机制

6.1 双亲委派机制

工作过程

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

证明?源码分析(↑)

优势与劣势

优势

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

弊端

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以体现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

破坏双亲委派机制及举例

第一次"被破坏"

发生在双亲委派模型出现之前-即JDK1.2出现之前。双亲委派模型在JDK1.2之后才被引入,但是类加载的概念和抽象类ClassLoader则在Java的第一个版本中就已经存在,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型时不得不做出一些妥协,为了兼容这些已有代码,无法再以技术手段避免loadClass()被子类覆盖的可能性,只能在JDK1.2之后的java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写类加载逻辑时尽可能去重写这个方法,而不是在loadClass()中编写代码。

第二次"被破坏"

是由这个模型自身的缺陷导致的,双亲委派很好地解决了各个类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器进行加载),基础类型之所以被称为“基础”,是因为它们总是作为被用户代码继承、调用的API存在,但程序设计往往没有绝对不变的完美规则,如果有基础类型又要调用回用户的代码,那该怎么办呢?

为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

第三次"被破坏"

是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(HotDeployment)等。

例如OSGi实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

1)将以java.*开头的类,委派给父类加载器加载。
2)否则,将委派列表名单内的类,委派给父类加载器加载。
3)否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类查找失败。

6.2 沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。
所有的Java程序运行都可以指定沙箱

在这里插入图片描述
增加了安全策略:
在这里插入图片描述
增加了代码签名:
在这里插入图片描述
引入域:系统域和应用域在这里插入图片描述

七、Tomcat的类加载机制

Tomcat8和Tomcat6比较大的区别是:
Tomcat8可以通过配置表示遵循双亲委派机制。

Tomcat的类加载机制是违反了双亲委派模型的,对于一些未加载的非基础类,各个web应用自己的类加载器(WebAppClassLoader)会优先查看自己的仓库加载,加载不到时再交给CommonClassLoader走双亲委托。
在这里插入图片描述
在这里插入图片描述

Tomcat类加载的源码

public synchronized Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    
    
 
    if (log.isDebugEnabled())
        log.debug("loadClass(" + name + ", " + resolve + ")");
    Class<?> clazz = null;
 
    // Log access to stopped classloader
    if (!started) {
    
    
        try {
    
    
            throw new IllegalStateException();
        } catch (IllegalStateException e) {
    
    
            log.info(sm.getString("webappClassLoader.stopped", name), e);
        }
    }
 
    // (0) Check our previously loaded local class cache
    // 1 
    clazz = findLoadedClass0(name);
    if (clazz != null) {
    
    
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }
 
    // (0.1) Check our previously loaded class cache
    // 2
    clazz = findLoadedClass(name);
    if (clazz != null) {
    
    
        if (log.isDebugEnabled())
            log.debug("  Returning class from cache");
        if (resolve)
            resolveClass(clazz);
        return (clazz);
    }
 
    // (0.2) Try loading the class with the system class loader, to prevent
    //       the webapp from overriding J2SE classes
    // 3 
    try {
    
    
        clazz = system.loadClass(name);
        if (clazz != null) {
    
    
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
    } catch (ClassNotFoundException e) {
    
    
        // Ignore
    }
 
    // (0.5) Permission to access this class when using a SecurityManager
    if (securityManager != null) {
    
    
        int i = name.lastIndexOf('.');
        if (i >= 0) {
    
    
            try {
    
    
                securityManager.checkPackageAccess(name.substring(0,i));
            } catch (SecurityException se) {
    
    
                String error = "Security Violation, attempt to use " +
                    "Restricted Class: " + name;
                log.info(error, se);
                throw new ClassNotFoundException(error, se);
            }
        }
    }
 
    //4 
    boolean delegateLoad = delegate || filter(name);
 
    // (1) Delegate to our parent if requested
    // 5 
    if (delegateLoad) {
    
    
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader1 " + parent);
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
    
    
            clazz = Class.forName(name, false, loader);
            if (clazz != null) {
    
    
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
    
    
            // Ignore
        }
    }
 
    // (2) Search local repositories
    if (log.isDebugEnabled())
        log.debug("  Searching local repositories");
    // 6 
    try {
    
    
        clazz = findClass(name);
        if (clazz != null) {
    
    
            if (log.isDebugEnabled())
                log.debug("  Loading class from local repository");
            if (resolve)
                resolveClass(clazz);
            return (clazz);
        }
    } catch (ClassNotFoundException e) {
    
    
        // Ignore
    }
 
    // (3) Delegate to parent unconditionally
    // 7
    if (!delegateLoad) {
    
    
        if (log.isDebugEnabled())
            log.debug("  Delegating to parent classloader at end: " + parent);
        ClassLoader loader = parent;
        if (loader == null)
            loader = system;
        try {
    
    
            clazz = Class.forName(name, false, loader);
            if (clazz != null) {
    
    
                if (log.isDebugEnabled())
                    log.debug("  Loading class from parent");
                if (resolve)
                    resolveClass(clazz);
                return (clazz);
            }
        } catch (ClassNotFoundException e) {
    
    
            // Ignore
        }
    }
 
    throw new ClassNotFoundException(name);
 
}

流程分析

  • 先从自己的缓存中查找,有则返回,无则继续
  • 再从parent的缓存中查找
  • 缓存中没有,则首先使用system类加载器来加载
  • 判断是否需要先让parent代理,
    • delegateLoad为true,先让parent加载,然后parent加载失败,自己加载
    • delegateLoad为false,直接自己加载。

猜你喜欢

转载自blog.csdn.net/qq_43478625/article/details/121064724