概述

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用和卸载。

什么情况下,需要开始类加载的第一个阶段:加载? JVM规范并没有进行强制约束。只要求了以下几种情况,必须立刻对类进行初始化:

  • 遇到new,getstatic,putstatic,invokestatic这4条指令时。如果类没有进行初始化,则需要先触发其初始化。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发初始化。
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析的结果REF_getstatic, REF_putStatic, REF_invokeStatic的方法句柄,并且那个方法句柄所对应的类还没有进行初始化,则需要先进行初始化。

对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语--有且只有。这5种场景被称为对类的主动引用。 除此之外,所有引用类的方式都不会触发初始化,被称为被动引用。

例如:
用子类来引用父类中定义的静态变量,只会触发父类的初始化,而不会触发子类的初始化。
定义某个类的数组,并不会导致该类发生初始化。
调用类的静态final变量,并不会导致该类发生初始化。 这是因为final的static变量,在经过编译优化之后,被放到NotInitialization类的常量池中。

SuperClass[] sca = new SuperClass[10]; //定义数组时,并不会触发初始化

另外接口中,是不能使用 static语句块的。

类加载的过程

加载

在加载过程中,JVM需要完成以下三件事情

  • 通过一个类的全限定名来获取定义此类的二级制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

验证

验证是连接阶段的第一步,这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证过程非常复杂,而且本身是一个不可解问题--即再怎么验证,也无法保证class文件一定是安全的。
JVM虚拟机进行验证主要有四种验证:

  • 文件格式验证
    这一阶段主要验证Class文件字节流是否符合Class文件的格式规范。这一阶段可能包括以下这些验证点:
  • 是否以魔数开头
  • 主次版本号是否在JVM处理范围内
  • 常量池中是否有不被支持的常量类型
  • 指向常量的各种引用中是否有指向不存在的常量或者不符合类型的常量
  • CONSTANT_Utf8_info 型的常量是否有不符合UTF8编码的数据
  • Class文件中各个部分和文件本身是否有被删除的或附加的其他信息
  • 元数据验证
    第二阶段是对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求。可能包括的验证点有:
  • 这个类是否有父类
  • 这个类的父类是否继承了不被允许继承的类
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  • 类中的字段,方法是否与父类产生矛盾
  • 字节码验证
    第三阶段是整个验证过程中最复杂的一个阶段。主要目的是通过数据流和控制流分析,确定程序的语义是否合法,符合逻辑。这个阶段将对类的方法进行验证,保证类的方法在运行的时候不会对JVM的安全产生危害。

  • 符号引用验证
    最后一个阶段的校验发生再虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段--解析阶段发生。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量使用的内存将都在方法区进行分配。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。有点类似于DLL加载过程中的重定位。

符号引用: 符号引用以一组符号来描述引用的目标,符号可以是任何形式的字面量。 符号引用与虚拟机实现的内存布局无关。
直接引用: 直接引用可以是直接指向目标的指针,相对偏移量或者一个能间接定位到目标的句柄。 直接引用与虚拟机实现的内存布局有关。

虚拟机规范中并未规定解析阶段发生的具体时间,只要求了在执行anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic这16个用于操作符号引用的字节码指令之前,需要先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载时就对常量池中的符号引用进行解析,还是在等到一个符号引用将要被使用前才去解析它。

解析引用主要有一下几种

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

初始化

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。 或者说: 初始化阶段是执行类构造器 clinit() 方法的过程。

  • clinit() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
  • clinit() 方法与类的构造函数不同,它不需要显式的调用父类构造函数,虚拟机会保证在子类的()方法执行之前,父类的 clinit() 方法已经执行完毕。
  • clinit() 方法是不必须的,如果一个类中没有静态语句块,也没有对变量赋值的操作,那么编译器可以不为该类生成 clinit() 方法。
  • JVM会保证 clinit() 方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只有一个线程去执行这个类的 clinit() 方法。