Java类加载机制&类加载器

189 阅读22分钟

1.JVM产生的背景

Java是一种高级语言,要让计算机执行你的Java程序,也得通过编译程序的编译。但是Java编译程序并不直接将Java源代码编译为相依于计算机平台的0、1序列,而是将其编译为字节码。

Java源代码的扩展名为.java,经过编译程序编译之后生成扩展名为.class的字节码。如果想要执行字节码文件,目标平台必须要安装JVM(java虚拟机),JVM会将字节码翻译为相依与平台的计算机指令,即:0、1序列。但是一定要记着:不同平台要安装专属该平台的JVM。原因就是不同的平台他们的机器码是不一样的。比如windos查看IP的指令是ipconfig,而centOS查看IP的指令是ifconfig

image.png

所以JVM担任的职责之一就是当地翻译员,将字节码文件翻译为当前平台能够识别的机器码,有了JVM,你的Java程序就达到了“编译一次到处运行”的跨平台目的。

通过上文分得出java程序跨平台性好的根本原因就是java虚拟机的存在。

2.Java代码执行流程图

image.png

2.1java编译器阶段

在编写好代码之后,我们需要将 .java文件编译成 .class文件,才能在虚拟机上正常运行代码。文件的编译通常是由JDK中自带的Javac工具完成,一个简单的 .java文件,我们可以通过javac命令来生成 .class文件。

下面我们通过javap反编译来看看一个class文件结构中主要包含了哪些信息:

image.png

前期编译的过程主要包括词法分析、填充符号表、注解处理、语义分析以及生成class文件,这个过程我们不用过多关注。只要从上图中知道,编译后的字节码文件主要包括常量池和方法表集合这两部分就可以了。

常量池主要记录的是类文件中出现的字面量以及符号引用。字面常量包括字符串常量(例如String str=“abc”,其中"abc"就是常量),声明为final的属性以及一些基本类型(例如,范围在-127-128之间的整型)的属性。符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用(例如String str=“abc”,其中str就是成员变量引用)等。

方法表集合中主要包含一些方法的字节码、方法访问权限(public、protect、prviate等)、方法名索引(与常量池中的方法引用对应)、描述符索引、JVM执行指令以及属性集合等。

3.Java虚拟机

JVM(Java Virtual Mechinal),Java虚拟机,是JRE的一部分。它是整个java实现跨平台的最核心的部分,负责解释执行字节码文件,是可运行java字节码文件的虚拟计算机。所有平台的上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机来解释执行。

3.1 Java虚拟机作用:

虚拟机拥有语言无关性,只要符合有效的字节码文件,就能被虚拟机所识别并装载运行。字节码文件可以来自许多地方,例如kotlin,scala等。

Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。

3.1.1 特点:

  1. 一次编译,到处运行
  2. 自动内存管理
  3. 自动垃圾回收功能

3.2 JVM 的架构模型

Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令集架构则是基于寄存器的指令集架构。

具体来说:这两种架构之间的区别:

基于栈式架构的特点

  • 设计和实现更简单,适用于资源受限的系统
  • 避开了寄存器的分配难题:使用零地址指令方式分配。
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现。
  • 不需要硬件支持,可移植性更好,更好实现跨平台基于寄存器架构的特点

基于寄存器架构的特点

  • 典型的应用是x86的二进制指令集:比如传统的PC以及Android 的 Davlik虚拟机。
  • 指令集架构则完全依赖硬件,可移植性差。
  • 性能优秀和执行更高效
  • 花费更少的指令去完成一项操作。
  • 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主。

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。栈的优点:跨平台,指令集小,编译器容易实现,缺点是性能比寄存器差一些。

通过上文学习可以得到,Jav程序运行在JVM之上,JVM主要包含的功能有

  1. 类加载器
  2. 执行引擎

4. 类加载子系统

通过加载Class文件,经过链接初始化等步骤使class文件内容解析成jvm能识别能运行的机器码格式

