类的加载过程

类的生命周期概述

1、Java 数据类型

(1)基本数据类型:由虚拟机预先定义

(2)引用数据类型:需要进行类的加载

2、Java 虚拟机规范,从 .class 文件到加载到内存中的类,到类卸载出内存为止,它的整个生命周期包括 7 个阶段

3、类的加载过程

(1)加载

(2)链接:验证 -> 准备 -> 解析

(3)初始化

(4)使用

(5)卸载

4、加载、验证、准备、初始化、卸载这五个阶段的顺序是确定的

(1)类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定

(2)解析在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 语言的运行时绑定特性(也称为动态绑定或晚期绑定)

(3)这些阶段通常都是互相交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段

 

加载阶段(Loading)

1、概述

(1)将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型:类模板对象

(2)类模板对象:Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,JVM 在运行期通过类模板,获取 Java 类中的任意信息,对 Java 类的成员变量进行遍历,进行 Java 方法的调用

(3)如果 JVM 没有存储 Java 类的声明信息,则 JVM 在运行期无法反射

2、JVM 查找并加载类的二进制数据,生成 Class 实例,必须完成 3 个操作

(1)通过类的全名,获取类的二进制数据流

(2)解析类的二进制数据流为方法区内的数据结构(Java 类模型)

(3)创建 java.lang.Class 类的实例,表示该类型,作为方法区这个类的各种数据的访问入口

3、加载阶段结束后,JVM 外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的数据存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体数据结构

4、类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口

5、加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序

6、关于在什么情况下需要开始类加载过程的第一个阶段“加载”,《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握

 

类的二进制数据流

1、虚拟机可以通过多种途径产生或获得,只要所读取的字节码符合 JVM 规范即可

2、获取方式

(1)虚拟机可能通过文件系统读入一个 .class 后缀的文件

(2)读入 jar、zip 等归档数据包,提取类文件

(3)事先存放在数据库中的类的二进制数据

(4)使用类似于 HTTP 之类的协议通过网络进行加载

(5)在运行时,生成一段 .class 的二进制信息等

3、在获取到类的二进制信息后,JVM 就会处理这些数据,并最终转为一个 java.lang.Class 的实例

4、如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError

 

类模型的位置

1、加载的类在 JVM 中创建相应的类结构

2、类结构会存储在方法区

(1)JDK 1.8 之前:永久代

(2)JDK 1.8 及之后:元空间

 

Class 实例的位置

1、类将 .class 文件加载至元空间后,在堆中创建一个 Java.lang.Class 对象,封装类位于方法区(元空间)内的数据结构

2、该 Class 对象是在加载类的过程中创建,每个类都对应有一个 Class 类型的对象

 

数组类的加载

1、数组类不是由类加载器负责创建

(1)是由 JVM 在运行时,根据需要而直接创建

(2)数组的元素类型(Element Type,指的是数组去掉所有维度的类型),需要依靠类加载器创建

2、一个数组类(下面简称为 C)创建过程遵循以下规则

(1)如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组 C 将被标识在加载该组件类型的类加载器的类名称空间上

(2)如果数组的组件类型不是引用类型(例如 int[] 数组的组件类型为 int),JVM 将会把数组 C 标记为与引导类加载器关联

(3)数组类的可访问性与它的组件类型的可访问性一致,如果组件类型不是引用类型,它的数组类的可访问性将默认为 public,可被所有的类和接口访问到

 

链接阶段:验证(Verification)

1、目的

(1)保证加载的字节码是合法、合理、符合规范

(2)链接阶段的验证虽然拖慢加载速度,但是它避免在字节码运行时还需要进行各种检查

(3)验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了

(4)如果程序运行的全部代码(包括自己编写的、第三方包中的、从外部加载的、动态生成的等所有代码)都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

2、验证的内容:类数据信息的格式验证、语义检查、字节码验证、符号引用验证等

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

(2)格式验证之外的验证操作将会在方法区中进行

3、文件格式验证

(1)验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理

是否以魔数 0XCAFEBABE 开头
主版本、副版本号是否在当前 JVM 支持范围内
数据中每一个项是否都拥有正确的长度
常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)
指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
……

(2)该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求

(3)这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才被允许进入 JVM 内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了

