JVM——类与字节码加载篇

字节码文件的跨平台性

在这里插入图片描述

Java的前端编译器

  • Java源代码的编译结果是字节码,那么肯定需要有一种编译器能够将Java源码编译为字节码,承担这个重要责任的就是配置在path环境变量中的javac编译器。javac是一种能够将Java源码编译为字节码的前端编译器
  • Javac编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤,分别是词法解析、语法解析、语义解析以及生成字节码。
在这里插入图片描述

⭐⭐BAT面试题

①类文件结构有几个部分?

  • 概括分

    • 魔数
    • Class 文件版本
    • 常量池
    • 访问标志
    • 类索引、父类索引、接口索引集合
    • 字段表集合
    • 方法表集合
    • 属性表集合
  • 细分(记住概括分就行):

img

② 知道字节码吗?字节码都有哪些?Integer x=5;int y=5;比较x==y都经过哪些步骤?

  • 字节码由多个指令组合而成,一个指令由操作码+操作数组成,但大多数的指令都不包含操作数,只有一个操作码

    JVM中的字节码指令集按用途大致分成9类。

    • 加载与存储指令
    • 算术指令
    • 类型转换指令
    • 对象的创建与访问指令
    • 方法调用与返回指令
    • 操作数栈管理指令
    • 比较控制指令
    • 异常处理指令·
    • 同步控制指令

    在这里插入图片描述

  • 字节码角度讲int y=5单纯压入int类型变量,赋值为5,而Integer x=5则会调用Integer.valueOf()的方法。 x==y会使用到if_icmp指令比较。

③ ++i 和 i++区别?

  • 如果一行代码仅仅是++i 或 i++,他们的字节码指令完全一样,没有任何区别。

    int i=10;
    i++;
    
    int i=10;
    ++i;
    

    这两段代码的字节码指令完全一样。

  • 如果是赋值/运算,则++i是先自增,再运算;i++是先运算,再自增。

    int i=10;
    int j = i++; //j=11;
    

⭐透过字节码指令查看代码细节

为什么new 一个对象,操作数栈中入栈了两个对象的引用?

上面那个引用是为了让对象调用方法进行对象的初始化,初始化完成后出栈,

下面那个引用就存入局部变量表中,所以操作数栈中入栈了两个对象的引用。

在这里插入图片描述

异常及异常的处理

  • 过程一:异常对象的生成过程–>throw(手动/自动)–>指令:athrow
  • 过程二:异常的处理:抓抛模型。try-catch-finally -->使用异常表

处理异常

  • 在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现的(早期使用jsr、ret指令),而是采用异常表来完成的。

异常表

  • 如果一个方法定义了一个try-catch或者try-finally的异常处理,就会创建一个异常表。它包含了每个异常处理或者finally块的信息。异常表保存了每个异常处理信息。比如:
  • 起始位置·结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引
  • 当一个异常被抛出时,JVM会在当前的方法里寻找一个匹配的处理,如果没有找到,这个方法会强制结束并弹出当前栈帧,并且异常会重新抛给上层调用的方法(在调用方法栈帧)。如果在所有栈帧弹出前仍然没有找到合适的异常处理,这个线程将终止。如果这个异常在最后一个非守护线程里抛出,将会导致JVM自己终止,比如这个线程是个main线程。
  • **不管什么时候抛出异常,如果异常处理最终匹配了所有异常类型,代码就会继续执行。**在这种情况下,如果方法结束后没有抛出异常,仍然执行finally块,在return前,它直接跳到finally块来完成目标

类的加载过程(类的声明周期)

引言

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

  • 按照Java虚拟机规范,从class文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括如下7个阶段:
    在这里插入图片描述

  • 从程序中类的使用过程看:
    在这里插入图片描述

过程一:加载

加载的理解

  • 所谓加载,简而言之就是将Java类的字节码文件加载到机器内存中并在内存中构建出Java类的原型——类模板对象(就是方法区中的类的元数据)。所谓类模板对象,其实就是Java类在]VM内存中的一个快照,JVM将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样]VM在运行期便能通过类模板而获取Java类中的任意信息,能够对Java类的成员变量进行遍历,也能进行Java方法的调用。
  • 反射的机制即基于这一基础。如果JVM没有将Java类的声明信息存储起来,则JVM在运行期也无法反射。

