04-JVM类加载

类加载

1.对象访存

图片存疑:new Customer( )实例的类型指针是指向该对象的Class类对象,而不是直接指向方法区的元数据信息。

1.1存储结构

一个 Java 对象内存中存储为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充 (Padding)

对象头:

  • 普通对象:分为两部分

    • Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等

      hash(25) + age(4) + lock(3) = 32bit					#32位系统
      unused(25+1) + hash(31) + age(4) + lock(3) = 64bit	#64位系统
      
    • Klass Word:类型指针,指向该对象的 Class 类对象的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;在 64 位系统中,开启指针压缩(-XX:+UseCompressedOops)或者 JVM 堆的最大值小于 32G,这个指针也是 4byte,否则是 8byte(就是 Java 中的一个引用的大小

    |-----------------------------------------------------|
    | 				  Object Header (64 bits) 			  |
    |---------------------------|-------------------------|
    | 	 Mark Word (32 bits)	|  Klass Word (32 bits)   |
    |---------------------------|-------------------------|
    
  • 数组对象:如果对象是一个数组,那在对象头中还有一块数据用于记录数组长度(12 字节)

    |-------------------------------------------------------------------------------|
    | 						  Object Header (96 bits) 							    |
    |-----------------------|-----------------------------|-------------------------|
    |  Mark Word(32bits)    | 	  Klass Word(32bits) 	  |   array length(32bits)  |
    |-----------------------|-----------------------------|-------------------------|
    

实例数据:实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来

对齐填充:Padding 起占位符的作用。64 位系统,由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,就是对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或者 2 倍),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全

32 位系统:

  • 一个 int 在 java 中占据 4byte,所以 Integer 的大小为:

    private final int value;
    
    # 需要补位4byte
    4(Mark Word) + 4(Klass Word) + 4(data) + 4(Padding) = 16byte
    
  • int[] arr = new int[10]

    # 由于需要8位对齐,所以最终大小为56byte`。
    4(Mark Word) + 4(Klass Word) + 4(length) + 4*10(10个int大小) + 4(Padding) = 56sbyte
    

1.2实际大小

浅堆(Shallow Heap):对象本身占用的内存,不包括内部引用对象的大小,32 位系统中一个对象引用占 4 个字节,每个对象头占用 8 个字节,根据堆快照格式不同,对象的大小会同 8 字节进行对齐

JDK7 中的 String:2个 int 值共占 8 字节,value 对象引用占用 4 字节,对象头 8 字节,对齐后占 24 字节,为 String 对象的浅堆大小,与 value 实际取值无关,无论字符串长度如何,浅堆大小始终是 24 字节

private final char value[];
private int hash;
private int hash32;

保留集(Retained Set):对象 A 的保留集指当对象 A 被垃圾回收后,可以被释放的所有的对象集合(包括 A 本身),所以对象 A 的保留集就是只能通过对象 A 被直接或间接访问到的所有对象的集合,就是仅被对象 A 所持有的对象的集合

深堆(Retained Heap):指对象的保留集中所有的对象的浅堆大小之和,一个对象的深堆指只能通过该对象访问到的(直接或间接)所有对象的浅堆之和,即对象被回收后,可以释放的真实空间

对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和,也就是通常意义上我们说的对象大小

下图显示了一个简单的对象引用关系图,对象 A 引用了 C 和 D,对象 B 引用了 C 和 E。那么对象 A 的浅堆大小只是 A 本身,A 的实际大小为 A、C、D 三者之和,A 的深堆大小为 A 与 D 之和,由于对象 C 还可以通过对象 B 访问到 C,因此 C 不在对象 A 的深堆范围内

内存分析工具 MAT 提供了一种叫支配树的对象图,体现了对象实例间的支配关系

基本性质:

  • 对象 A 的子树(所有被对象 A 支配的对象集合)表示对象 A 的保留集(retained set),即深堆

  • 如果对象 A 支配对象 B,那么对象 A 的直接支配者也支配对象 B

  • 支配树的边与对象引用图的边不直接对应

左图表示对象引用图,右图表示左图所对应的支配树:

比如:对象 F 与对象 D 相互引用,因为到对象 F 的所有路径必然经过对象 D,因此对象 D 是对象 F 的直接支配者

参考文章:https://www.yuque.com/u21195183/jvm/nkq31c


1.3节约内存

  • 尽量使用基本数据类型

  • 满足容量前提下,尽量用小字段

  • 尽量用数组,少用集合,数组中是可以使用基本类型的,但是集合中只能放包装类型,如果需要使用集合,推荐比较节约内存的集合工具:fastutil

    一个 ArrayList 集合,如果里面放了 10 个数字,占用多少内存:

    private transient Object[] elementData;
    private int size;
    

    Mark Word 占 4byte,Klass Word 占 4byte,一个 int 字段占 4byte,elementData 数组占 12byte,数组中 10 个 Integer 对象占 10×16,所以整个集合空间大小为 184byte(深堆)

  • 时间用 long/int 表示,不用 Date 或者 String


1.4对象访问

JVM 是通过栈帧中的对象引用访问到其内部的对象实例:

  • 句柄访问:Java 堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息

    优点:reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改

  • 直接指针(HotSpot 采用):Java 堆对象的布局必须考虑如何放置访问类型数据的相关信息,reference 中直接存储的对象地址

    优点:速度更快,节省了一次指针定位的时间开销

    缺点:对象被移动时(如进行 GC 后的内存重新排列),对象的 reference 也需要同步更新

参考文章:https://www.cnblogs.com/afraidToForget/p/12584866.html


2.对象创建

2.1生命周期