4.1 类加载器子系统作用

  1. 类加载子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识;
  2. ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine决定
  3. 加载的类信息存放于一块成为方法区的内存空间。除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)

image.png

4.2 类的生命周期

类的生命周期包括:加载链接初始化使用卸载,其中加载链接初始化,属于类加载的过程,也是本文的重点梳理的内容。使用是指我们new对象进行使用,卸载指对象被垃圾回收掉了。

image.png

4.2.1 加载阶段(Loading)

当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。在类加载后,class类文件中的常量池信息以及其它数据会被保存到JVM内存的方法区中

  • 通过类的全限定名(包名 + 类名),获取到该类的.class文件的二进制字节流
  • 将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构
  • 内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

总结:加载二进制数据到内存 —> 映射成jvm能识别的结构 —> 在内存中生成class文件

4.2.1.1 加载class文件的方式:

  1. 从本地系统中直接加载
  2. 通过网络获取,典型场景:Web Applet
  3. 从zip压缩包中读取,成为日后jar、war格式的基础
  4. 运行时计算生成,使用最多的是:动态代理技术
  5. 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
  6. 从加密文件中获取,典型的防Class文件被反编译的保护措施

image.png

  1. class file(Car.class)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个实例。
  2. class file加载到JVM中,被称元数据模板(Car Class),放在方法区。
  3. 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器 ClassLoader),扮演一个快递员的角色。

4.2.2 链接阶段(Linking)

链接分为三个子阶段:验证 -> 准备 -> 解析

4.2.2.1 验证

目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全

主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

文件格式验证:
  • 魔数检查:是否以魔数0xCAFEBABE开头。
  • 版本检查:主、次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
  • 长度检查
元数据验证:
  • 是否有父类
  • 是否继承了final修饰的类
  • 抽象方法是否有实现
字节码验证:

主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作:例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
  • 保证跳转指令不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换是有效的:例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。

(Halting Problem:通过程序去校验程序逻辑是无法做到绝对准确的——不能通过程序准确的检查出程序是否能在有限时间之内结束运行。)

符号引用验证:

符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:

  • 符号引用中通过字符串描述的全限定名是否能够找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

间接引用&直接引用

间接引用:指向运行时产量池的引用,符号引用,比如#32这个符号对应的是某个类的全限定名的字符串而已;

直接引用:类的内存地址,不再指向常量池


4.2.2.2 准备

  1. 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
  2. 这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显式初始化

注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1

public class HelloApp {
    private static int a = 1;//prepare:a = 0 ---> initial : a = 1


    public static void main(String[] args) {
        System.out.println(a);
    }
        class MyObject {
            static int num1 = 100;
            static int num2 = 100;
            static MyObject myObject = new MyObject();

            public MyObject() {
                num1 = 200;
                num2 = 200;
            }

            @Override
            public String toString() {
                return num1 + "\t" + num2;
            }
        }

        class MyObject2 {
            static int num1 = 100;      
            static MyObject2 myObject2 = new MyObject2(); 
            public MyObject2() {
                num1 = 200;
                num2 = 200;
            }

            static int num2 = 100;      
            @Override
            public String toString() {
                return num1 + "\t" + num2;
            }
        }

        public class ClassLoadingProcessStatic {