4、语义检查

(1)对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求

这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
这个类的父类是否继承了不允许被继承的类(被final修饰的类)
如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)
……

(2)目的:对类的元数据信息进行语义校验,保证不存在与《Java语言规范》定义相悖的元数据信息

5、字节码验证

(1)目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的

(2)在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为

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

6、栈映射帧(StackMapTable)

(1)在字节码验证阶段,用于检测在特定的字节码处,其局部变量表、操作数栈是否有着正确的数据类型

(2)但无法 100% 判断一段字节码,是否可以被安全执行,因此,该过程只是尽可能地检查出可以预知的明显的问题

(3)如果在字节码验证阶段无法通过检查,虚拟机也不会正确装载这个类,但是,如果通过了这个阶段的检查,不能说明这个类是完全没有问题

(4)在前面 3 次检查中,已经排除文件格式错误、语义错误、字节码的不正确性,但依然不能确保类没有问题

(3)如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的;但如果一个方法体通过了字节码验证,也仍然不能保证它一定就是安全的。即使字节码验证阶段中进行了再大量、再严密的检查,也依然不能保证这一点

(4)停机问题:即不能通过程序准确地检查出程序是否能在有限的时间之内结束运行。在讨论字节码校验的上下文语境里,通俗一点的解释是通过程序去校验程序逻辑是无法做到绝对准确的,不可能用程序来准确判定一段程序是否存在 Bug

(5)由于数据流分析和控制流分析的高度复杂性,JVM 的设计团队为了避免过多的执行时间消耗在字节码验证阶段中,在 JDK 6之后的 javac 编译器和 java 虚拟机里进行了一项联合优化,把尽可能多的校验辅助措施挪到 javac 编译器里进行

(6)具体做法:给方法体 Code 属性的属性表中新增加了一项名为 StackMapTable 的新属性,这项属性描述了方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,JVM 就不需要根据程序推导这些状态的合法性,只需要检查 StackMapTable 属性中的记录是否合法即可,这样就将字节码验证的类型推导转变为类型检查,从而节省了大量校验时间

(7)理论上 StackMapTable 属性也存在错误或被篡改的可能,所以是否有可能在恶意篡改了 Code 属性的同时,也生成相应的 StackMapTable 属性来骗过虚拟机的类型校验,则是虚拟机设计者们需要仔细思考的问题

7、符号引用的验证

(1)发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生

(2)符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源

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

(3)主要目的是确保解析行为能正常执行

(4)Class 文件在其常量池会通过字符串,记录将要使用的其他类或方法,在验证阶段,JVM 会检查这些类或方法确实存在,并且当前类有权限访问这些数据

(4)如果无法通过符号引用验证,Java虚拟机将会抛出一个 java.lang.IncompatibleClassChangeError 的子类异常,典型的如:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError、java.lang.NoClassDefFoundError 等

 

链接阶段:准备(Preparation)

1、为类的静态变量分配内存,并将其初始化为默认值

2、在这个阶段,虚拟机会为这个类分配相应的内存空间,并设置默认初始值

类型 默认初始值
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0
char \u0000
boolean false
reference null

(1)Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是 0,对应 boolean 的默认值就是 false

3、不包含基本数据类型的字段用 static final 修饰的情况,因为 final 在编译的时就已经分配,准备阶段会显式赋值

(1)一般情况:static final 修饰的基本数据类型、字符串类型字面量会在准备阶段赋值

(2)特殊情况:static final 修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值

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

5、该阶段不会像初始化阶段,会有初始化或代码被执行

 

链接阶段:解析(Resolution)

1、将常量池内的类、接口、字段、方法的符号引用,转为直接引用

2、符号引用(Symbolic References)

(1)符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可

(2)符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容

(3)各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的 Class 文件格式中

3、直接引用(Direct References)

(1)直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄

(2)直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同

(3)如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在

4、解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这 7 类符号引用进行,分别对应于常量池的 CONSTANT_Class_info、CON-STANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dyna-mic_info、CONSTANT_InvokeDynamic_info 8 种常量类型

5、类或接口的解析

(1)假设当前代码所处的类为 D,如果要把一个从未解析过的符号引用 N 解析为一个类或接口 C 的直接引用,那虚拟机完成整个解析的过程需要包括以下 3 个步骤