在 Java 中,对象的生命周期包括以下几个阶段:

  1.  创建阶段 (Created):
    
  2.  应用阶段 (In Use):对象至少被一个强引用持有着
    
  3.  不可见阶段 (Invisible):程序的执行已经超出了该对象的作用域,不再持有该对象的任何强引用
    
  4.  不可达阶段 (Unreachable):该对象不再被任何强引用所持有,包括 GC Root 的强引用
    
  5.  收集阶段 (Collected):垃圾回收器对该对象的内存空间重新分配做好准备,该对象如果重写了 finalize() 方法,则会去执行该方法
    
  6.  终结阶段 (Finalized):等待垃圾回收器对该对象空间进行回收,当对象执行完 finalize() 方法后仍然处于不可达状态时进入该阶段
    
  7.  对象空间重分配阶段 (De-allocated):垃圾回收器对该对象的所占用的内存空间进行回收或者再分配
    

参考文章:https://blog.csdn.net/sodino/article/details/38387049


2.2创建时机

类在第一次实例化加载一次,后续实例化不再加载,引用第一次加载的类

Java 对象创建时机:

  1. 使用 new 关键字创建对象:由执行类实例创建表达式而引起的对象创建

  2. 使用 Class 类的 newInstance 方法(反射机制)

  3. 使用 Constructor 类的 newInstance 方法(反射机制)

    public class Student {
        private int id;
        public Student(Integer id) {
            this.id = id;
        }
        public static void main(String[] args) throws Exception {
            Constructor<Student> c = Student.class.getConstructor(Integer.class);
            Student stu = c.newInstance(123);
        }
    }
    

    使用 newInstance 方法的这两种方式创建对象使用的就是 Java 的反射机制,事实上 Class 的 newInstance 方法内部调用的也是 Constructor 的 newInstance 方法

  4. 使用 Clone 方法创建对象:用 clone 方法创建对象的过程中并不会调用任何构造函数,要想使用 clone 方法,我们就必须先实现 Cloneable 接口并实现其定义的 clone 方法

  5. 使用(反)序列化机制创建对象:当反序列化一个对象时,JVM 会创建一个单独的对象,在此过程中,JVM 并不会调用任何构造函数,为了反序列化一个对象,需要让类实现 Serializable 接口

从 Java 虚拟机层面看,除了使用 new 关键字创建对象的方式外,其他方式全部都是通过转变为 invokevirtual 指令直接创建对象的


2.3创建过程

创建对象的过程:

  1. 判断对象对应的类是否加载、链接、初始化

  2. 为对象分配内存:指针碰撞、空闲链表。当一个对象被创建时,虚拟机就会为其分配内存来存放对象的实例变量及其从父类继承过来的实例变量,即使从隐藏变量也会被分配空间(继承部分解释了为什么会隐藏)

  3. 处理并发安全问题:

    • 采用 CAS 配上自旋保证更新的原子性
    • 每个线程预先分配一块 TLAB
  4. 初始化分配的空间:虚拟机将分配到的内存空间都初始化为零值(不包括对象头),保证对象实例字段在不赋值时可以直接使用,程序能访问到这些字段的数据类型所对应的零值

  5. 设置对象的对象头:将对象的所属类(类的元数据信息)、对象的 HashCode、对象的 GC 信息、锁信息等数据存储在对象头中

  6. 执行 init 方法进行实例化:实例变量初始化、实例代码块初始化 、构造函数初始化

    • 实例变量初始化与实例代码块初始化:

      对实例变量直接赋值或者使用实例代码块赋值,编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(Java 要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前

    • 构造函数初始化:

      Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性,在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到 Object 类。然后从 Object 类依次对以下各类进行实例化,初始化父类中的变量和执行构造函数


2.4承上启下

  1. 一个实例变量在对象初始化的过程中会被赋值几次?一个实例变量最多可以被初始化 4 次

    JVM 在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个实例变量被第一次赋值;在声明实例变量的同时对其进行了赋值操作,那么这个实例变量就被第二次赋值;在实例代码块中又对变量做了初始化操作,那么这个实例变量就被第三次赋值;;在构造函数中也对变量做了初始化操作,那么这个实例变量就被第四次赋值

  2. 类的初始化过程与类的实例化过程的异同?

    类的初始化是指类加载过程中的初始化阶段对类变量按照代码进行赋值的过程;类的实例化是指在类完全加载到内存中后创建对象的过程(类的实例化触发了类的初始化,先初始化才能实例化)

  3. 假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?(经典案例

    public class StaticTest {
        public static void main(String[] args) {
            staticFunction();//调用静态方法,触发初始化
        }
    
        static StaticTest st = new StaticTest();
    
        static {   //静态代码块
            System.out.println("1");
        }
    
        {       // 实例代码块
            System.out.println("2");
        }
    
        StaticTest() {    // 实例构造器
            System.out.println("3");
            System.out.println("a=" + a + ",b=" + b);
        }
    
        public static void staticFunction() {   // 静态方法
            System.out.println("4");
        }
    
        int a = 110;    		// 实例变量
        static int b = 112;     // 静态变量
    }
    
    2
    3
    a=110,b=0
    1
    4
    

    static StaticTest st = new StaticTest();

    • 实例实例化不一定要在类初始化结束之后才开始

    • 在同一个类加载器下,一个类型只会被初始化一次。所以一旦开始初始化一个类,无论是否完成后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此在实例化上述程序中的 st 变量时,实际上是把实例化嵌入到了静态初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置,这就导致了实例初始化完全发生在静态初始化之前,这也是导致 a 为 110,b 为 0 的原因

    代码等价于:

    public class StaticTest {
        <clinit>(){
            System.out.println("2");	// 实例代码块
            a = 110;    // 实例变量
            System.out.println("3");	// 实例构造器中代码的执行
            System.out.println("a=" + a + ",b=" + b);  // 实例构造器中代码的执行
            类变量st被初始化
            System.out.println("1");	//静态代码块
            类变量b被初始化为112
        }
    }
    

3.类加载阶段

3.1生命周期

类是在运行期间第一次使用时动态加载的(不使用不加载),而不是一次性加载所有类,因为一次性加载会占用很多的内存,加载的类信息存放于一块成为方法区的内存空间

包括 7 个阶段:

  • 加载(Loading) 加载就是寻找类/接口的二进制标识,并且根据二进制表示创建类/接口

    Loading is the process of finding the binary representation of a class or interface type with a particular name and creating a class or interface from that binary representation
    
  • 链接:验证(Verification)、准备(Preparation)、解析(Resolution):组合类/接口让JVM执行

    Linking is the process of taking a class or interface and combining it into the run-time state of the Java Virtual Machine so that it can be executed.
    
  • 初始化(Initialization): 更多参考 init

    Initialization of a class or interface consists of executing the class or interface initialization method <clinit>
    
  • 使用(Using)

  • 卸载(Unloading)


3.2加载

加载是类加载的其中一个阶段,注意不要混淆;完整的类加载包括加载、链接、初始化

2.1加载流程

加载过程完成以下三件事:

  • 通过类的完全限定名称获取定义该类的二进制字节流(二进制字节码)
  • 将该字节流表示的静态存储结构转换为方法区的运行时存储结构
  • 在内存中生成一个代表该类的 Class 对象,作为该类在方法区中的各种数据的访问入口

其中二进制字节流可以从以下方式中获取:

  • 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础
  • 从网络中获取,最典型的应用是 Applet
  • 由其他文件生成,例如由 JSP 文件生成对应的 Class 类
  • 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 生成字节码
2.2加载内容

将字节码文件加载至方法区后,会在堆中创建一个 java.lang.Class 对象,用来引用位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象。

方法区内部采用 C++ 的 instanceKlass 描述 Java 类的数据结构:

  • _java_mirror 即 java 的类镜像,例如对 String 来说就是 String.class,作用是把 Klass 暴露给 Java 使用
  • _super 父类、_fields 成员变量、_methods 方法、_constants 常量池、_class_loader 类加载器、_vtable 虚方法表_itable 接口方法表

加载过程:

  • 如果这个类还有父类没有加载,先加载父类
  • 加载和链接可能是交替运行的
  • Class 对象和 _java_mirror 相互持有对方的地址,堆中对象通过 instanceKlass 和元空间进行交互
  • JDK1.8中类对象位于堆中,方法区中存储的是类的结构信息instanceKlass。

创建数组类有些特殊,因为数组类本身并不是由类加载器负责创建,而是由 JVM 在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建,创建数组类的过程:

  • 如果数组的元素类型是引用类型,那么遵循定义的加载过程递归加载和创建数组的元素类型
  • JVM 使用指定的元素类型和数组维度来创建新的数组类
  • 基本数据类型由启动类加载器加载

3.3链接

类加载过程中链接阶段的可以大致分为三个阶段:1)验证 2)准备 3)解析

