classLoader相关

classLoader相关

在跟着B站康师傅学习了JVMclassLoader相关后总结了一些点

1.class文件i相关

1.java的跨平台性:

Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与"Class 文件"这 种特定的二进制文件格式所关联。**无论使用何种语言进行软件开发, 只要能 将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上执 行,可以说,统一而强大的 Class 文件结构,就是 Java 虚拟机的基石、桥梁。

2.前端编译器

想要让一个 Java 程序正确地运行在 JVM 中,Java 源码就是必须要被编译 为符合 JVM 规范的字节码

前端编译器的主要任务就是负责将符合 Java 语法规范的 Java 代码转换为 符合 JVM 规范的字节码文件

javac 是一种能够将 Java 源码编译为字节码的前端编译器 (idea采用的,是全量编译)

javac 编译器在将 Java 源码编译为一个有效的字节码文件过程中经历了 4 个步骤,分别是词法分析、语法分析、语义分析以及生成字节码

3.class文件的结构:

  1. 魔数
  2. Class 文件版本
  3. 常量池
  4. 访问标志
  5. 类索引、父类索引、接口索引集合
  6. 字段表集合
  7. 方法表集合
  8. 属性表集合

1.常量池

常量池计数器和常量池表

Class 文件使用了一个前置的容量计数器(constant_pool_count)加若 干个连续的数据项(constant_pool)的形式来描述常量池内容,我们把这一系列连续常量池数据称为常量池集合

常量池表项中,用于存放编译时期生成的各种字面量和符号引用,这部分内 容将在类加载后进入方法区的运行时常量池中存放

它包含了 Class 文件结构及其子结构中引用的所有字符串常量、类或接口 名、字段名和其他常量。

常 量 池 主 要 存 放 两 大 类 常 量 : 字 面 量 和符号引用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MeTcUPCM-1629732074518)(C:\Users\chenleics\AppData\Roaming\Typora\typora-user-images\image-20210820094852211.png)]

虚拟机在加载 Class 文件时才会进行动态链接,也就是说,Class 文件中不 会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用 不经过转换是无法直接被虚拟机使用的

当虚拟机运行时,需要从常量池中获得 对应的符号引用,再在类加载过程中的***解析阶段将其替换为直接引用***,并翻译到 具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联:

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式 的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现 的内存布局无关,引用的目标并不一定已经加载到内存中

直接引用:直接引用可以是直接指**向目标的指针、相对偏移量或是一个能 间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,**同 一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

1.1常量类型和结构

常量池中每一项常量都是一个表,JDK 1.7 之后共 14 种不同的表结构数,要一个一个字节的解析

这 14 种表(或者常量项结构)的共同点是:

(1).表开始的第一位是一个 u1 类型的 标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型

(2).常量池列表中,CONSTANT_Utf8_info 常量项是一种使用改进过的 UTF-8 编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者 方法的简单名称以及描述符等常量字符串信息

(3).这 14 种常量项结构还有一个特点是,其中 13 个常量项占用的字节固定,只 有 CONSTANT_Utf8_info 占用字节不固定,其大小由 length 决定。为什 么?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终 这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定, 比如你定义一个类,类名可以取长去短,所以在没编译前,大小不固定,编 译后,通过 UTF-8 编码,就可以知道其长度

常量池:可以理解为 Class 文件之中的资源仓库,它是 Class 文件结构中与 其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占 用 Class 文件空间最大的数据项目之一

1.2 常量池中为何包含这些内容?

Java 代码在进行 javac 编译的时候,并不像 C 和 C++ 那样有"连接"这一 步骤,而是在虚拟机加载 Class 文件的时候进行动态链接。也就是说,**在 Class 文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚 拟机使用。**当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建 时或运行时解析、翻译到具体的内存地址之中。

2.方法表集合

有一个比较有意思的地方,如果要重载一个方法的时候,多个方法名相同,而他们的**参数的数量不同或数量相同而类型和次序不同,**则称为方法的重载 (Overloading)。

方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写 (Overriding)。 (3)方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。

从底层解释重载:在 Java 语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简 单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个 方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含 在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方 法进行重载。

2.字节码指令集与解析

1.加载与指令

do { 
    自动计算 PC 寄存器的值加 1; 
    根据 PC 寄存器的指示位置,从字节码流中取出操作码;
    if(字节码存在操作数) 从字节码流中取出操作数;
	执行操作码所定义的操作;
}while(字节码长度>0)