(2)如果 C 不是一个数组类型,那虚拟机将会把代表 N 的全限定名传递给 D 的类加载器去加载这个类 C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就将宣告失败

(3)如果 C 是一个数组类型,并且数组的元素类型为对象,也就是 N 的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第一点的规则加载数组元素类型。如果 N 的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表该数组维度和元素的数组对象

(4)如果上面两步没有出现任何异常,那么 C 在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成前还要进行符号引用验证,确认 D 是否具备对 C 的访问权限。如果发现不具备访问权限,将抛出 java.lang.IllegalAccessError 异常

6、针对上面第 3 点访问权限验证,在 JDK 9 引入了模块化以后,一个 public 类型也不再意味着程序任何位置都有它的访问权限,还必须检查模块间的访问权限

(1)如果说一个 D 拥有 C 的访问权限,那就意味着以下 3 条规则中至少有其中一条成立

(2)被访问类 C 是 public 的,并且与访问类 D 处于同一个模块

(3)被访问类 C 是 public 的,不与访问类 D 处于同一个模块,但是被访问类 C 的模块允许被访问类 D 的模块进行访问

(4)被访问类 C 不是 public 的,但是它与访问类 D处 于同一个包中

(5)在后续涉及可访问性时,都必须考虑模块间访问权限隔离的约束,即以上列举的 3 条规则

7、字段解析

(1)要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_index 项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用

(2)如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败

(3)如果解析成功完成,那把这个字段所属的类或接口用 C 表示,《Java虚拟机规范》要求按照如下步骤对 C 进行后续字段的搜索

(4)如果 C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

(5)否则,如果在 C 中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

(6)否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束

(7)否则,查找失败,抛出 java.lang.NoSuchFieldError 异常

(8)如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出 java.lang.IllegalAccessError 异常

(9)以上解析规则能够确保 JVM 获得字段唯一的解析结果,但在实际情况中,Javac 编译器往往会采取比上述规范更加严格一些的约束,譬如有一个同名字段同时出现在某个类的接口和父类当中,或者同时在自己或父类的多个接口中出现,按照解析规则仍是可以确定唯一的访问字段,但 Javac 编译器就可能直接拒绝其编译为 Class 文件

8、方法解析

(1)先解析出方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,那么用 C 表示这个类,接下来虚拟机将会按照如下步骤进行后续的方法搜索

(2)由于 Class 文件格式中类的方法和接口的方法符号引用的常量类型定义是分开的,如果在类的方法表中发现 class_index 中索引的 C 是个接口的话,那就直接抛出 java.lang.IncompatibleClassChangeError 异常

(3)如果通过了(2),在类 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束

(4)否则,在类 C 的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束

(5)否则,在类 C 实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类 C 是一个抽象类,这时候查找结束,抛出 java.lang.AbstractMethodError 异常

(6)否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError

(7)最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出 java.lang.IllegalAccessError 异常

9、接口方法解析

(1)先解析出接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索

(2)与类的方法解析相反,如果在接口方法表中发现 class_index 中的索引 C 是个类而不是接口,那么就直接抛出 java.lang.IncompatibleClassChangeError 异常

(3)否则,在接口 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束

(4)否则,在接口 C 的父接口中递归查找,直到 java.lang.Object 类(接口方法的查找范围也会包括 Object 类中的方法)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束

(5)对于(4),由于 Java 的接口允许多重继承,如果 C 的不同父接口中存有多个简单名称和描述符都与目标相匹配的方法,那将会从这多个方法中返回其中一个并结束查找,《Java虚拟机规范》中并没有进一步规则约束应该返回哪一个接口方法。但与之前字段查找类似地,不同发行商实现的 Javac 编译器有可能会按照更严格的约束拒绝编译这种代码来避免不确定性

(6)否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError 异常

(7)在 JDK 9 之前,Java 接口中的所有方法都默认是 public 的,也没有模块化的访问约束,所以不存在访问权限的问题,接口方法的符号解析就不可能抛出 java.lang.IllegalAccessError 异常

(8)在 JDK 9 中增加了接口的静态私有方法,也有了模块化的访问约束,所以从 JDK 9 起,接口方法的访问也完全有可能因访问权限控制而出现 java.lang.IllegalAccessError 异常

 