加载完成的操作:

  • 加载阶段,简言之,查找并加载类的二进制数据,生成Class的实例。
  • 在加载类时,Java虚拟机必须完成以下3件事情:
    • 通过类的全名,获取类的二进制数据流
    • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
    • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

过程二:链接

环节1:验证

  • 作用:保证加载的字节码是合法、合理并符合规范的。
    • 格式检查
    • 语义检查
    • 字节码验证
    • 符号引用验证

环节2:准备

  • 作用:为类的静态变量分配内存,并将其初始化为默认值。

  • 在这里插入图片描述
  • 注意

    1. 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就会分配了,准备阶段会显式赋值。

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

    3. 这个阶段并不会像初始化阶段中那样会有初始化或者代码被执行

    4. 基本数帮类型:

      • 非Final修饰的变量,在准备环节进行默认初始化时值。
      • Final修饰以后,在准备环节直接进行显示慰值。

      引用类型String:

      • 如果使用字面量的方式定义一个字符的常量的话,也是在解析环节直接进行显示财值。

环节3:解析

  • 作用:将类、接口、字段和方法的符号引用转为直接引用。

具体描述:

符号引用就是一些字面量的引用,和虚拟机的内部数据结构和和内存布局无关。比较容易理解的就是在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下println()方法被调用时,系统需要明确知道该方法的位置。

以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。

小结:

  • 所调解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但只存在符号引用,不能确定系统中一定存在该结构。
  • 不过Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行。在HotSpot VM中,加载、验证、准备和初始化会按照顺序有条不紊地执行,但链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行

过程三:初始化

作用:为类的静态变量赋予正确的初始值。

具体描述:

  • 类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行Java字节码。(即:到了初始化阶段,才真正开始执行类中定义的Java程序代码。
  • 初始化阶段的重要工作是执行类的初始化方法:()方法来对类的静态变量初始值。

关于static + final:

/**
 * 说明:使用static+ final修饰的字段的显式赋值的操作,到底是在哪个阶段进行的赋值?
 * 情况1:在链接阶段的准备环节赋值
 * 情况2:在初始化阶段<cLinit>()中赋值
 * 
 * 结论:
 * 在链接阶段的准备环节赋值的情况:
 * 1.对于基本数据类型的字段来说,如果使用static final修饰,则显式赋值(直接赋值常量,而非调用方法)通常是在链接阶段的准备环节进行
 * 2.对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显式赋值通常是在链接阶段的准备环节进行
 * 
 * 在初始化阶段<cLinit>()中赋值的情况:
 * 排除上述的在准备环节赋值的情况之外的情况。
 * 
 * 最终结论:使用static+final修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类到或String类型的显式财值,是在链接阶段的准备环节进行。
 */
public class InitializationTest2 {

    public static int a = 1; //在初始化阶段<clinit>()中赋值

    public static final int INT_CONSTANT = 10;     //在链接阶段的准备环节赋值

    public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);     // 在初始化阶段<clinit>()中赋值

    public static Integer INTEGER_CONSTANT2 = Integer.valueOf(100);     // 在初始化阶段<clinit>()中概值

    public static final String se = "helloworlde";     // 在链接阶段的准备环节赋值

    public static final String s1 = new String("helloworld1");     // 在初始化阶段<clinit>()中赋值

    public static final int NUM1 = new Random().nextInt(10);//在初始化阶段clinit>()中赋值
}

所有非final的static都是在初始化()显示赋值
static+final 不涉及符号引用(链接阶段的解析环节),就直接在
链接
阶段的准备环节显示赋值,涉及符号引用(对象),则在**初始化阶段()**中赋值。

()的线程安全性:()是带锁线程安全的。

类的主动使用与被动使用:

  • Java程序对类的使用分为两种:主动使用和被动使用。

  • 主动使用才会调用(初始化),被动使用不会引起类的初始化

  • 主动使用例子:比如创建对象实例(new、反射、克隆、反序列化方式)、调用类的静态方法,jvm会调用

  • 被动使用例子:直接调用类的静态属性。

过程四:类的使用

  • 经历过完整的加载、链接和初始化3个类加载步骤后,就可以使用了
  • 开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。

过程五:类的卸载(回收)