1.1复习:再谈操作数栈与局部变量表

1.操作数栈(Operand Stacks)

我们知道,Java 字节码是 Java 虚拟机所使用的***指令集。因此,它与 Java 虚 拟机基于栈的计算模型是密不可分的 在解释执行过程中,每当为 Java 方法分配栈帧时,Java 虚拟机往往需要开 辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果 具体来说便是:执行每一条指令之前,Java 虚拟机要求该指令的操作数已被 压入操作数栈中*。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出, 并且**将指令的结果重新压入栈中**

2.局部变量表

Java 方法栈帧的另外一个重要组成部分则是局部变量区,字节码程序可以将 计算的结果缓存在局部变量区之中(??什么作用?上面操作结果的缓存表?等某个时候再存到操作数栈中吗????)

实际上,Java 虚拟机将局部变量区当成一个数组,依次存放 this 指针(仅非 静态方法),所传入的参数,以及字节码中的局部变量。 和操作数栈一样,long 类型以及 double 类型的值将占据两个单元,其余类 型仅占据一个单元

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。局部变量表中 的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象 都不会被回收 在方法执行时,虚拟机使用局部变量表完成方法的传递

引申:哪些可以作为GCRoot?

GC Root对象:(1)虚拟机栈中引用对象(准确来说就是局部变量表中的变量??)(2)方法区中的静态变量、常量对象;(3)本地

方法引用对象;(4)被 synchronized 修饰的对象等。

补充:

栈映射帧:

它表示的是方法中的字节码指令在执行时栈的状态(这里说的栈包括了局部变量区和操作栈),这么做的目的是为了加快字节码校验的速度,

用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型,这也解释了为什么这一过程发生在了链接中的验证阶段(字节码验证)

3.类的加载过程详解

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

按照 Java 虚拟机规范,从 Class 文件到加载到内存中的类,到类卸载出内 存位置,它的整个生命周期包括如下七个阶段:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3RYXC0GL-1629732074521)(C:\Users\chenleics\AppData\Roaming\Typora\typora-user-images\image-20210820141110960.png)]

1.什么是类的加载?

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内(也就是下面说的类模板对象),然后在****堆区****创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。

说细一点:

将 Java 类的字节码文件加载到机器内存中,并在 内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、 类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获 取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用 .

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

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误.

综上:

加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例 在加载类时,Java 虚拟机必须完成以下 3 件事情:

  1. 通过类的全名,获取类的二进制数据流 (全类名???是不是会联想到反射呢???)
  2. 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  3. 创建 java.lang.Class 类的实例,表示该类型。作为方法区这个类的各种数据 的访问入口

注意:关于数组类的加载

  1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创 建数组 A 的元素类型

  2. JVM 使用指定的元素类型和数组维度来创建新的数组类

    如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问 性决定

    否则数组类的可访问性将被缺省定义为 publi

2:链接的过程(验证,准备,解析)

2.1 验证

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kr5PkTEH-1629732074522)(C:\Users\chenleics\AppData\Roaming\Typora\typora-user-images\image-20210820144600146.png)]

后三项是针对方法区中的类模板对象操作的

其中格式验证会和加载阶段一起执行(也可以将他算加载的过程里)。验证通过之后,类加载器才会成功将 类的二进制数据信息加载到方法区中

2.2.准备

准备阶段(Preparation),简言之,为***类的静态变量分配内存***,并将其初始化为 默认值(***注意!!!!是静态变量!!!!***)

注意:

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

    ​ ??疑惑》、?没看懂什么意思,回头再看一眼,在class文件解析的时候,几大内容区域中,有一个叫**字段表集合**,它也指向常量池索引集合,它描述了每个字段的完整信息。final类型的值,直接在常量池中就已经有了!!,,所以说准备阶段就只是显示一下而已。

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

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

2.3.解析

简言之,将类、接口、字段和方法的***符号引用转为 直接引用***

符号引用就是一些字面量的引用 , 只有符号引用是不够的,比如当如下 println() 方法被调 用时,系统需要明确知道该方法的位置。

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

总结:

所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存 中的指针或者偏移量。

字符串的复习