            public static void main(String[] args) {
                System.out.println(MyObject.myObject);          
                System.out.println(MyObject2.myObject2);      
            }

        }
  200	200
  200	100
第一个输出结果:

准备阶段:

num1:0
num2:0
myObject:null

初始化阶段:

num1:0赋值100
num2:0赋值100
myObject:null变成一个内存地址,同时执行构造方法(init()):num由100赋值为200; num2由100赋值为200
所以最终输出结果: 200 200
第二个输出结果:

准备阶段:

num1:0
myObject:null
num2:0

初始化阶段:

num1:0赋值100
myObject:null变成一个内存地址,同时执行构造方法:num1由100赋值200;num2由0赋值200 num2:200赋值100
所以最终输出结果: 200 100

第二个输出结果初始化阶段注意:

这里myObject和num2都是静态变量,初始化阶段语句要包括在clinit()方法,即

//伪代码
clinit(){
  static MyObject2 myObject2 = new MyObject2(); 
  static int num2 = 100;      
}

num2语句且在myObject实例化后面,所有实例化先调用构造函数,再运行static int num2 = 100; 按照顺序执行clinit()方法语句

4.2.2.3 解析

将符号引用转为直接引用的过程。我们知道,在编译时,Java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为JVM可以直接获取的内存地址或指针,即直接引用。

  1. 将常量池内的符号引用转换为直接引用的过程
  2. 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
  3. 符号引用就是一组符号来描述所引用的目标。 符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  4. 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

4.3初始化阶段(Initialization)

4.3.1 类的初始化时机

  1. 创建类的实例
  2. 访问某个类或接口的静态变量,或者对该静态变量赋值
  3. 调用类的静态方法
  4. 反射(比如:Class.forName(“全限定名(包名 + 类名)”))
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化,即不会执行初始化阶段(不会调用 clinit() 方法和 init() 方法)

clinit() 方法和 init()方法的区别

  • init is the (or one of the) constructor(s) for the instance, and non-static field initialization.
  • clinit are the static initialization blocks for the class, and static field initialization.

翻译过来就是说 init()是instance实例构造器,对非静态变量解析初始化, 而clinit()是class类构造器对静态变量,静态代码块进行初始化。

4.3.2 clinit()

  1. 初始化阶段就是执行类构造器方法<clinit>()的过程
  2. 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法
  3. <clinit>()方法中的指令按语句在源文件中出现的顺序执行
  4. <clinit>()不同于类的构造器。(构造器是虚拟机视角下的<init>()
  5. 若该类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()已经执行完毕
  6. 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁
package com.jvm;

/**
 * @author yatao.xu
 * @version 1.0.0
 * @date 2022-07-19
 **/
public class StaticDemo {

    private static int number1 = 100;

    static {
        number1 = 1000;
        number2 = 2000;
        System.out.println(number1);
        // System.out.println(number2); //Illegal forward reference
    }

    /**
     * 1、linking之prepare: number2 = 0 --> initial: 2000 --> 200
     * 2、这里因为静态代码块出现在声明变量语句前面,所以之前被准备阶段为0的number变量会
     * 首先被初始化为2000,再接着被初始化成200
     *
     */
    private static int number2 = 200;


}

image.png 这里因为静态代码块出现在声明变量语句前面,所以之前被准备阶段为0的number变量会首先被初始化为2000,再接着被初始化成200.

虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁

public class DeadThreadTest {
    public static void main(String[] args) {
        Runnable r = () -> {
            System.out.println(Thread.currentThread().getName() + "开始");
            DeadThread dead = new DeadThread();
            System.out.println(Thread.currentThread().getName() + "结束");
        };

        Thread t1 = new Thread(r,"线程1");
        Thread t2 = new Thread(r,"线程2");

        t1.start();
        t2.start();
    }
}

class DeadThread{
    static{
        if(true){
            System.out.println(Thread.currentThread().getName() + "初始化当前类");
            while(true){

            }
        }
    }
}

输出结果:

线程2开始
线程1开始
线程2初始化当前类

然后程序卡死了

程序卡死,分析原因:

  • 两个线程同时去加载 DeadThread 类,而 DeadThread 类中静态代码块中有一处死循环
  • 先加载 DeadThread 类的线程抢到了同步锁,然后在类的静态代码块中执行死循环,而另一个线程在等待同步锁的释放
  • 所以无论哪个线程先执行 DeadThread 类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)

类的实例化与类的初始化是两个完全不同的概念:

  • 类的实例化是指创建一个类的实例(对象)的过程;
  • 类的初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期中的一个阶段。

5. 类加载器

java自带的类加载器有三种:bootstrap classLoader,extension classLoader,App classLoader。

启动类加载器(引导类加载器)-BootstrapClassLoader

  1. 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  2. 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  3. 并不继承自java.lang.ClassLoader,没有父加载器
  4. 在这个过程中会通过启动类加载器,来加载sun.launcher.LauncherHelper,并执行checkAndLoadMain,以及加载main函数所在的类,并启动扩展类加载器、应用类加载器,并作为他们的父类加载器
  5. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  6. 我们不能获取到启动类加载器,返回为null

扩展类加载器-ExtClassLoader

  1. Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  2. 加载扩展类库(jre/lib/ext包)如果用户创建的jar包放在此目录,也会加载;或加载java.ext.dirs系统属性所指定的目录
  3. 派生于ClassLoader,父类加载器为启动类加载器

应用类加载器(系统类加载器)-AppClassLoader

  1. Java语言编写,由sun.misc.LaunchersAppClassLoader实现
  2. 父类加载器为扩展类加载器
  3. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  4. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  5. 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器
package com.classLoader;

/**
 * @author yatao.xu
 * @version 1.0.0
 * @date 2022-07-19
 **/
public class ClassLoaderTest {

    public static void main(String[] args) {
        //获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //获取其上层:扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@4dc63996

        //获取其上层:获取不到引导类加载器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //对于用户自定义类来说:默认使用系统类加载器进行加载
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null
    }
}

5.2 ClassLoader源码

A class loader is an object that is responsible for loading classes. The class <tt>ClassLoader</tt> is an abstract class.  Given the <a href="#name">binary name</a> of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class.  A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.

public abstract class ClassLoader {
    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;

大致意思如下:

class loader 是一个负责加载 classes 的对象,ClassLoader 类是一个抽象类,需要给出类的二进制名称,class loader 尝试定位或者产生一个 class 的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。

以下是 ClassLoader 常用到的几个方法及其重载方法:

  • defineClass(byte[], int, int) 把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实
    例。这个方法被声明为final的
  • findClass(String name) 查找名称为 name的类,返回的结果是java.lang.Class类的实例
  • loadClass(String name) 加载名称为 name的类,返回的结果是java.lang.Class类的实例
  • resolveClass(Class<?>) 链接指定的 Java 类
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,检测是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父加载器不为空则调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则调用Bootstrap Classloader
                        //这也解释了ExtClassLoader的parent为null,但仍然说Bootstrap ClassLoader是它的父加载器。 
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加载器没有找到,则调用findclass
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //调用resolveClass(),生成最终的Class对象。
                resolveClass(c);
            }
            return c;
        }
    }

该方法大概意思:

使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用findLoadedClass(String) 方法检查这个类是否被加载过 使用父加载器调用 loadClass(String) 方法,如果父加载器为 Null,类加载器装载虚拟机内置的加载器调用 findClass(String) 方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的 resolve 参数的值为 true,那么就调用resolveClass(Class) 方法来处理类。 ClassLoader 的子类最好覆盖 findClass(String) 而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)

该段也解释了双亲委托机制。

自定义classLoader建议:覆盖findClass()方法,而不要直接改写loadClass()方法。

5.3 用户自定义类加载器

什么时候需要自定义类加载器?

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

  1. 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
  2. 修改类加载的方式
  3. 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
  4. 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            byte[] result = getClassFromCustomPath(name);
            if (result == null) {
                throw new FileNotFoundException();
            } else {
                //defineClass和findClass搭配使用
                return defineClass(name, result, 0, result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }
    
	//自定义流的获取方式
    private byte[] getClassFromCustomPath(String name) {
        //从自定义路径中加载指定类:细节略
        //如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
        return null;
    }

    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("One", true, customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. 双亲委派机制

双亲委派机制原理

image.png

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

  1. 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  2. 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  3. 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
  4. 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常