初始化阶段(Initialization)

1、类的初始化是类装载的最后一个阶段

(1)如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中

(2)初始化阶段,才真正开始执行类中定义的 Java 程序代码

2、执行类的初始化方法:<clinit>()

(1)该方法仅能由 Java 编译器生成,并由 JVM 调用

(2)程序开发者无法自定义一个同名的方法,更无法直接在 Java 程序中调用该方法,即使该方法也是由字节码指令所组成

(3)由类静态成员的赋值语句,以及 static 语句块合并产生的

(4)编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

3、在加载一个类之前,虚拟机总是会试图加载该类的父类

(1)父类 <clinit>() 总是在子类 <clinit>() 之前被调用

(2)即父类 static 语句块优先级高于子类

(3)Java虚拟机会保证在子类的 <clinit>() 方法执行前,父类的 <clinit>() 方法已经执行 完毕。因此在 JVM 中第一个被执行的()方法的类型肯定是 java.lang.Object

4、Java 编译器并不会为所有的类都产生 <clinit>() 初始化方法

(1)一个类中并没有声明任何的类变量,也没有静态代码块时,字节码文件中将不会包含 <clinit>()

(2)一个类中声明类变量,但是没有明确使用类变量的初始化语句,以及静态代码块来执行初始化操作时,字节码文件中将不会包含 <clinit>()

(3)一个类中包含 static final 修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式,字节码文件中将不会包含 <clinit>()

5、接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法

(1)接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的()方法, 因为只有当父接口中定义的变量被使用时,父接口才会被初始化

(2)此外,接口的实现类在初始化时也 一样不会执行接口的 <clinit>() 方法

6、JVM 必须保证一个类的 <clinit>() 方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的 <clinit>() 方法,其他线程都需要阻塞等待,直到活动线程执行完毕 <clinit>() 方法

 

使用 static final 修饰

1、赋值阶段

(1)情况一:在链接阶段的准备环节赋值

(2)情况二:在初始化阶段 <clinit>() 中赋值

2、 在链接阶段的准备环节

(1)基本数据类型的字段,使用显式赋值(直接赋值常量,而非调用方法)

(2)String,如果使用字面量的方式赋值,即 ""

3、在初始化阶段 <clinit>() 中赋值: 排除 2 之外的情况

 

<clinit>() 线程安全性

1、对于 <clinit>() 方法的调用(类的初始化)

(1)虚拟机会保证一个类的 <clinit>() 在多线程环境中被正确地加锁、同步

(2)如果多个线程同时去初始化一个类,则只会有一个线程去执行这个类的 <clinit>(),其他线程都需要阻塞等待,直到活动线程执行 <clinit>() 完毕

(3)如果之前的线程成功加载了类,则等在队列中的线程,就没有机会再执行 <clinit>(),当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息

2、<clinit>() 带锁线程安全

(1)如果在一个类的 <clinit>() 中有耗时很长的操作,就可能造成多个线程阻塞,引发死锁

(2)这种死锁难以发现,因为它们并没有可用的锁信息

 

类的初始化情况

1、Java 程序对类的使用

(1)主动使用

(2)被动使用

2、初始化指调用类的 <clinit>()

 

主动使用

1、Class 只有在必须要首次使用时才会被装载,JVM 不会无条件地装载 Class 类型

2、JVM 规定,一个类或接口在初次使用(主动使用)前,必须要进行初始化

3、实例化

(1)当创建一个类的实例时

(2)比如:使用 new 关键字,通过反射、克隆、反序列化

4、静态方法

(1)当调用类的静态方法时

(2)底层:使用字节码 invokestatic 指令

5、静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)

(1)当使用类、接口的静态字段时

(2)比如:使用 getstatic、putstatic 指令(对应访问变量、赋值变量操作)

6、反射:使用 java.lang.reflect 包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化

7、继承

(1)当初始化子类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化

(2)当 JVM 初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口

(3)在初始化一个类时,并不会先初始化它所实现的接口

(4)在初始化一个接口时,并不会先初始化它的父接口

(5)一个父接口并不会因为它的子接口,或实现类的初始化而初始化