3.1验证

确保 Class 文件的字节流中包含的信息是否符合 JVM 规范,保证被加载类的正确性,不会危害虚拟机自身的安全

主要包括四种验证

  • 文件格式验证

  • 语义检查,但凡在语义上不符合规范的,虚拟机不会给予验证通过

    • 是否所有的类都有父类的存在(除了 Object 外,其他类都应该有父类)

    • 是否一些被定义为 final 的方法或者类被重写或继承了

    • 非抽象类是否实现了所有抽象方法或者接口方法

    • 是否存在不兼容的方法

  • 字节码验证,试图通过对字节码流的分析,判断字节码是否可以被正确地执行

    • 在字节码的执行过程中,是否会跳转到一条不存在的指令
    • 函数的调用是否传递了正确类型的参数
    • 变量的赋值是不是给了正确的数据类型
    • 栈映射帧(StackMapTable)在这个阶段用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型
  • 符号引用验证,Class 文件在其常量池会通过字符串记录将要使用的其他类或者方法


3.2准备

准备阶段为静态变量分配内存并设置初始值,使用的是堆的内存:

  • 类变量也叫静态变量,就是是被 static 修饰的变量
  • 实例变量也叫对象变量,即没加 static 的变量

说明:实例变量不会在这阶段分配内存,它会在对象实例化时随着对象一起被分配在堆中,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次

类变量初始化:

  • static 变量分配空间和赋值是两个步骤:分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型以及字符串常量,那么编译阶段值(转化为字节码)就确定了,准备阶段会显式初始化
  • 如果 static 变量是 final 的,但属于引用类型或者构造器方法的字符串,赋值在初始化阶段完成

Java的静态变量存储位置?

  • 在JDK1.7前与类的结构存储在一起,即位于方法区。
  • 从JDK7开始与class对象存储在一起,比如JDK8中静态变量就位于class对象所在的堆中。
  • Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故 boolean 的默认值就是 false
public class test10 {
    static int a;                                 // 1) 准备阶段仅仅进行空间分配
    static int b = 10;                            // 2) 准备阶段完成赋值并初始化
    static final int c = 20;                      // 3) 准备阶段完成赋值并初始化
    static final String d = "hello";              // 4) 准备阶段完成赋值并初始化
    static final Object e = new Object();         // 5) 准备阶段仅仅进行空间分配,初始化阶段完成赋值,通过 <cinit> 构造函数实现
}

3.3解析

将常量池中类、接口、字段、方法的符号引用替换为直接引用(内存地址)的过程:

  • 符号引用:一组符号来描述目标,可以是任何字面量,属于编译原理方面的概念,如:包括类和接口的全限名、字段的名称和描述符、方法的名称和方法描述符
  • 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,如果有了直接引用,那说明引用的目标必定已经存在于内存之中``

例如:在 com.demo.Solution 类中引用了 com.test.Quest,把 com.test.Quest 作为符号引用存进类常量池,在类加载完后,用这个符号引用去方法区找这个类的内存地址

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等

  • 在类加载阶段解析的是非虚方法,静态绑定
  • 也可以在初始化阶段之后再开始解析,这是为了支持 Java 的动态绑定
  • 通过解析操作,符号引用就可以转变为目标方法在类的虚方法表中的位置,从而使得方法被成功调用
