jvm-类的生命周期

jvm-类的生命周期

起男 78 2022-12-06

jvm-类的生命周期

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

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

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

其中验证、准备和解析统称为连接

加载阶段

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

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

加载阶段,简而言之,就是查找并加载类的二进制数据,生成class的实例

在加载类时,jvm必须完成以下3件事

  1. 通过类的全名,获取类的二进制数据流
  2. 解析类的二进制数据流为方法区的数据结构(类模板对象)
  3. 创建Class类的实例(堆中),表示该类型。作为方法区这个类的各种数据的访问入口

如果加载的数据不是ClassFile的结构,则会抛出ClassFormError

数组

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

创建数组类的过程:

  1. 如果数组的元素类型是引用类型,那么就遵循定义的加载过程递归加载和创建数组的元素类型
  2. jvm使用指定的元素类型和数组维度来创建新的数组类

如果数组的元素类型是引用类型,数组类的可访问性就由元素类型的可访问性决定。否则数组类的可访问性将被缺省定义为public

连接阶段

验证阶段

当类加载到系统后,就开始连接操作,验证是连接的第一步

它的目的是保证加载的字节码是合法、合理并符合规范的

大体上jvm需要做以下认检查:

  1. 格式验证:魔数、版本、长度检查
  2. 语义验证:语法上是否符合规范。比如:是否继承final类、是否有父类、抽象方法是否有实现等
  3. 字节码验证:判断字节码是否可以被正确的执行。比如:跳转指令是否指向正确位置,操作数类型是否合理(栈映射帧StackMapTable)等
  4. 符号引用验证:符号引用的直接引用是否存在

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

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

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

准备阶段

准备阶段,就是为类的静态变量(static)分配内存,并将其初始化为默认值

当一个类验证通过时,虚拟机会进入准备阶段,虚拟机会为这个类分配相应的内存空间,并设置默认初始值

注意:

  1. 这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就分配了,准备阶段会显示的赋值(只会对基本数据类型和字面量形式的字符串进行赋值,new的对象还是在初始化阶段赋值)
  2. 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是随着对象一起分配到java堆中的
  3. 在这个阶段并不会像初始化阶段中那样会有初始化或代码被执行

解析阶段

解析阶段,就是将类、接口、字段和方法的符号引用转为直接引用

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

jvm规范并没有明确要求解析阶段一定要按照顺序执行。在hotspot中,加载、验证、准备和初始化会按照顺序有条不紊的执行,但连接阶段中的解析阶段往往会在初始化之后再执行

初始化阶段

初始化阶段就是为类的静态变量赋予正确的初始值

类的初始化是类装载的最后一个阶段。如果前面的步骤都没有问题,那么表示类可以顺利装载到系统中。此时,类才会开始执行java字节码。即到了初始化阶段,才真正开始执行类中定义的java程序代码

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

  • 该方法仅能由java编译器生成并由jvm调用,程序开发者无法自定义一个同名的方法,更无法直接在java程序中调用该方法,虽然该方法也是由字节码指令所组成
  • 它是由静态成员的赋值语句以及static语句块合并产生的

在加载一个类之前,虚拟机总是会加载该类的父类,因此父类的<clinit>()总是在子类的<clinit>()之前被调用。也就是说,父类的static块优先于子类

java编译器并不会为所有的类都产生<clinit>()初始化方法,以下类的字节码文件中将不会包含<clinit>()

  • 一个类中并没有声明任何的类变量,也没有静态代码块时
  • 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行赋值操作时
  • 一个类中包含static final修饰符的基本数据类型和字面量字符串的字段(没有new对象的情况,和方法的调用),这些类字段初始化语句采用编译时常量表达式

<clinit>()的线程安全性

对于<clinit>()方法的调用,也就是类的初始化,虚拟机会在内部确保其多线程环境中的安全性

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

正因为函数<clinit>()是线程安全的,因此,如果在一个类的<clinit>()中有很耗时的操作时,就可能会造成多个线程阻塞,引发死锁。并且这种死锁很难发现,因为他们并没有可用的锁信息(访问表示里没有锁标识,隐式锁)

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

使用阶段

任何一个类在使用之前都必须经过加载、连接和初始化阶段,一但一个类型成功经过这3个步骤之后,便可用由开发者进行使用了

卸载阶段

类、类的加载器和类的实例之间的关系:

在类加载器的内部实现中,用一个java集合存放所加载的引用。另一方面,一个class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例和与其类的加载器之间为双向关联关系

一个类的实例总是引用代表这个类的Class对象。在Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的java类都有一个静态属性class,它引用了代表这个类的Class对象

类的卸载需要:

  • 该类的所有实例都被回收
  • 加载该类的类加载器被回收(基本只有自定义的类加载器才有可能被回收)
  • 该类的Class对象在任何地方都没有被使用