最后,再来看一下 CONSTANT_String 的解析。由于字符串在程序开发中有 着重要的作用,因此,读者有必要了解一下 String 在 Java 虚拟机中的处理。 当在 Java 代码中直接使用字符串常量时,就会在类中出现 *CONSTANT_String, 它表示字符串常量,并且会引用一个 CONSTANT_UTF8 的常量项。*在 Java 虚 拟机内部运行中的常量池,会维护一张字符串拘留表(intern),它会保存所有出现 过的字符串常量,并且没有重复项。只要以 CONSTANT_String 形式出现的字 符串也都会在这张表中。使用 String.intern() 方法可以得到一个字符串在拘留表 中的引用,因为该表中没有重复项,所以任何字面相同的字符串的 String.intern() 方法返回总是相等的

3.初始化

初始化阶段,简言之,为类的静态变量赋予正确的初始值(之前分配了一些默认值,现在要把他赋值成初始值了)

初始化阶段的重要工作是执行类的初始化方法:clinit() 方法

该方法仅能由 Java 编译器生成并由 JVM 调用,程序开发者无法自定义一 个同名的方法,更无法直接在 Java 程序中调用该方法,虽然该方法也是由 字节码指令所组成

它是类静态成员的赋值语句以及 static

1.一些说明:

  1. 在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的 总是 在子类 之前被调用,也就是说,父类的 static 块优先级高于子类(由父及子,静态先行)
  2. Java 编译器并不会为所有的类都产生 () 初始化方法。哪些类在编译为字节 码后,字节码文件中将不会包含 () 方法?
  3. 一个类中并没有声明任何的类变量,也没有静态代码块时
  4. 一个类中声明类变量,**但是没有明确使用类变量的初始化语句以及静态代码 块来执行初始化操作(我认为是没有一些赋值语句)**时
  5. 一个类中包含 static final 修饰的基本数据类型的字段,这些类字段初始化语 句采用编***译时常量表达式***

static +final 情况结论:

最终结论:***使用 static + final 修饰,且显示赋值中不涉及到方法或构造器调用的基本数据类型或 String 类型的显式赋值,是在链接阶段的准备环节进行***

2.clinit()的线程安全性

对于 clinit() 方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环 境中的安全性 虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,如果 多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法, 其他线程都需要阻塞等待,直到活动线程执行 () 方法完毕。因此如果一个类中的clinit()方法里有很耗时的操作时,有可能会引发死锁,这个比较难发现。

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

1.主动使用

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

  1. 当创建一个类的实例时,比如使用 new 关键字,或者通过反射、克隆、反 序列化
  2. 当调用类的静态方法时,即当使用了字节码 invokestatic 指令
  3. 当使用类、接口的静态字段时(final 修饰特殊考虑),比如,使用 getstatic 或 者 putsttic 指令。(对应访问变量、赋值变量操作)
  4. 当 使 用 java.lang.reflect 包 中 的 方 法 反 射 类 的 方 法 时 。
  5. 当初始化子类时,如果发现其分类还没有进行过初始化,则需要先触发其父 类的初始化
  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类 的初始化,该接口要在其之前被初始化
  7. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个 类),虚拟机会先初始化这个主类
  8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所 在的类。
2.被动使用

除了以上的情况属于主动使用,其他的情况均属于被动使用。被动使用不会 引起类的初始化 也就是说:并不是在代码中出现的类,就一定会被加载或者初始化。如果不 符合主动使用的条件,类就不会初始化

  1. 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化

    当通过***子类引用父类的静态变量***,不会导致子类初始化 (但是父类会初始化)

  2. 通过数组定义类引用,不会触发此类的初始化 (返回到上面的类加载,说了一些数组加载的情况,这里所有的情况都基于已经经过了加载,链接的过程到这一步的)

  3. ***引用变量***不会触发此类或接口的初始化。因为常量在链接阶段就已经被显式 赋值了

  4. 调用 ClassLoader 类的 loadClass() 方法加载一个类,并不是对类的主动使 用,不会导致类的初始化

4.类的加载器

一些问题:

(1):什么是双亲委派模型,使用原因?

(2):有哪些类加载器,都加载哪些文件

(3):双亲可以打破嘛?为什么?

1.一些概述

1.加载类型

类的加载分类:显式加载,隐式加载

​ 显式加载指的是在代码中通过调用 ClassLoader 加载 Class 对象,如直接使 用 Class.forName(name) 或 this.getClass().getClassLoader().loadClass() 加载 Class 对象

​ 隐式加载则是不直接在代码中调用 ClassLoader 的方法加载 Class 对象,而 是通过虚拟机自动加载到内存中,如在加载某个类的 Class 文件时,该类的 Class 文件中引用了另外一个类的对象,此时额外引用的类将通过 JVM 自 动加载到内存中

