JVM类加载过程

类加载时机

整体分为7个阶段

类加载整体流程
其中:加载、验证、准备、初始化四个过程确定的,必须按照这个步骤进行,而解析阶段不一定,有可能要到初始化之后才进行:Java为了支持运行时绑定特性(也称之为动态绑定或者晚期绑定)。

初始化阶段场景

《Java虚拟机规范》严格约定了下面六种场景必须立即对类进行”初始化“:

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有
    • 使用new关键字实例化对象的时候
    • 读取或者设置一个类型的静态字段(被final修饰、已在编译期把结果放进常量池的静态字段除外)的时候。
    • 调用一个类型的静态方法的时候
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有被初始化,则先要进行初始化
  3. 当初始化类的时候,如果发现其父类还没有被初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个类
  5. 当使用JDK7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析为四个句柄(四个REF打头的,细节略)时,如果这个方法句柄没有被初始化,则要先进行初始化。
  6. 当一个接口定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)。

类加载的整个过程

这里说的就是整个类加载的全过程,即:加载、验证、准备、解析和初始化这5个步骤。

加载

加载阶段,虚拟机需要完成三个事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据
  3. 在内存中生成一个代表这个类java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

验证(连接第一步)

这一阶段的目的时确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
从历史沿袭来看:从JDK7版本之后,验证阶段在《Java虚拟机规范》中才得以充分完善,主要经历下面四个步骤:

文件格式验证

说白了,这个过程是验证第一道门槛,即从读取文件字节流到存储在方法区的验证。如果这一步不过,后面就别谈了,只有这步验证通过,才允许存放在方法区,后续验证步骤直接读方法区内容。
可以了解的几种验证场景:

  • 是否以魔数0xCAFEBABE开头
  • 主、次版本号是否在当前Java虚拟机接受范围之内
  • 常量池的常量中是否有不被支持的常量类型(检查常量的tag标志)
  • 指向常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
  • ……
元数据验证

对字节码描述的信息进行语义分析(注:从编译原理角度,更像是语法分析),可能包含如下:

  • 这个类是否有父类(除了Object类之外,所有的类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或者接口中的要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不允许的方法重载)
  • ……
字节码验证

最复杂的一个过程,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
了解几个场景:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于”在操作栈放置了一个int类型数据,使用时则按long类型来加载本地变量表中“这样的情况
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上
  • 保证方法体中的类型转换总是有效的。(UpCast与DownCast问题)

注:书中还提到了一个离散数学问题:停机问题(Halting Problem)维基百科介绍-停机问题
这个问题本质结论,即:不能通过程序准确的检查出程序是否能在有限的时间内结束运行。

符号引用验证

准备(连接第二步)

解析(连接第三步-可能)

初始化