public class Load2 {
    public static void main(String[] args) throws Exception{
    ClassLoader classloader = Load2.class.getClassLoader();
    // cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D
    Class<?> c = classloader.loadClass("cn.jvm.t3.load.C");
        
    // new C();会导致类的解析和初始化,从而解析初始化D
    System.in.read();
    }
}
class C {
	D d = new D();
}
class D {
}

使用HSDB工具

D:\Java\bin>java -cp ../lib/sa-jdi.jar sun.jvm.hotspot.HSDB
ClassLoader classloader = Load2.class.getClassLoader();
// cloadClass 加载类方法不会导致类的解析和初始化,也不会加载D
Class<?> c = classloader.loadClass("cn.jvm.t3.load.C");


创建实例对象会导致类的解析和初始化,从而解析初始化D

new C();


3.4初始化

4.1介绍

​ 初始化阶段才真正开始执行类中定义的 Java 程序代码,在准备阶段,类变量已经赋过一次系统要求的初始值;在初始化阶段,通过程序制定的计划去初始化类变量和其它资源,执行

​ 在编译生成 class 文件时,编译器会产生两个方法加于 class 文件中,一个是类的初始化方法 clinit,另一个是实例的初始化方法 init

​ 类构造器 () 与实例构造器 () 不同,它不需要程序员进行显式调用,在一个类的生命周期中,类构造器最多被虚拟机调用一次,而实例构造器则会被虚拟机调用多次,只要程序员创建对象

4.2clinit

():类构造器,由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的

作用:是在类加载过程中的初始化阶段进行静态变量初始化和执行静态代码块

  • 如果类中没有静态变量或静态代码块,那么 clinit 方法将不会被生成
  • clinit 方法只执行一次,在执行 clinit 方法时,必须先执行父类的clinit方法
  • static 变量的赋值操作和静态代码块的合并顺序由源文件中出现的顺序决定
  • static 不加 final 的变量都在初始化环节赋值

线程安全问题:

  • 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都阻塞等待,直到活动线程执行 () 方法完毕
  • 如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽

特别注意:静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问

public class Test {
    static {
        //i = 0;                // 给变量赋值可以正常编译通过
        System.out.print(i);  	// 这句编译器会提示“非法向前引用”
    }
    static int i = 1;
}

接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 () 方法,两者不同的是:

  • 在初始化一个接口时,并不会先初始化它的父接口,所以执行接口的 () 方法不需要先执行父接口的 () 方法
  • 在初始化一个类时,不会先初始化所实现的接口,所以接口的实现类在初始化时不会执行接口的 () 方法
  • 只有当父接口中定义的变量使用时,父接口才会初始化

4.3时机

原则:类的初始化是懒惰的,只有在首次使用时才会被装载,JVM 不会无条件地装载 Class 类型,Java 虚拟机规定,一个类或接口在初次使用前,必须要进行初始化

  • Class.forName:返回与给定的字符串名称相关联类或接口的Class对象。

主动引用:虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列情况必须对类进行初始化(加载、验证、准备都会发生):

  • 当创建一个类的实例时,使用 new 关键字,或者通过反射、克隆、反序列化(前文讲述的对象的创建时机)
  • 当调用类的静态方法或访问静态字段时,遇到 getstatic、putstatic、invokestatic 这三条字节码指令,如果类没有进行过初始化,则必须先触发其初始化
    • getstatic:程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)
    • putstatic:程序给类的静态变量赋值
    • invokestatic :调用一个类的静态方法
  • 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发其初始化
  • 子类初始化,如果父类还没初始化,会引发,但这条规则并不适用于接口
  • 子类访问父类的静态变量,不会导致子类初始化,只会触发父类的初始化
  • 当虚拟机启动时,需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类
  • MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这两个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类
  • 执行Class.forName默认情况下会导致类的初始化。
  • 补充:当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

被动引用:所有引用类的方式都不会触发初始化,称为被动引用

  • 通过子类引用父类的静态字段,不会导致子类初始化,只会触发父类的初始化
  • 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法
  • 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化(原因:准备阶段已完成)
  • 使用类加载器loadclass方法
  • Class.forName方法的参数2为false情况。
class A {
    static int a = 0;
    static {
        System.out.println("a init");
    }
}

class B extends A {
    final static double b = 5.0;
    static boolean c = false;
    static {
        System.out.println("b init");
    }
}

验证(实验时请先全部注释,每次只执行其中一个)

public class Load3 {
    static {
        System.out.println("main init");
    }
    public static void main(String[] args) throws ClassNotFoundException, IOException {
//        // 1. 静态常量不会触发初始化
//        System.out.println(B.b);
//        // 2. 类对象.class 不会触发初始化
//        System.out.println(B.class);
//        // 3. 创建该类的数组不会触发初始化
//        System.out.println(new B[0]);
        // 4. 不会初始化类 B,但会加载 B、A
//        ClassLoader cl = Thread.currentThread().getContextClassLoader();
//        cl.loadClass("cn.itcast.jvm.t3.load.B");
//        // 5. 不会初始化类 B,但会加载 B、A
//        ClassLoader c2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("cn.itcast.jvm.t3.load.B", false, c2);
        System.in.read();


//        // 1. 首次访问这个类的静态变量或静态方法时
//        System.out.println(A.a);
//        // 2. 子类初始化,如果父类还没初始化,会引发
//        System.out.println(B.c);
//        // 3. 子类访问父类静态变量,只触发父类初始化
//        System.out.println(B.a);
//        // 4. 会初始化类 B,并先初始化类 A
//        Class.forName("cn.itcast.jvm.t3.load.B");

    }
}

懒惰初始化的单例模式(类的懒惰初始化应用)

// 内部类中保存单例

// 这里利用了 Singleton类加载过程中在初始化阶段的懒惰初始化的特性,当首次访问类的静态方法getInstance时,触发类的初始化。