2.命名空间

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

每个类加载器都有自己的命名空间,命名空间由该加载器所有的父加载器所 加载的类组成

3.类加载机制的基本特征:

**双亲委派模型。**但不是所有类加载都遵守这个模型,有的时候,启动类加载 器 所 加 载 的 类 型 , 是 可 能 要 加 载 用 户 代 码 的 , 比 如 JDK 内 部 的 ServiceProvider/ServiceLoader 机制,用户可以在标准 API 框架上,提供自 己的实现,JDK 也需要提供些默认的参考实现。例如,Java 中 JNDI、JDBC、 文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双 亲委派模型去加载,而是利用所谓的上下文加载器

可见性子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。 不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻 辑

单一性,****由于父加载器的类型对于子加载器是可见的,所以父加载器中加载 过的类型,就不会在子加载器中重复加载。但是注意,类加载器"邻居"间, 同一类型仍然可以被加载多次,因为相互并不可见

2.类加载器的分类

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

常见结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rHkXhVk4-1629732074525)(C:\Users\chenleics\AppData\Roaming\Typora\typora-user-images\image-20210823214003620.png)]

注意:启动类加载器又叫引导类加载器(作用: 加载扩展类和应用程序类加载器,并指定为他们的父类加载器)

应用程序类加载器又叫:系统类加载器(负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库)

. 用户自定义类加载器:自定义加载器能够实现应用隔离,(利用的是不同类加载器加载不同的类)通常需要继承ClassLoader

3.类加载器测试时的一些注意问题

对于数组类的类加载器来说,是通过 Class.geetClassLoader() 返回的,与数组当中元素类型的类加载器是一样的:如 果数组当中的元素类型是基本数据类型,数组类是没有类加载器的(基本类型不需要类加载器,引用类型需要)

4.Class Loader源码解析

Launcher类下有ApplicationClass Loader和ExClassLoder

sun.misc.Launcher类是Java的入口类,在启动Java应用是,会创建Launcher,然后根据需要再来加载一些类加载器。

对于ClassLoader ,它是一个顶层的抽象类,上面所说的加载一些类加载器,这些类加载器最顶层的加载器都是ClassLoader:结构如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vdwr1PL4-1629732074527)(C:\Users\chenleics\AppData\Roaming\Typora\typora-user-images\image-20210823215113244.png)]

Java 提 供了抽象类 java.lang.ClassLoader,所有用户自定义的类加载器都应该继承 ClassLoader 类

1.ClassLoader的主要方法:

(1):public final ClassLoader getParent() 返回该类加载器的超类加载器

(2):public Class loadClass(String name) throws ClassNotFoundException

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

(3):protected Class findClass(String name) throws ClassNotFoundException

查找二进制名称为 name 的类,返回结果为 java.lang.Class 类的实例。这是 一个受保护的方法,JVM 鼓励我们重写此方法,需要自定义加载器遵循双亲委 派机制,该方法会在检查完父类加载器之后被 loadClass() 方法调用

在 JDK 1.2 之后已不再建议 用户去覆盖 loadClass() 方法,而是建议把自定义的类加载逻辑写在 find Class() 方法中,从前面的分析可知,findClass() 方法是在 loadClass() 方法中被调用的, 当 loadClass() 方法中父加载器加载失败后,则会调用自己的 findClass() 方法来 完成类加载,这样就可以保证自定义的类加载器也符合双亲委派机制

​ 需要注意的是 ClassLoader 类中并没有实现 findClass() 方法的具体代码逻 辑,取而代之的是抛出 ClassNotFoundException 异常,同时应该知道的是 findClass() 方法通常是和 defineClass() 方法一起使用的。一般情况下,在自定 义类加载器时,***会直接覆盖 ClassLoader 的 findClass() 方法并编写加载规则, 取得要加载类的字节码后转换成流,然后调用 defineClass() 方法生成类的 Class 对象***

(4):protected final Class defineClass(String name, byte[] b, int off, int len) 根据给定的字节数组 b 转换为 Class 的实例,off 和 len 参数表示实际 Class 信息在 byte 数组中的位置和长度,其中 byte 数组 b 是 ClassLoader 从 外部获取的。这是受保护的方法,只有在自定义 ClassLoader 子类中可以

    protected Class<?> loadClass(String name, boolean resolve)    //resolve:true 加载 Class 的同时进行解析操作
        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 {
                        // parent 为 null :父类加载器是引导类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
			
                if (c == null) {// 当前类的加载器的父类加载未加载其此类 or 当前类的加载器未加载此类
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //调用findClass 自己找
                    /*
                    注意!!!!这里的findClass是没有东西的,已经在URLClassLoader中重写了,在findClass内部又调用了defindClassLoader()
                    用以获取类实力,由给定的二进制流加载实例
                    */
                    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;
        }
    }

