天天看点

《深入理解Java虚拟机》学习笔记之类加载机制

一、概述

  • 定义:
    • 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制
  • 过程:
    • 在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的
      • 优点:高度的灵活性。Java中可以动态扩展的语言特性就是依赖 运行期间动态加载和动态连接 这个特点实现的
      • 缺点:类加载时的性能开销。

二、类加载的时机

1、生命周期
  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括7个阶段
    《深入理解Java虚拟机》学习笔记之类加载机制
    • 这些阶段的“开始”时机是按照这个顺序
      • 这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段
    • 加载 -> 验证 -> 准备 -> 初始化 -> 卸载

      这五个阶段的顺序是确定的
    • “解析”阶段的开始时机是不确定的,在某些情况下可以在初始化阶段之后再开始
      • 这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
2、初始化的时机
  • 可通过静态代码块检查类是否加载
    static {
        ...
    }
               
  • 虚拟机开始类加载的时机规范中没有强制约束,但在以下有且只有5种情况下如果类没有进行初始化,则必须立刻对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)。
    • (1)遇到

      new

      getstatic

      putstatic

      invokestatic

      这四条字节码指令时。生成这四条指令的常见Java代码场景是:
      • 1⃣使用

        new

        关键字实例化对象的时候
      • 2⃣读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候
      • 3⃣调用一个类的静态方法的时候
    • (2)使用

      java.lang.reflect

      包的方法对类进行反射调用的时候
    • (3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
      • 接口:当一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化
    • (4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)、虚拟机会先初始化这个主类
    • (5)当使用JDK 1.7的动态语言支持时,如果一个

      java.lang.invoke.MethodHandle

      实例最后的解析结果是

      REF_getStatic

      REF_putStatic

      REF_invokeStatic

      的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
  • 主动引用与被动引用
    • (1)主动引用:上述五种场景中的行为称为对一个类的主动引用
    • (2)被动引用:除上述情况外,所有引用类的方式都不会触发初始化,称为被动引用
    • 被动引用的例子:
      • 1⃣通过子类引用父类的静态字段,不会导致子类初始化
      • 2⃣通过数组定义来引用类,不会触发此类的初始化
        //不会触发SuperClass类的初始化
        //因为创建动作是由字节码指令 newarray 触发
        
        SuperClass[] sca = new SuperClass[10];    
                   
      • 3⃣常量在编译阶段会存入调用类的常量池(常量传播优化)中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

三、类加载的过程(7个阶段)

1、加载
  • 加载阶段需要完成的三件事
    • (1)通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等)
    • (2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • (3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各种数据的访问入口
  • 非数组类与数组类的加载(加载阶段中获取类的二进制字节流的动作)
    • (1)非数组类
      • 可以使用系统提供的引导类加载器来完成
      • 可以使用开发人员自定义的类加载器去控制字节流的获取方式(即重写一个类加载器的

        loadClass()

        方法)
    • (2)数组类
      • 本身不通过类加载器创建,而是由Java虚拟机直接创建的
      • 但数组类的元素类型最终还是要靠类加载器去创建
  • 数组类创建过程需遵循的规则
    • (1)如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就采用普通的加载过程去加载这个组件类型,数组类将在加载该组件类型的类加载器的类名称空间上被标识(一个类必须与类加载器一起确定唯一性)
    • (2)如果数组的组件类型不是引用类型(eg:int[] 数组),Java虚拟机将会把原数组标记为与引导类加载器关联
    • (3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public
  • 结果
    • 加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中
      • 方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范未规定此区域的具体数据结构
    • 然后在内存中实例化一个

      java.lang.Class

      类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口
      • 并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区中
2、验证
  • 目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
    • 是虚拟机对自身保护的一项重要工作
    • 本阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击
    • 本阶段很重要,但不必需(对程序运行期没有影响)
  • 执行性能:验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分
  • 验证不通过:如果验证到输入的字节流不符合Class文件格式的约束,虚拟机就应抛出一个

    java.lang.VerifyError

    异常或其子类异常
  • 过程(四个阶段):
    • (1)文件格式验证(检查字节流)
      • 目的:验证字节流是否符合Class文件格式的规范(保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求),并能被当前版本的虚拟机处理
      • 内容:是否以魔数

        0xCAFEBABE

        开头、主次版本号是否在当前虚拟机处理范围之内、常量池的常量中是否有不被支持的常量类型…
      • 特点:本阶段的验证是基于二进制字节流进行的,通过这个阶段的验证后,字节流才会进入内存的方法区中进行存储。所以后面的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流
    • (2)元数据验证(检查数据类型)
      • 目的:对字节码描述的信息进行语义分析(语义校验),以保证其描述的信息符合Java语言规范的要求
      • 内容:这个类是否有父类、这个类的父类是否继承了不允许被继承的类(被final修饰的类)、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法…
    • (3)字节码验证(检查类的方法体)
      • 目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
      • 内容:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令上、保证方法体中的类型转换是有效的…
      • 将字节验证的类型推导转变为类型检查:为避免过多的时间消耗在字节码验证阶段,给方法体的Code属性的属性表中增加了一项名为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块(Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可
    • (4)符号引用验证(检查常量池中的信息)
      • 发生时机:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生
      • 目的:对类自身以外(常量池中的各种符号引用)的信息进行匹配性检验,确保解析动作能正常执行
      • 内容:符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定的类中是否存在符合方法访问的字段描述父以及简单名称所描述的方法和字段、在符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
      • 结果:如果无法通过符号引用验证
3、准备
  • 作用:正式为类变量分配内存并设置类变量初始值的阶段
    • 这些变量所使用的内存都将在方法区中进行分配
    • 本阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量。实例变量将会在对象实例化时随着对象一起分配在Java堆中
    • 本阶段将会对类变量第一次赋初值
      • 这里的初值“通常情况”下是数据类型的零值
        //此阶段value被赋的初值是默认的int零值,0
        
        public static int value = 123;
                   
      • 但如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量就会被初始化为ConstantValue属性所指定的值
        //被final修饰的类变量含有ConstantValue属性,因此此阶段就已经被赋予了值123
        
        public final static int value = 123;
                   
4、解析
  • 作用:虚拟机将常量池内的符号引用替换为直接引用的过程
    类别 定义 指向目标与内存情况
    符号引用 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可 符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
    直接引用 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄 直接引用是和虚拟机实现的内存布局相关的,如果有了直接引用,那引用的目标必定已经在内存中存在
  • 发生时机:规范中并未规定解析阶段发生的具体时间,只要求在执行以下16个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析
    • anewarray

    • checkcast

    • getfield

    • getstatic

    • instanceof

    • invokedynamic

    • invokeinterface

    • invokespecial

    • invokestatic

    • invokevirtual

    • ldc

    • ldc_w

    • multianewarray

    • new

    • putfield

    • putstatic

  • 对同一个符号引用进行多次解析
    • (1)除

      invokedynamic

      指令以外的指令(静态的):虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行
    • (2)对于

      invokedynamic

      指令(动态的):之前解析的结果不一定对后续相同的

      invokedynamic

      指令生效,此指令目的就在于动态语言支持,必须等到程序实际运行到这条指令的时候,解析动作才能进行。
  • Java是一门强类型、静态类型语言
    • 静态与动态:Static typing when possible, dynamic typing when needed。
    类型 特点 例子
    静态类型 数据类型是在编译其间检查的。在写程序时要声明所有变量的数据类型 python、ruby、脚本语言
    动态类型 在运行期间才去做数据类型检查。在用动态类型的语言编程时,永远也不用给任何变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来 C#、Java
    • 强类型与弱类型:
    类型 特点 优缺点
    强类型 强制数据类型定义的语言。一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了 编译速度稍差,类型安全
    弱类型 数据类型可以被忽略的语言。一个变量可以赋不同数据类型的值 编译速度较快,类型不安全
  • 解析的对象(七类符号引用):
    对象 常量池中的常量类型
    类或接口 CONSTANT_Class_info
    字段 CONSTANT_Fieldref_info
    类方法 CONTANT_Methodref_info
    接口方法 CONTANT_InterfaceMethordref_info
    方法类型 CONSTANT_MethodType_info
    方法句柄 CONSTANT_MethodHandle_info
    调用点限定符 CONSTANT_InvokeDynamic_info
    • (1)类或接口的解析(最后需进行权限验证):
      • 假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,需要如下三个步骤
      • 1⃣ 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C
      • 2⃣ 如果C是一个数组类型,并且数组的元素类型是对象,则会按照1中的规则去加载数组元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象
      • 3⃣ 如果1⃣2⃣没有出现任何异常,则C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前需要进行符号引用验证,确认D是否具备对C的访问权限,如果发现不具备访问权限,将抛出

        java.lang.IllegalAccessError

    • (2)字段解析(最后需进行权限验证)
      • 要解析一个未被解析过的字段符号引用,首先会对字段表内的

        class_index

        项中索引的

        CONSTANT_Class_info

        符号引用进行解析,也就是字段所属的类或接口的符号引用。
      • 如果解析成功,将这个字段所属的类或接口用C表示,需要如下步骤对C的后续字段进行搜索
      • 1⃣ 查自身:如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段。则返回这个字段的直接引用,查找结束。
      • 2⃣ 查接口实现:否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接饮用,查找结束。
      • 3⃣ 查父类继承:否则,如果C不是

        java.lang.Object

        的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
      • 4⃣ 否则,查找失败,抛出

        java.lang.NoSuchFieldError

        异常
    • (3)类方法解析(最后需进行权限验证)
      • 要解析一个未被解析过的类方法,首先会对类方法表内的

        class_index

        项中索引的

        CONSTANT_Class_info

        符号引用进行解析,也就是类方法所属的类或接口的符号引用。
      • 如果解析成功,使用C表示这个类方法所属的类,按如下步骤进行后续类方法搜索
      • 1⃣ 查是否是接口:类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法中发现

        class_index

        中索引的C是个接口,那就直接抛出

        java.lang.IncompatibleClassChangeError

        异常
      • 2⃣ 查自身:如果确认该C为一个类,则在类C中查找是否有简单名称和描述符斗鱼目标相匹配的方法,如果有则返回这个方法的直接引用
      • 3⃣ 查父类:否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
      • 4⃣ 查是否为抽象类:否则,在类C实现的接口列表及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,查找结束,抛出

        java.lang.AbstractMethodError

        异常
      • 5⃣ 否则,宣告方法查找失败,抛出

        java.lang.IllegalAccessError

    • (4)接口方法解析(无需权限验证,接口方法默认public)
      • 要解析一个未被解析过的接口方法,首先会对接口方法表内的

        class_index

        项中索引的

        CONSTANT_Class_info

        符号引用进行解析,也就是接口方法所属的类或接口的符号引用。
      • 如果解析成功,使用C表示这个接口方法所属的接口,按如下步骤进行后续接口方法搜索
      • 1⃣ 检查是否是类:如果在接口方法表中发现

        class_index

        中的索引C是个类而不是接口,那就直接抛出

        java.lang.IncompatibleClassChangeError

      • 2⃣ 查自身:否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
      • 3⃣ 查父接口:否则,在接口C的父接口中递归查找,直到

        java.lang.Object

        (查找范围包括Object类)类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
      • 4⃣ 否则,宣告方法查找失败,抛出

        java.lang.NoSuchMethodError

5、初始化
  • 作用:开始真正执行类中定义的Java程序代码(或者说字节码)
    • 在准备阶段,变量已赋过一次系统要求的初值
    • 初始化阶段,是根据程序员通过程序指定的主观计划去初始化类变量和其他资源
      • 初始化阶段是执行类构造器

        <clinit>()

        方法的过程
  • <clinit>()

    方法执行过程的特点:
    • (1)

      <clinit>()

      方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(

      static{}

      块)中的语句合并产生的。
      • 编译器收集的顺序是由语句在源文件中出现的顺序所决定的
      • 静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问
      public class Test {
          static {
              i = 0;                        //给变量赋值可以正常编译通过
              System.out.print(i);          //这句编译器会提示“非法的向前引用”
          }
          static int i = 1;
      }
                 
    • (2)

      <clinit>()

      方法与类的构造函数(或者说实例构造器

      <init>()

      方法)不同,它不需要显式的调用父类构造器(super),虚拟机会保证在子类的

      <clinit>()

      方法执行之前,父类的

      <clinit>()

      方法已经执行完毕
      • 由于父类的

        <clinit>()

        方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
    • (3)

      <clinit>()

      方法对于类或接口来说并不是必需的。
      • 如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成

        <clinit>()

        方法
    • (4)接口与类一样都会生成

      <clinit>()

      方法。
      • 原因:接口中不能使用静态语句块,但仍然有变量初始化的赋值操作
      • 接口与类的区别1⃣ :执行接口的

        <clinit>()

        方法不需要先执行父接口的

        <clinit>()

        方法。只有当父接口中定义的变量使用时,父接口才会初始化。
      • 接口与类的区别2⃣ :接口的实现类在初始化时也一样不会执行接口的

        <clinit>()

        方法
    • (5)虚拟机会保证一个类的

      <clinit>()

      方法在多线程环境中被正确的加锁、同步
      • 如果有多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的

        <clinit>()

        方法,其他线程都需要阻塞等待,直到活动线程执行

        <clinit>()

        方法完毕
      • 如果

        <clinit>()

        方法中有耗时很长的操作,可能造成多个进程阻塞。
      • 如果存在线程阻塞的情况,当执行

        <clinit>()

        方法的那条线程退出

        <clinit>()

        方法后,其他线程唤醒之后不会再次进入

        <clinit>()

        方法。因为同一个类加载器下,一个类型只会初始化一次。

四、类加载器

  • 定义:在类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
1、类与类加载器
  • 判定类是否“相等”
    • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
    • “相等”:代表类的Class对象的

      equals()

      方法、

      isAssignableFrom()

      方法、

      isInstance()

      方法的返回结果,也包括使用

      instanceof

      关键字做对象所属关系判定等情况
    • 比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
  • 从Java虚拟机的角度,类加载器有2种
    • (1)启动类加载器(Bootstrap ClassLoader)
      • 使用C++语言实现,是虚拟机自身的一部分
    • (2)所有其他的类加载器
      • 由Java语言实现,独立于虚拟机外部
      • 都继承自抽象类

        java.lang.ClassLoader

  • 从Java开发人员角度,类加载器有3种
    • (1)启动类加载器(Bootstrap ClassLoader)
      • 使用方式:无法被Java程序直接引用。用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
        public ClassLoader getClassLoader() {
            ClassLoader cl = getClassLoader();
            if (cl == null) {
                return null;
            }
        }
                   
      • 作用:负责将存放在

        <JAVA_HOME>\lib

        目录中的、并且是被虚拟机识别的(仅按照文件名识别)类库加载到虚拟机内存中
    • (2)扩展类加载器(Extension ClassLoader)
      • 使用方式:开发者可直接使用
      • 作用:由

        sun.misc.Launcher$ExtClassLoader

        实现,负责加载

        <JAVA_HOME>\lib\ext

        目录中的,或者被

        java.ext.dirs

        系统变量所指定的路径中的所有类库
    • (3)应用程序类加载器(Application ClassLoader)
      • 别名:系统类加载器
      • 使用方式:开发者可以直接使用。如果引用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
      • 作用:由

        sun.misc.Launcher$AppClassLoader

        实现。是ClassLoader中的

        getSystemClassLoader()

        方法的返回值,负责加载用户路径(ClassPath)上所指定的类库
2、双亲委派模型
《深入理解Java虚拟机》学习笔记之类加载机制
  • 要求:
    • 类加载器存在层次关系
    • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器
  • 工作过程:
    • 如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成
    • 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中
    • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
  • 好处:
    • Java类随着它的类加载器一起具备了一种带有优先级的层次关系
      • 实现了基类能总是被同一个类加载器加载,确保唯一性
    • eg:类

      java.lang.Object

      ,它存放在

      rt.jar

      之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
  • 逻辑:
    • 位置:实现双亲委派的代码集中在

      java.lang.ClassLoader

      loadClass()

      方法中
    • (1)检查是否被加载过
    • (2)若没有加载则调用父加载器的

      loadClass()

      • 若父加载器为空,则默认使用启动类加载器作为父加载器
    • (3)如果父类加载失败,抛出

      ClassNotFoundException

      异常后,再调用自己的

      finaClass()

      方法进行加载
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //调用父类的类加载器
                        c = parent.loadClass(name, false);
                    } else {
                        //父类加载器为空,调用启动类加载器
                        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(c);
            }
            return c;
        }
    }
               
  • 沙箱机制
    • 沙箱机制是由基于双亲委派机制上 采取的一种JVM的自我保护机制
    • 假设你要写一个

      java.lang.String

      的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.
3、破坏双亲委派模型
  • 第一次被破坏
    • 原因:为了向前兼容
      • 双亲委派模型是JDK1.2之后才被引入,而ClassLoader在JDK1.0就已存在
    • JDK1.2后,不提倡用户覆盖

      loadClass()

      方法,而应当把自己的类加载逻辑写到

      findClass()

      方法中
      • loadClass()

        方法的逻辑例如果父类加载失败,则会调用自己的

        findClass()

        方法,从而确保新写的类加载器是符合双亲委派规则的。
  • 第二次被破坏
    • 原因:双亲委派模型自身的缺陷
    • 问题:基础类要回调用户的代码(eg:JNDI服务对资源进行集中管理和查找)
    • 解决:引入线程上下文类加载器(Thread Context ClassLoader)
      • 违背了双亲委派机制,使父类加载器请求子类加载器去完成类加载
      • 这个类加载器可以通过

        java.lang.Thread

        类的

        setContextClassLoader()

        方法进行设置
      • 如果创建线程时还未设置,它将会从父线程中继承一个
      • 如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
  • 第三次被破坏
    • 原因:用户对程序动态性的追求
      • 动态性:代码热替换(HotSwap)、模块热部署(Hot Deployment)
    • 目前OSGi成为业界事实上的Java模块化标准
      • OSGi模块化热部署实现关键 -> 自定义的类加载器机制的实现:当需要更换一个Bundle时,就把Bundle连同类加载器一同换掉以实现代码的热替换
    • 在OSGi环境下,类加载器不再是双进委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。当收到类加载请求时,OSGi将按照下面的顺序进行类搜索(1⃣2⃣:双亲委派,其它:平级类加载器中的类查找)
      • 1⃣ 将以java.*开头的类委派给父类加载器加载
      • 2⃣ 否则,将委派列表名单内的类委派给父类加载器加载
      • 3⃣ 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
      • 4⃣ 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
      • 5⃣ 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
      • 6⃣ 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
      • 7⃣ 否则,类查找失败

继续阅读