// 注意:单例模式实现是线程安全的,静态代码只在class的加载过程中执行一次,JVM提供的类加载器(C实现)需要确保多个线程进行初始化操作的时候,只有一个线程实现初始化

// 双亲委派机制是推荐使用类加载器的方式,而类加载器是JVM底层实现用于加载类的代码,二者要注意区分。

class Singleton {
    private Singleton() {}
    
    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
        static {
            System.out.println("lazy holder init");
        }
    }
    public static Singleton getInstance() {
        return LazyHolder.SINGLETON;
    }
}

3.5init

init 指的是实例构造器,主要作用是在类实例化过程中执行,执行内容包括成员变量初始化和代码块的执行

​ 实例化即调用 ()V ,虚拟机会保证这个类的构造方法的线程安全,先为实例变量分配内存空间,再执行赋默认值,然后根据源码中的顺序执行赋初值或代码块,没有成员变量初始化和代码块则不会执行

​ 类实例化过程:父类的类构造器() -> 子类的类构造器() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数


3.6卸载阶段

时机:执行了 System.exit() 方法,程序正常执行结束,程序在执行过程中遇到了异常或错误而异常终止,由于操作系统出现错误而导致Java虚拟机进程终止

卸载类即该类的 Class 对象被 GC,卸载类需要满足3个要求:

1. 该类所有的实例已经被回收
2. 加载该类的ClassLoder已经被回收
3. 该类对应的java.lang.Class对象没有任何对方被引用

​ 在 JVM 生命周期类,由 JVM 自带的类加载器加载的类是不会被卸载的,自定义的类加载器加载的类是可能被卸载。因为 JVM 会始终引用启动、扩展、系统类加载器,这些类加载器始终引用它们所加载的类,这些类始终是可及的。

总结:由Java虚拟机自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载

Java 类何时会被卸载

Unloading classes in java?


类加载器

1.类加载方式

类加载方式:

  • 隐式加载:不直接在代码中调用 ClassLoader 的方法加载类对象
    • 创建类对象、使用类的静态域、创建子类对象、使用子类的静态域
    • 在 JVM 启动时,通过三大类加载器加载 class
  • 显式加载:
    • ClassLoader.loadClass(className):只加载和连接,不会进行初始化
    • Class.forName(String name, boolean initialize, ClassLoader loader):使用 loader 进行加载和连接,根据参数 initialize 决定是否初始化

类的唯一性:

  • 在 JVM 中表示两个 class 对象判断为同一个类存在的两个必要条件:
    • 类的完整类名必须一致,包括包名
    • 加载这个类的 ClassLoader(指 ClassLoader 实例对象)必须相同
  • 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true

命名空间:

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

基本特征:

  • 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的
  • 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,不会在子加载器中重复加载

2.加载器

类加载器是 Java 的核心组件,用于加载字节码到 JVM 内存,得到 Class 类的对象

从 Java 虚拟机规范来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):使用 C++ 实现,是虚拟机自身的一部分
  • 自定义类加载器(User-Defined ClassLoader):Java 虚拟机规范将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器,使用 Java 语言实现,独立于虚拟机

从 Java 开发人员的角度看:

  • 启动类加载器(Bootstrap ClassLoader):
    • 处于安全考虑,Bootstrap 启动类加载器只加载包名为 java、javax、sun 等开头的类
    • 类加载器负责加载在 JAVA_HOME/jre/libsun.boot.class.path 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的类,并且是虚拟机识别的类库加载到虚拟机内存中
    • 仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在 lib 目录中也不会被加载
    • 启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果要把加载请求委派给启动类加载器,直接使用 null 代替
  • 扩展类加载器(Extension ClassLoader):
    • 由 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现,上级为 Bootstrap,显示为 null
    • JAVA_HOME/jre/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中
    • 开发者可以使用扩展类加载器,创建的 JAR 放在此目录下,会由扩展类加载器自动加载
  • 应用程序类加载器(Application ClassLoader):
    • 由 AppClassLoader(sun.misc.Launcher$AppClassLoader) 实现,上级为 Extension
    • 负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库
    • 这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此称为系统类加载器
    • 可以直接使用这个类加载器,如果应用程序中没有自定义类加载器,这个就是程序中默认的类加载器
  • 自定义类加载器:由开发人员自定义的类加载器,上级是 Application
名称加载的类(管理区域)说明
Bootstrap ClassLoader (启动类加载器)JAVA_HOME/jre/lib无法直接访问,C++代码实现
Extension ClassLoader (扩展类加载器)JAVA_HOME/jre/lib/ext上级为 Bootstrap
Application ClassLoader (应用类加载器,最常见)classpath上级为 Extension
自定义类加载器自定义上级为 Application
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@610455d6

    //获取其上层 获取不到引导类加载器
    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

}

补充两个类加载器:

  • SecureClassLoader 扩展了 ClassLoader,新增了几个与使用相关的代码源和权限定义类验证(对 class 源码的访问权限)的方法,一般不会直接跟这个类打交道,更多是与它的子类 URLClassLoader 有所关联
  • ClassLoader 是一个抽象类,很多方法是空的没有实现,而 URLClassLoader 这个实现类为这些方法提供了具体的实现,并新增了 URLClassPath 类协助取得 Class 字节流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承 URLClassLoader 类,这样就可以避免去编写 findClass() 方法及其获取字节码流的方式,使自定义类加载器编写更加简洁

2.1常用API

ClassLoader 类,是一个抽象类,其后所有的类加载器都继承自 ClassLoader(不包括启动类加载器)

获取 ClassLoader 的途径:

  • 获取当前类的 ClassLoader:clazz.getClassLoader()
  • 获取当前线程上下文的 ClassLoader:Thread.currentThread.getContextClassLoader()
  • 获取系统的 ClassLoader:ClassLoader.getSystemClassLoader()
  • 获取调用者的 ClassLoader:DriverManager.getCallerClassLoader()