当同时满足以下三个条件时,类可以被卸载(回收)

  1. 该类所有的实例已经被回收

  2. 加载该类的ClassLoder对象已经被回收(指的是用自己设计的MyClassLoder

  3. 该类对应的java.lang.Class对象没有任何对方被引用

满足这三个条件仅仅是这个类“被允许”卸载(回收),当方法区发生GC才会被卸载(回收)。

案例

Class clazz=loader.loadClass(“Sample”);

Object obj=clazz.newInstance();

d7899936d5e6ff31ea1bfed0eeb4cc5b.png ![在这里插入图片描述](https://img-blog.csdnimg.cn/96c10ab374934cd28f4d81f7785b102f.png)

loader1变量和obj变量间接引用代表Sample类的Class对象,而objClass变量则直接引用它。

如果在程序运行过程中,将上图左侧三个引用变量都置为null,此时Sample对象结束生命周期,MyClassLoader对象结束生命周期,代表Sample类的Class对象也结束生命周期,Sample类在方法区内的二进制数据被卸载。

当再次有需要时,会检查Sample类的Class对象是否存在,如果存在则会直接使用,不在重新加载,如果不存在,则Sample类会被重新加载,在Java虚拟机的堆区会重新生成一个代表Sample类的Class实例。

类的生命周期总结

  • 当Sample类被加载、链接和初始化后,它的生命周期就开始了。当代表Sample类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,Sample类在方法区内的数据也会被卸载,从而结束Sample类的生命周期。
  • 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

⭐⭐ 大厂面试题

蚂蚁金服:
描述一下JvM加载Class文件的原理机制?
一面:类加载过程
百度:
类加载的时机
java类加载过程?
简述java类加载机制?
腾讯:
JVM中类加载机制,类加载过程?
滴滴:
JVM类加载机制
美团:
Java类加载过程
描述一下jvm加载c1ass文件的原理机制
京东:
什么是类的加载?|
哪些情况会触发类的加载?
讲一下JVM加载一个类的过程JVM的类加载机制是什么?

再谈类的加载器

  • ClassLoader的作用:ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。

类加载的分类

  • 类的加载分类:显式加载 vs隐式加载
  • class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。
    • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或this.getClass().getClassLoader().loadClass()加载class对象。
    • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象(就是一个类中new另一个类对应的对象实例),此时额外引用的类将通过JVM自动加载到内存中。
  • 在日常开发以上两种方式一般会混合使用。
  • 在这里插入图片描述

类的加载器的分类

    • 1、引导类加载器:BootStrap,它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader
    • 2、扩展类加载器:ExtClassLoader,它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
    • 3、应用类加载器(也叫“系统类加载器”):AppClassLoader,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的
      可以通过类名.class.getClassLoader();获取类加载器
  • JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。

  • 扩展类加载器,应用程序类加载器(也叫“系统类加载器”),用户自定义器都属于自定义类加载器

  • 只有引导类加载器(Bootstrap ClassLoader)是C/C++语言编写,就是JVM自带的,其他加载器都是java语言编写的,只不过扩展类加载器和应用程序类加载器是由jdk开发人员编写的。

  • 在这里插入图片描述

  • 除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加戟器。

  • 不同类加载器看似是继承(Inheritance)关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用

① 引导类加载器

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

② 扩展类加载器

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现

  • 继承于ClassLoader类

  • 父类加载器为启动类加载器

  • 加载 Java 的扩展库(从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库,或自己放到ext目录下的jar包)。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

③ 系统类加载器

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

④ 用户自定义类加载器

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

⑤ 说明

  • 站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载器压根儿就不是一个Java类,因此在Java程序中只能打印出空值。

  • 数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。

  • 代码举例:

  • package T1;
    
    public class ClassLoaderTest2 {
        public static void main(String[] args) throws ClassNotFoundException {
            // 关于基本类的  类的加载器
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader); // null,表示是引导类加载器
    
            // 关于自定义类的 类的加载器
            ClassLoader classLoader2 = Class.forName("T1.ClassLoaderTest2").getClassLoader();
            System.out.println(classLoader2); // sun.misc.Launcher$AppClassLoader@18b4aac2
    
            // ############关于数组的########
    
            // 与数组当中元素类型的类加载器是一样的
            String[] arrStr = new String[10];
            System.out.println(arrStr.getClass().getClassLoader()); // null,表示是引导类加载器
    
            ClassLoaderTest2[] ClassLoaderTest2Arr = new ClassLoaderTest2[10];
            System.out.println(ClassLoaderTest2Arr.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
    
            // 基本数据类型由虚拟机预先定义,引用数据类型则需要进行类的加载
            int[] intArr = new int[10];
            System.out.println(intArr.getClass().getClassLoader()); // null,表示不需要类的加载器
    
        }
    }
    

⑥ClassLoader源码解析

  • ClassLoader是一个抽象类,但内部没有抽象方法

  • 常用方法

    • public final ClassLoader getParent()

      • 返回该类加载器的超类加载器
    • ⭐(最核心)public Class<?> loadClass(String name)throws ClassNotFoundException

      • 加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现

      • 在这里插入图片描述

      • 在这里插入图片描述

    • protected Class<?> findClass(String name) throws ClassNotFoundException

      • 查找二进制名称为name的类,返回结果为java.lang.Class类的实例。loadClass()方法最后调用了findClass方法来返回Class对象
    • protected final Class<?>defineclass(String name,byte[]b,int off,int len)

      • defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化Class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。
    • 还有其他…

在这里插入图片描述

除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。

⑥ Class.forName()与ClassLoader.loadClass()

Class.forName()

  • Class.forName():是一个静态方法,最常用的是Class.forName(String className);
  • 根据传入的类的全限定名返回一个Class对象。该方法在将Class文件加载到内存的同时,会执行类的初始化。
  • 如:Class.forName(“com.atguigu.java.Helloworld”);

ClassLoader.loadClass()

  • ClassLoader.loadClass():这是一个实例方法,需要一个ClassLoader对象来调用该方法。
  • 该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。原因是该方法 不链接该类,则表示验证,准备,解析不会执行,后面的初始化阶段的 静态块和静态对象就不会得到执行。
  • 如:Classloader cl=…; cl.loadClass(“com.atguigu.java.Helloworld”);

⑦ 双亲委派机制:

定义:

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

优势⭐:

  • 避免类的重复加载,确保一个类的全局唯一性
    • Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 保护程序安全,防止核心API被随意篡改

代码支持:

  • 双亲委派机制在java.lang.ClassLoader.loadClass(String,boolean)方法中体现
    • 该方法的逻辑如下:
      • 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
      • 判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)方法进行加载。
      • 反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassorNull(name)方法,让引导类加载器进行加载。
      • 如果通过以上3条路径都没能成功加载,则调用findClass(name)方法进行加载。该方法最终会调用java.lang.ClassLoader方法的defineClass系列的native方法加载目标Java类。

弊端:

  • 检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
  • 通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

破坏双亲委派机制(重点在2)

破坏双亲委派机制1:

  • 用户自定义加载器直接重写loadClass()而不是loadClass()中调用的findClass()。
  • 建议不要直接重写loadClass(),而是重写findClass(),这样才不会破坏双亲委派机制。

破坏双亲委派机制2:

  • 第二次破坏双亲委派机制:线程上下文类加载器
  • 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的。越基础的类由越上层的加载器加载,如果有基础类要调用回用户的代码,启动类加载器是绝不可能认识、加载这些代码的。未解决这个问题,就引入了折一个不太优雅的设计:线程上下文类加载器
  • 有了线程上下文类加载器,引导类加载器也可以请求子类加载器去加载类。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。(SPI:在Java平台中,通常把核心类rt.jar中提供外部服务、可由应用层自行实现的接口称为SPI)
  • 在这里插入图片描述

破坏双亲委派机制3:

  • 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等
    • 热替换是指在程序的运行过程中,不停止服,只通过替换程序文件来修改程序的行为。热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中。基本上大部分脚本语言都是天生支持热替换的,比如:PHP,只要替换了PHP源文件,这种改动就会立即生效,而无需重启Web服务器。

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

  • 隔离加载类

    • 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如:Tomcat这类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。
  • 修改类加载的方式

    • 类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载
  • 扩展加载源

    • 比如从数据库、网络、甚至是电视机机顶盒进行加载
  • 防止源码泄漏

    • Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
  • 常见的场景

    • 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是JavaEE和OSGI、JPMS等框架。
    • 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值