你未必出类拔萃,但一定与众不同
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机使用的Java类型。
类型的加载,连接和初始化都是在程序运行期间完成
一个类型(每个Class文件都代表Java语言中的一个类或者接口)从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载,验证,准备,解析,初始化,使用和卸载七个阶段,其中验证,准备,解析三个部分统称为连接
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cyWfPfqW-1623851784053)(C:\Users\宇文拓
《Java虚拟机》中严格规定了有且只有六种情况必须立即对类进行初始化
遇到new,getstatic,putstatic,invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段,能够生成这四条指令的典型Java代码场景
使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要触发初始化
当初始化类的时候,如果发现其父类还没有初始化,则需要先触发其父类的初始化
当虚拟机启动的时候,用户需要指定一个执行的主类 虚拟机会初始化这个主类
当使用JDK7引入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_putStatic,
REF_invokeStatic,REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要触发其初始化
当一个接口定义了默认方法(被default修饰的方法)如果有这个接口的实现类 发生了初始化,那该接口要在其之前被初始化。
对于以上六种的情况《Java虚拟机》使用了一个限定语,有且只有。这六种场景的行为成为对一个类型进行主动应用,除此之外,所有引用类型都不会触发初始化,称为被动引用。
public class Demo6161547 {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static {
System.out.println("SubClass init");
}
}
输出结果
SuperClass init
123
上述代码只会输出SuperClass init 不会输出SubClass init 对于静态字段,只有直接定义这个字段的类才会被初始化
因此通过其子类的来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化,但是会导致子类被加载
public class Demo6161547 {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
复用上方代码,这里并没有输出SuperClass init 说明没有触发com.bluedot.test616.SuperClass的初始化阶段
但是却触发了另一个为[Lcom.bluedot.test616.SuperClass]的类的初始化,这不是一个合法的类型名称,是一个虚拟机自动生成的,直接继承与java.lang.Object的子类,创建动作由newarray触发,代表了一个元素类型为com.bluedot.test616.SuperClass的一维数组,数组中应该有的属性和方法都实现在这个类里。
public class Demo6161547 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLO);
}
}
class ConstClass{
static {
System.out.println("ConstClass init");
}
public final static String HELLO = "HELLO";
}
以上代码运行也没有ConstClass init出现,这是因为虽然在Java源码里确实引用了常量HELLO,但是其实在编译期间已经通过常量的传播将常量HELLO存在了Demo6161547的常量池之中 也就是说Demo6161547对常量ConstClass.HELLO的引用 都被转换为Demo6161547对自身常量池的引用了
加载----验证----准备-----解析----初始化
加载阶段是类加载过程中的一个阶段,在加载阶段虚拟机完成三件事情
对比与类加载过程的其他过程,非数组类型的加载阶段,是开发人员可控性最强的阶段,加载阶段即可通过引导类加载器来完成,也可以通过用户自定义的类加载器去完成,根据自己的想法来赋予应用程序获取运行代码的动态性
对于数组类而言,情况有些不同,数组类不通过类加载器创建,而是虚拟机直接在内存中动态构造而成
一个数组类创建过程需要遵循以下规则:
验证阶段主要完成四个阶段的校验当做
这一阶段主要是验证字节流是否符合Class文件格式的规范,并且能被虚拟机处理
这阶段是基于二进制字节流进行
这阶段是对字节码描述的信息进行语义分析
作为验证阶段最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的,符合逻辑的,第二阶段对元数据信息中的数据类型校验完毕,这阶段就对类的方法体进行校验分析
如果一个类型中有方法体的字节码没有通过字节码验证,那么肯定有问题,但是如果一个方法体通过字节码验证,就不能保证是一定安全。和布隆过滤器性质有一丝相同,布隆说不存在一定不存在,说存在的不一定存在
最后一个阶段的校验行为发生在虚拟机将符号引用转为直接引用,这个转换发生在连接的第三个阶段,解析阶段
符号引用验证 主要是对类自身以外的各类信息进行匹配性校验,也就是该类是否缺少或者被禁止访问它依赖的某些类,方法,字段等资源
通常负责校验以下内容
符号引用主要是保证解析行为能否正常执行,无法通过符号访问验证,就会抛出一个
IncompatibleClassChangeError的子类异常
准备阶段主要是为类中定义的变量(static修饰的变量)分配内存并且设置变量初始值的阶段
基本数据类型的零值
数据类型 | 零值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
char | ‘\u000’ |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
在准备阶段对静态变量进行的赋值都是赋零值,真正赋值需要等初始化阶段
而final修饰的变量在这个阶段则是直接赋初值
将符号引用替换为直接引用的过程
解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符这7类符号引用进行
假设当前代码所处的类为 D,如果要把一个从未解析过的符号引用 N 解析为一个类或接口 C 的直接引用,那虚拟机完成整个解析的过程需要以下三个步骤:
也就是假使一个类
public class Demo6161547 {
public static void main(String[] args) {
SuperClass superClass = new SubClass();
}
}
class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
当我在主函数中new 了一个superClass出来,main的类加载器就会去加载SuperClass这个类,如果这个类有父类或者实现了接口,那么就会去加载他的父类或者接口,没有出现任何问题的话,这个类在虚拟机就存在了,但是在解析完成前还要进行符号引用验证,确认 D 是否具备对 C 的访问权限。如果多次 new SubClass的话,解析不会重复进行,因为在运行时常量池中将这个符号的直接引用记录下来,并把常量标识为已解析状态。
要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_index 项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用 C 表示,虚拟机规范要求按照如下步骤对 C 进行后续字段的搜索。
public class FieldResolution {
interface Interface0{
int A = 0;
}
interface Interface1 extends Interface0{
int A = 1;
}
interface Interface2{
int A = 2;
}
static class Parent implements Interface1{
public static int A = 3;
}
static class Sub extends Parent implements Interface2{
public static int A = 4;
}
public static void main(String[] arg){
System.out.println(Sub.A);
}
}
方法解析的第一个步骤与字段解析一样,也需要先解析出类防范表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用 C 表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索
类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现 class_index 中索引的 C 是个接口,那就直接抛出 java.lang.IncompatibleClassChangeError 异常
如果通过了第 1 步,在类 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的引用,查找结束
否则,在类 C 实现的接口列表及他们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的引用,查找结束
否则,在类 C 实现的接口列表及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,证明 C 是一个抽象类,这时查找结束,抛出 java.lang.AbstractMethodError 异常
否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError异常
最后,如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出 java.lang,IllegalAccessError异常
接口方法也需要先解析出接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用 C 表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索。
与类方法解析不同,如果在接口方法表中发现 class_index 项中的索引 C 是个类而不是接口,那就直接抛出 java.lang.IncompatibleClassChangeError异常
否则,在接口 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
否则,在接口 C 的父接口中递归查找,直到 java.lang.Object 类(查找范围会包括 Object 类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError异常
由于接口中的所有方法默认都是 public 的,所以不存在访问权限的问题,因此接口方法的符号解析不会抛出 java.lang,IllegalAccessError异常
类的初始化是类加载过程的最后一个步骤,直到初始化过程,虚拟机才会开始真正执行类中编写的Java程序代码
1.编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,而定义在它之后的变量,在前面的静态语句块可以赋值,但不能访问
public class Test {
static {
i = 0; //给变量赋值可以正常编译通过
System.out.print(i); //编译器会提示“非法向前引用”
}
static int i = 1;
}
2.初始化方法执行的顺序,虚拟机会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕,因此在虚拟机中第一个被执行的类初始化方法一定是java.lang.Object。另外,也意味着父类中定义的静态语句块要优先于子类的变量赋值操作
public class Demo6161547 {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static {
System.out.println("SubClass init");
}
}
3.clinit ()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成clinit()方法。
4.接口中不能使用静态语句块,但仍然有变量初始化的操作,因此接口与类一样都会生成clinit()方法,但与类不同的是,执行接口的初始化方法之前,不需要先执行父接口的初始化方法。只有当父接口中定义的变量使用时,才会执行父接口的初始化方法。另外,接口的实现类在初始化时也一样不会执行接口的clinit()方法。
5.虚拟机必须保证一个类的clinit()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的clinit()方法,其他线程都需要阻塞等待,直到活动线程执行类初始化方法完毕。