ClassLoader 类常用方法:

  • getParent():返回该类加载器的超类加载器
  • loadclass(String name):加载名为 name 的类,返回结果为 Class 类的实例,该方法就是双亲委派模式
  • findclass(String name):查找二进制名称为 name 的类,返回结果为 Class 类的实例,该方法会在检查完父类加载器之后被 loadClass() 方法调用
  • findLoadedClass(String name):查找名称为 name 的已经被加载过的类,final 修饰无法重写
  • defineClass(String name, byte[] b, int off, int len):将字节流解析成 JVM 能够识别的类对象
  • resolveclass(Class<?> c):链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析
  • InputStream getResourceAsStream(String name):指定资源名称获取输入流

2.2指定加载器

用 Bootstrap 类加载器加载类:

public class F {
    static {
        System.out.println("bootstrap F init");
    }
}
public class Load10 {
    //  -XX:+TraceClassLoading
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        Class<?> aClass = Class.forName("c_04.F");
        System.out.println(aClass.getClassLoader());

    }
}

(通过JVM参数指定)

-Xbootclasspath      // 设置启动类加载的路径
java -Xbootclasspath:<new bootclasspath>
java -Xbootclasspath/a:<追加路径>
java -Xbootclasspath/p:<追加路径>
D:\StudyJava\AboutDiKnown\study-08-JVM\target\classes> 
												java -Xbootclasspath/a:. c_04.Load10

执行结果:

  • 通过JVM参数将定义的F class的路径添加到启动类路径
  • 由于启动类加载器是采用C++编写的,因此无法直接获得启动类加载器,因此输出结果为null


用 扩展类加载器加载类:

public class G {
    static {
        System.out.println("ext G init");
        //System.out.println("classpath G init");
    }
}

使用扩展类加载器需要打包成Jar ,放入 ext目录下:

D:\StudyJava\AboutDiKnown\study-08-JVM\target\classes> 
													jar -cvf my.jar c_04\G.class
/**
 * 演示 扩展类加载器
 * 在 D:\Java\jre\lib\ext 下有一个 my.jar
 * 里面也有一个 G 的类,观察到底是哪个类被加载了
 */
public class Load10_2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> aClass = Class.forName("c_04.G");
        System.out.println(aClass.getClassLoader());
    }
}



2.3加载模型

2.1加载机制

在 JVM 中,对于类加载模型提供了三种,分别为全盘加载、双亲委派、缓存机制

  • 全盘加载:当一个类加载器负责加载某个 Class 时,该 Class 所依赖和引用的其他 Class 也将由该类加载器负责载入,除非显示指定使用另外一个类加载器来载入

  • 双亲委派:先让父类加载器加载该 Class,在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。简单来说就是,某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有当父加载器无法完成此加载任务时,才自己去加载

  • 缓存机制:会保证所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区中搜寻该 Class,只有当缓存区中不存在该 Class 对象时,系统才会读取该类对应的二进制数据,并将其转换成 Class 对象存入缓冲区(方法区)中

    • 这就是修改了 Class 后,必须重新启动 JVM,程序所做的修改才会生效的原因

2.2双亲委派

双亲委派模型(Parents Delegation Model):该模型要求除了顶层的启动类加载器外,其它类加载器都要有父类加载器,这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance)

工作过程:一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载

双亲委派机制的优点:

  • 可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证全局唯一性

  • Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一

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

    例如:在工程中新建 java.lang 包,接着在该包下新建 String 类,并定义 main 函数

    public class String {
        public static void main(String[] args) {
            System.out.println("demo info");
        }
    }
    

    此时执行 main 函数,会出现异常,在类 java.lang.String 中找不到 main 方法,防止恶意篡改核心 API 库。出现该信息是因为双亲委派的机制,java.lang.String 的在启动类加载器(Bootstrap)得到加载,启动类加载器优先级更高,在核心 jre 库中有其相同名字的类文件,但该类中并没有 main 方法

双亲委派机制的缺点:检查类是否加载的委托过程是单向的,这个方式虽然从结构上看比较清晰,使各个 ClassLoader 的职责非常明确,但顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类(可见性)

2.4源码分析

所谓的双亲委派,就是指调用类加载器的 loadClass 方法时,查找类的规则

具体流程可以参考上图 ↑

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // 这里的类加载通过synchronized来保证安全性
    synchronized (getClassLoadingLock(name)) {
       // 1. 调用当前类加载器的 findLoadedClass(name),检查当前类加载器是否已加载过指定 name 的类
        Class c = findLoadedClass(name);
        
        // 当前类加载器如果没有加载过
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 判断当前类加载器是否有父类加载器
                if (parent != null) {
     			 // 2.如果当前类加载器有父类加载器,则调用父类加载器的loadClass(name,false)
                    c = parent.loadClass(name, false);
                } else {
				// 3. 如果没有上级了(ExtClassLoader),则委派 BootstrapClassLoader
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) { }

            if (c == null) {
                //4. 如果调用父类的类加载器无法对类进行加载,则用自己的 findClass() 方法进行加载
                // 可以自定义 findClass() 方法
                long t1 = System.nanoTime();
                c = findClass(name);

                // 5.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) {
            // 链接指定的 Java 类,可以使类的 Class 对象创建完成的同时也被解析
            resolveClass(c);
        }
        return c;
    }
}

假设loadclass方法首次加载应用程序类

  • 整个过程经历二次调用findclass方法和一次异常捕获(非常巧妙)
  • 初始类加载器只有在启动的时候才会加载类,在代码中只是让该加载检查自己时候已经加载过这个类,没有让他尝试加载这个类。