2.SecureClassLoader 与 URLClassLoader

而 URLClassLoader 这个实现类为这些方法提供 了具体的实现。并新增了 URLClassPath 类协助取得 Class 字节码流等功能。 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免自己去编写 findClass() 方法及其获取字节 码流的方式,使自定义类加载器编写更加简洁

3.ExtClassLoader 与 AppClassLoader

ExtClassLoader 并没有重写 loadClass() 方法,这足以说明其遵循双亲委派 模式,而 AppClassLoader 重载了 loadClass() 方法,但最终调用的还是父类 loadClass() 方法,因此依然遵循双亲委派模式

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

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

ClassLoader.loadClass() 这是一个实例方法,需要一个 ClassLoader 对象来调 用该方法。该方法将 Class 文件加载到内存时,并不会执行类的初始化,直 到 这 个 类 第 一 次 使 用 时 才 进 行 初 始 化 。 该 方 法 因 为 需 要 得 到 一 个 ClassLoader 对象,所以可以根据需要指定使用哪个类加载器,(被动调用)

5.双亲委派模型

1.定义与本质

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

2.本质 规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载 器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载

2.优势与劣势

避免类的重复加载,确保一个类的全局唯一性

​ Java 类随着它的类加载器一起具备了一种带有优先级的层级关系,通过这种 层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一

保护程序安全,防止核心 API 被随意篡改

preDefineClass() 接口,该接口中提供了对 JDK 核心类库的保护

3.破坏双亲委派机制

双亲机制问题:

顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类

比如在系统类中提供了一个接 口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该 接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方 法无法创建由应用类加载器加载的应用实例的问题

方法:

(1)如果父类 加载失败,会自动调用自己的 findClass() 方法来完成加载,这样既不影响用户 按照自己的意愿去加载类,又可以保证新写出来的类加载器是符合双亲委派规则 的

(2)线程上下文类加载器(Thread Context ClassLoad

JNDI 服务 使用这个线程上下文类加载器去加载所需的 SPI 服务代码。这是一种负累加载 器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型 的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也 是无可奈何的事情。

(3)热替换(通过运用 ClassLoader实现)

由不同 ClassLoader 加载的同名类属于不同的类型,不能相互转换和 兼容。即两个不同的 ClassLoader 加载同一个类,在虚拟机内部,会认为这 2 个类是完全不同的 根据这个特点,可以用来模拟热替换的实现

6:沙箱安全

保护程序安全

保护 Java 原生的 JDK 代码

Java 安全模型的核心就是 Java 沙箱(Sandbox),什么是沙箱?沙箱就是一个 限制程序运行的环境 沙箱机制就是将 Java 代码**限定在虚拟机(JVM)特定的运行范围中,并且 严格限制代码对本地系统资源访问。**通过这样的措施来保证对代码的有限隔离, 防止对本地系统造成破坏

7.自定义类加载器

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

(1).隔离加载类

(2).修改类加载的方式

(3).扩展类加载源

(4).防止源码泄露

实现方式:、

(1):重写loadClass()

(2):重写find Class()

让其父类是系统类加载器;

assLoader实现)

由不同 ClassLoader 加载的同名类属于不同的类型,不能相互转换和 兼容。即两个不同的 ClassLoader 加载同一个类,在虚拟机内部,会认为这 2 个类是完全不同的 根据这个特点,可以用来模拟热替换的实现

6:沙箱安全

保护程序安全

保护 Java 原生的 JDK 代码

Java 安全模型的核心就是 Java 沙箱(Sandbox),什么是沙箱?沙箱就是一个 限制程序运行的环境 沙箱机制就是将 Java 代码**限定在虚拟机(JVM)特定的运行范围中,并且 严格限制代码对本地系统资源访问。**通过这样的措施来保证对代码的有限隔离, 防止对本地系统造成破坏

7.自定义类加载器

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

(1).隔离加载类

(2).修改类加载的方式

(3).扩展类加载源

(4).防止源码泄露

实现方式:、

(1):重写loadClass()

(2):重写find Class()

让其父类是系统类加载器;

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值