欢迎关注头条号:java小马哥
周一至周日早九点半!下午三点半!精品技术文章准时送上!!!
精品学习资料获取通道,参见文末
![1192b62f398b7cc937ab52ac141fa8fe.png](https://i-blog.csdnimg.cn/blog_migrate/5a48ac8c12c7262f4acf9b4afefa7b1a.jpeg)
类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。
![23628aedfd52dc914bc97efcb8eb6320.png](https://i-blog.csdnimg.cn/blog_migrate/cf2a029e849716b4753a9217482fcafa.jpeg)
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,然而,解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定和晚期绑定)。
验证、准备和解析这3个阶段又可以称为连接阶段。
一、加载:
在加载阶段,虚拟机需要完成以下3件事情:
1、通过一个类的全限定名来获取定义此类的二进制字节流;(这个二进制字节流可以从任何地方获取)
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容。
二、验证:
验证是连接阶段的第一步,验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;
验证阶段大致上完成 文件格式验证、元数据验证、字节码验证和符号引用验证 4个检验动作;
1、文件格式验证:这一阶段验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理;
验证点包括:是否以魔数0xCAFeBABE开头、主次版本号是否在当前虚拟机处理范围之内 等等;
2、元数据验证:这一阶段是对字节码描述的信息进行语义校验,保证不存在不符合Java语言规范的元数据信息;
验证点包括:这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类)、这个类的父类是否继承了不允许被继承的类(被final类修饰的类)、如果这个类不是抽象类,是否实现了其父类或者接口之中要求实现的所有方法 等等;
3、字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
验证点包括:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现这样的情况:在操作数栈放置了一个int'类型的数据,使用时却按照long类型来加载到本地变量表中;保证跳转指令不会跳转到方法体以外的字节码指令上 等等;
4、符号引用验证:这个验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段-----解析阶段中发生,符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
目的是确保解析动作能正常执行。
验证点包括:符号引用中通过字符串描述的全限定名是否能够找到对应的类;在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段;符合引用中的类、字段、方法的访问性是否可以被访问 等等;
三、准备:
准备阶段是正式为类变量(被static修饰的变量)分配内存并设置类型变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配(实例变量将会在对象实例化时随着对象一起分配在Java堆中)。
类型变量初始值:是一个数据类型的零值,比如 public static int value = 123; 那变量 value 在准备阶段过后的初始化值是0而不是123,因为这个时候尚未开始执行Java 方法,而把 value 赋值为123 的 putstatic 指令是程序被编译后存放于类构造器 () 方法之中,所以把 value 赋值为 123 的动作将在初始化阶段才会执行。
但是,如果一个类变量被 final 修饰,如 public static final int value = 123; 那么准备阶段过后 value 的值是123。
四、解析:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量,只要使用时无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。
虚拟机规范没有规定解析阶段发生的具体时间,只是要求在执行 anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarry、newputfield 和 putstatic 这16个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析。
除 invokedynamic 指令之外,虚拟机实现可以对第一次解析的结果进行缓存,从而避免解析动作重复进行。无论是否真正执行了多次解析动作,虚拟机需要保证在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样的,如果第一次解析失败,那么么其他指令对这个符号的解析请求也应该收到相同的异常。
解析动作主要针对类或者接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)、方法类型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)和调用点限定符(CONSTANT_InvokeDynamic_info) 7类符号引用进行。
1、类或接口的解析:假如当前代码所处的类为D,要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么虚拟机完成整个解析过程需要3个步骤:
1)、如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器区加载这个类C。
2)、如果C是一个数组类型,并且数组元素类型是一个对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那么将会按照第一点的规则加载数组元素类型,接着又虚拟机生成一个代表此数组唯独和元素的数组对象。
3)、如果上述步骤没有出现任何问题,那么C在虚拟机中实际上已经成为了一个有效的类或者接口了,在解析完成之后还需要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具有访问权限,则抛出 java.lang.IllegalAccessError异常。
2、字段解析:
要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_info 项中索引的 CONSTANT_Class_info 符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了异常,都会导致字段符号引用解析的失败。如果成功解析,把这个字段所属类或者接口用C表示,虚拟机按照以下步骤对C进行后续字段的搜索:
1)、如果C本身就包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用;
2)、否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标匹配,则返回这个字段的直接引用;
3)、否则,如果C不是 java.lang.Object,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标匹配,则返回这个字段的直接引用;
4)、否则,查找失败,抛出 java.lang.NoSuchFieldError异常;
5)、如果查找返回了引用,则将会对这个字段进行权限校验,如果发现不具备访问权限,将会抛出java.lang.IllegalAccessError异常。
3、类方法解析:
先解析出类方法表的class_index 项中索引的方法所属的类或者接口的符号引用,如果解析成功,用C表示这个类,然后虚拟机按照以下步骤对C进行后续类方法的搜索:
1)、类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现 class_index 中的索引的C是一个接口,那就直接抛出 java.lang.IncompatibleClassChangeError 异常;
2)、如果通过了第一步,类C中查找是否有简单名称和描述符都与目标匹配的方法,如果有则返回这个方法的直接引用;
3)、否则,在类C的父类中递归查找是否有简单名称和描述符和目标都匹配的方法,如果有返回这个方法的直接引用;
4)、否则,在类C实现的接口及接口的父接口中充下往上递归查找,是否有简单民称和描述符都和目标匹配的方法,如果有则返回这个方法的直接引用;
5)、否则,查找失败,抛出 java.lang.NoSuchFieldError异常;
6)、如果查找返回了直接引用,则将会对这个方法进行权限校验,如果发现不具备访问权限,将会抛出java.lang.IllegalAccessError异常。
4、接口方法解析:
先解析出接口方法表的 class_index 项中索引的方法所述的类或者接口的符号引用,如果解析成功,用C表示这个接口,然后虚拟机将会按照以下步骤对C进行后续接口方法的搜索;
1)、如果在接口方法表中发现 class_index 中的索引的C是一个类,那就直接抛出 java.lang.IncompatibleClassChangeError 异常;
2)、否则,在接口C中查找是否有简单名称和描述符都和目标匹配的方法,如果有,则返回这个方法的直接引用;
3)、否则,在接口C的父接口中递归查找是否有简单名称和描述符都与目标匹配的方法,如果有,则返回这个方法的直接引用;
4)、否则,查找失败,抛出 java.lang.NoSuchFieldError异常;
五、初始化:
类初始化阶段是类加载过程的最后一步,初始化阶段才真正开始执行类中定义的 Java 程序代码(前面加载阶段除了用户自定义的类加载器参与之外,其余动作都是完全有虚拟机主导和控制的)。
初始化阶段可以理解为执行类构造器 () 方法的阶段。
() 方法的特点:
1、() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{} 块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定,静态语句块中只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变脸,在前面的静态语句快汇总可以赋值,但是不能访问;
2、() 方法与类的构造函数(实例构造器())不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的() 方法执行之前,父类的() 方法已经执行完毕(所以虚拟机中第一个被执行()方法的类是 java.lang.Object);
3、由于父类的() 方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作;
4、() 方法对于类和接口来说并不是必须的,如果一个类中既没有静态语句块,也没有对类变量的赋值操作,那么编译器就不会产生这个类的 () 方法;
5、接口中不能使用静态语句块,但是仍然可以使用静态变量初始化的复制操作,因此接口中也会生成 () 方法,但是与类不同的是,执行接口中的 () 方法不需要先执行父接口的 () 方法;只有当父接口中定义的变量使用时,父接口才会初始化;另外,接口的实现类在初始化时也一样不会执行接口的 () 方法。
6、虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其他线程都需要阻塞等待。
封面图源网络,侵权删除)
私信头条号,发送:“资料”,获取更多“秘制” 精品学习资料
如有收获,请帮忙转发,您的鼓励是作者最大的动力,谢谢!
一大波微服务、分布式、高并发、高可用的原创系列文章正在路上,
欢迎关注头条号:java小马哥
周一至周日早九点半!下午三点半!精品技术文章准时送上!!!
十余年BAT架构经验倾囊相授