执行流程为:

  1. AppClassLoader //1 处, 开始查看已加载的类,结果没有
  2. AppClassLoader // 2 处,委派上级ExtClassLoader.loadClass()
  3. ExtClassLoader // 1 处,查看已加载的类,结果没有
  4. ExtClassLoader // 3 处,没有上级了,则委派 BootstrapClassLoader查找
  5. BootstrapClassLoader 是在 JAVA_HOME/jre/lib 下找 H 这个类,显然没有
  6. ExtClassLoader // 4 处,调用自己的 findClass 方法,是在JAVA_HOME/jre/lib/ext 下找 H 这个类,显然没有,同时会抛出ClassNotFoundException异常,该异常被应用类加载器给捕获,不做任何处理
  7. 继续执行到 AppClassLoader // 4 处,调用它自己的 findClass 方法,在classpath 下查找,找到了

引申问题:多线程环境下,是如何保证类加载的唯一性?

  • 双亲委派机制利用synchronized对加载类的名称作为key的object对象加锁
synchronized (getClassLoadingLock(name))    
// 这个加锁的过程用到了concurrentHashMap,key是类的名称,value是object对象,加载类的时候会对这个类的object对象进行加锁
protected Object getClassLoadingLock(String className) {
    Object lock = this;
    if (parallelLockMap != null) {
        Object newLock = new Object();
        lock = parallelLockMap.putIfAbsent(className, newLock);
        if (lock == null) {
            lock = newLock;
        }
    }
    return lock;
}
2.5线程上下文类

JDBC中的双亲委派机制的打破

实例:在使用 JDBC 时,都需要加载 Driver 驱动**,com.mysql.jdbc.Driver 是如何被正确加载的**?

Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs which currently load JDBC drivers using Class.forName() will continue to work without modification.
// DriverManger.java文件中的注解,为什么之前需要 Class.forName()加载驱动,现在不需要呢?)
public static void main(String[] args) {
    System.out.println(DriverManager.class.getClassLoader());
}

代码执行结果:

  • 从打印结果上看DriverManger是启动类加载器加载的,然而启动类加载路径JAVE_HOME/jre/lib没有JDBC的驱动,因此程序是如何加载JDBC驱动类的?
null

DriverManger.java的部分源码

step1:在静态代码块中调用loadInitialDrivers();

public class DriverManager {
    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();  
    ...
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    ...
}

step2: 调用loadInitialDrivers()方法

关键点1)使用SPI加载驱动类 2)使用应用程序类加载器加载驱动类

private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
       // 1)使用 ServiceLoader 机制加载驱动,即 SPI
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
       // 2)使用 jdbc.drivers 定义的驱动名加载驱动
        if (drivers == null || drivers.equals("")) {
            return;
        }
         /*这里就是String数组,里面每个String都是需要加载驱动类的名称*/
        String[] driversList = drivers.split(":");  
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化,关联的是应用程序类加载器,因此可以顺利完成类加载

再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

JDBC的驱动类加载总结

    1. 实际加载过程中通过启动类加载器加载DriverManger这个类,DriverManager中loadInitialDrivers()没有采用双亲委派机制去获取驱动类。
  • 2)首先先尝试采用服务提供接口(SPI)的线程上下文加载器实现对驱动类的加载,本质上是采用应用程序类加载器进行加载
  • 3)然后通过上个步骤获取的驱动的名称(drivers),再次利用应用类加载器加载驱动类。

备注:整个过程貌似是通过SPI获取到需要加载的类的类的名称,然后再调用应用程序类加载器加载(不是太确定)

2.6 SPI

定义: SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件

  • SPI的作用就是为这些被扩展的API寻找服务实现

mysql-connector-java在service目录下建立名称为java.sql.Driver的文件。

  • 文件名就是service provider interface(服务提供接口的名称)
  • 文件中的内容com.mysq.jdbc.Driver是该“服务提供接口实现类"的名称

使用SPI必须遵循上述约定

按照上述约定并将接口类实现,可通过ServiceLoader来得到实现类(如下所示),体现的是【面向接口编程+解耦】的思想

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
    while(iter.hasNext()) {
    iter.next();
}

SPI思想在其他框架的体现

JDBC
Servlet 初始化器
Spring 容器
Dubbo(对 SPI 进行了扩展)

背景SPI实现扩展功能的类加载的时候,使用了线程上下文类加载器,分析源码可以发现线程上下文类加载器就是应用程序类加载器。

线程上下文类加载器定义:当前线程使用的类加载器,默认就是应用程序类加载器 ,在每一个Thread启动的时候,JVM都会为该线程初始化一个类加载器

private ClassLoader contextClassLoader;    // Thread源码中包含有contextClassLoader这个属性

ServiceLoader.java的源码内容

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // 传入两个参数: 1)class对象 2)类的加载器
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

=========================================================================================
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 确保类加载器不为空
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;         
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);  // 创建一个发现service的迭代器
}

=========================================================================================
/* nextService方法中会获取*/
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // loader就是传递进来的线程上下文加载器,本质上就是应用程序类加载。
        c = Class.forName(cn, false, loader); 
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn  + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service, "Provider " + cn + " could not be instantiated", x);
    }
    throw new Error();          // This cannot happen
}

总结从上面的源码中看到SPI机制并没有遵循双亲委派机制去加载类即没有适用ClassLoader.loadClass方法加载类,而是通过Java的反射机制(@CallerSensitive)即使用Class.forName方法指定应用程序类加载器去加载class。


2.7类加载方法

Class.forName()加载类和使用ClassLoader.loadClass加载类的区别(重要)?

/*方式1:使用ClassLoader.loadClass进行类加载*/
Class<?> aClass1 = test13.class.getClassLoader().loadClass("part3.test13");
/*方式2:使用Class.forName进行类加载*/
Class.forName(String name, boolean initialize,ClassLoader loader)
  • 关于ClassLoader.loadClass

    • 采用这种方式加载严格遵照双亲委派机制进行类的加载,对于大部分类加载的情况这个方法是适用的。
  • 关于Class.forName()

    • 有三个参数:
    • 1)加载类的名称
    • 2)是否进行类的初始化
    • 3)用于加载类的类加载器
  • 可以看出Class.forName这个方法加载类更加的灵活,不要遵循双亲委派机制,可以自由的去加载类。

    (打破了双亲委派机制)