(6)只有当程序首次使用特定接口的静态字段时,才会导致该接口的初始化

8、default 方法:若一个接口定义 default 方法,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化

9、main 方法

(1)当虚拟机启动时,用户需要指定一个要执行的主类,即包含 main(String[]) 的类,虚拟机会先初始化这个主类

(2)JVM 启动时,通过引导类加载器加载一个初始类

(3)初始类在调用 public static void main(String[]) 之前,被链接和初始化

(4)public static void main(String[]) 的执行,将依次导致所需的类的加载、链接、初始化

10、MethodHandle

(1)当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类

(2)当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化

 

被动使用

1、除以上主动使用的情况,其他的情况均属于被动使用

2、被动使用不会引起类的初始化

(1)不是在代码中出现的类,就一定会被加载或初始化

(2)如果不符合主动使用的条件,类就不会初始化

3、父类 static 字段 / 方法

(1)当通过子类调用父类的 static 字段 / 方法,不会导致子类初始化

(2)只有真正声明这个字段的类才会被初始化

4、数组定义:通过数组定义类引用,不会触发此类的初始化

public class Test {
    public static void main(String[] args) {
        Object[] object = new Object[10];
    }
}

5、常量

(1)常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

(2)引用 static final 修饰的基本数据类型的字段,使用显式赋值(直接赋值常量,而非调用方法)

(3)引用 static final 修饰的 String,使用字面量的方式赋值,即 ""

(4)引用 static final 修饰的非基础数据类型、非 String 类型的变量

(5)因为在类加载的链接的准备阶段:static final 修饰的基本数据类型、字符串类型字面量会在准备阶段赋值;static final 修饰的引用类型不会在准备阶段赋值,而是在初始化阶段赋值

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

 

类的卸载(Unloading)

1、类、类的加载器、类的实例之间的引用关系

(1)在类加载器的内部实现中,用一个 Java 集合存放所加载类的引用

(2)一个 Class 对象总是会引用它的类加载器

(3)调用 Class 对象的 getClassLoader(),就能获得它的类加载器

(4)某个类的 Class 对象与其类的加载器之间为双向关联

(5)一个类的实例总是引用代表这个类的 Class 对象

(6)在 Object 类中定义 getClass(),返回代表对象所属类的 Class 对象的引用

(7)所有 Java 类都有一个静态属性 class,它引用代表这个类的 Class 对象

(8)关系:类的实例对象 -> 类的 Class 对象 <-> 类的 ClassLoader 对象

2、类的生命周期

(1)当类被加载、链接、初始化后,它的生命周期就开始

(2)当代表类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期,该类在方法区内的数据也会被卸载,从而结束类的生命周期

(3)一个类结束生命周期的时机,取决于代表它的 Class 对象生命周期的时机

3、类的卸载

(1)启动类加载器所加载的类型,在整个运行期间,是不可能被卸载的

(2)系统类加载器、扩展类加载器,所加载的类型,在运行期间,不太可能被卸载,因为系统类加载器实例、扩展类加载器实例,基本上在整个运行期间,总能直接或间接的访问到,不可到达的可能性极小

(3)开发者自定义的类加载器实例,所加载的类型,只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到

(4)复杂应用场景中,比如:用户在开发自定义类加载器实例时,采用缓存的策略以提高系统性能,被加载的类型在运行期间,几乎不可能被卸载,至少卸载的时间是不确定的

(5)综述:一个已经加载的类型,被卸载的几率很小,至少被卸载的时间是不确定的,同时,在开发时,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能

 

方法区的垃圾回收

1、常量池中废弃的常量、不再使用的类型

2、HotSpot 虚拟机对常量池的回收策略:只要常量池中的常量没有被任何地方引用,就可以被回收

3、不再使用的类,需要同时满足以下三个条件

(1)该类所有的实例都已经被回收,即 Java 堆中不存在该类及其任何派生子类的实例

(2)加载该类的类加载器已经被回收,除非是经过设计的可替换类加载器的场景,如:OSGi、JSP 重加载等,否则很难达成

(3)该类对应 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

4、JVM 被允许对满足上述三个条件的无用类进行回收,并不是和对象一样,没有引用就必然回收

posted @   半条咸鱼  阅读(711)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示