Class.java文件中Class.forName的源码

@CallerSensitive
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)
    throws ClassNotFoundException{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Reflective call to get caller class is only needed if a security manager
        // is present.  Avoid the overhead of making this call otherwise.
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}

2.8破坏委派

双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者的类加载器实现方式

破坏双亲委派模型的方式:

  • 自定义 ClassLoader

    • 如果不想破坏双亲委派模型,只需要重写 findClass 方法
    • 如果想要去破坏双亲委派模型,需要去**重写 loadClass **方法
  • 引入线程上下文类加载器

    Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI 等。这些 SPI 接口由 Java 核心库来提供,而 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径 classpath 里,SPI 接口中的代码需要加载具体的实现类:

    • SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的
    • SPI 的实现类是由系统类加载器加载,引导类加载器是无法找到 SPI 的实现类,因为双亲委派模型中 BootstrapClassloader 无法委派 AppClassLoader 来加载类

    JDK 开发人员引入了线程上下文类加载器(Thread Context ClassLoader),这种类加载器可以通过 Thread 类的 setContextClassLoader 方法进行设置线程上下文类加载器,在执行线程中抛弃双亲委派加载模式,使程序可以逆向使用类加载器,使 Bootstrap 加载器拿到了 Application 加载器加载的类,破坏了双亲委派模型

  • 实现程序的动态性,如代码热替换(Hot Swap)、模块热部署(Hot Deployment)

    IBM 公司主导的 JSR一291(OSGiR4.2)实现模块化热部署的关键是它自定义的类加载器机制的实现,每一个程序模块(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. 否则,类查找失败

    热替换是指在程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为,热替换的关键需求在于服务不能中断,修改必须立即表现正在运行的系统之中


2.3沙箱机制

沙箱机制(Sandbox):将 Java 代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问,来保证对代码的有效隔离,防止对本地系统造成破坏

沙箱限制系统资源访问,包括 CPU、内存、文件系统、网络,不同级别的沙箱对资源访问的限制也不一样

  • JDK1.0:Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码被看作是不受信的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的远程代码不可以访问本地资源,其实依赖于沙箱机制。如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现
  • JDK1.1:针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限
  • JDK1.2:改进了安全机制,增加了代码签名,不论本地代码或是远程代码都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制
  • JDK1.6:当前最新的安全机制,引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,不同的保护域对应不一样的权限。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问

2.4自定义加载器

对于自定义类加载器的实现,只需要继承 ClassLoader 类,覆写 findClass 方法即可

作用:

  • 1)想加载非 classpath 随意路径中的类文件

  • 2)都是通过接口来使用实现,希望解耦时,常用在框架设计

  • 3)这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器

步骤:

1. 继承 ClassLoader 父类
2. 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass方法,否则不会走双亲委派机制)
3. 读取类文件的字节码
4. 调用父类的 defineClass 方法来加载类
5. 使用者调用该类加载器的 loadClass 方法

示例:

编译生成 class文件

public class MapImpl1 {
    static {
        System.out.println("MapImpl1");
    }
}
public class MapImpl2 {
    static {
        System.out.println("MapImpl2");
    }
}

自定义类加载器,加载的路径为class文件的文件夹

/**
 * @date  自定义类加载器
 * 1. 继承 ClassLoader 父类
 */
class MyClassLoader extends ClassLoader {
    /**
     * 2. 要遵从双亲委派机制,重写 findClass 方法(不是重写 loadClass方法,否则不会走双亲委派机制)
     */
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        //3.确定加载路径
        String path = "D:\\StudyJava\\AboutDiKnown\\study-08-JVM\\target\\classes\\" + name +".class";
        try {
            //4. 读取类文件的字节码
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Files.copy(Paths.get(path),os);
            //5.字节码文件转二进制数组
            byte[] bytes = os.toByteArray();
            //6. 调用父类的 defineClass 方法来加载类
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到", e);
        }
    }
}

同一个类对象的需要同名、同包、通加载器加载

public class Load7 {
    public static void main(String[] args) throws Exception  {
        //6. 使用者调用该类加载器的 loadClass 方法
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3);

        c1.newInstance();
    }
}

2.5JDK9

为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动:

  • 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取

  • JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 <JAVA_HOME>\lib\ext 目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制就不需要再存在

  • 启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader


参考连接

参考黑马一

{
//4. 读取类文件的字节码
ByteArrayOutputStream os = new ByteArrayOutputStream();
Files.copy(Paths.get(path),os);
//5.字节码文件转二进制数组
byte[] bytes = os.toByteArray();
//6. 调用父类的 defineClass 方法来加载类
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(“类文件未找到”, e);
}
}
}


同一个类对象的需要同名、同包、通加载器加载

```java
public class Load7 {
    public static void main(String[] args) throws Exception  {
        //6. 使用者调用该类加载器的 loadClass 方法
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> c1 = classLoader.loadClass("MapImpl1");
        Class<?> c2 = classLoader.loadClass("MapImpl1");
        System.out.println(c1 == c2);

        MyClassLoader classLoader2 = new MyClassLoader();
        Class<?> c3 = classLoader2.loadClass("MapImpl1");
        System.out.println(c1 == c3);

        c1.newInstance();
    }
}

2.5JDK9

为了保证兼容性,JDK9 没有改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行做了一些变动:

  • 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform classloader),可以通过 ClassLoader 的新方法 getPlatformClassLoader() 来获取

  • JDK9 基于模块化进行构建(原来的 rt.jar 和 tools.jar 被拆分成数个 JMOD 文件),其中 Java 类库就满足了可扩展的需求,那就无须再保留 <JAVA_HOME>\lib\ext 目录,此前使用这个目录或者 java.ext.dirs 系统变量来扩展 JDK 功能的机制就不需要再存在

  • 启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk.internal.loader.BuiltinClassLoader


参考连接

参考黑马一

参考黑马二

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值