类加载的生命周期:加载、验证、准备、解析、初始化、使用 和  卸载 ,一共 7 个阶段。

1. 概述

在虚拟机的类加载机制中:类型的加载、连接 和 初始化 都是在程序运行期间完成的。这种策略会使类加载时稍微增加一些性能开销, 但是为 Java 提供了高度的灵活性 , 如 Java 的动态扩展语言特性就是依赖运行期动态加载和动态连接这个特点实现。

举例: 如果编写一个面向接口的应用程序,可以等到运行时在指定其实际的实现类 ( 多态 )。 用户可以通过通过 Java 预定义 和 自定义类加载器让本地的应用程序可以从 网络或其他地方 加载一个二进制流作为程序代码的一部分 。

2. 虚拟机对类的初始化的情况

上文已经说了类的初始化在运行期间完成。虚拟机规范有且只有下面5种情况立即对类进行“初始化”。

(1) 使用 new 关键字 ;Get static,Put static 即设置或访问静态变量 (类的静态字段 被 final 修饰,加入常量池的除外);Invoke static 调用静态方法 。

(2)使用 java.lang.reflect 包的方法对类进行反射的时候。

(3)初始一个类,发现父类没有进行初始化,先初始化父类。

(4)虚拟机启动时, 用户需要制定一个要执行的主类,先初始化主类。

(5)Java 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_setStatic、REF_invokeStatic 方法的句柄,并且该句柄的方法所对应类没有进行过初始化,则需要先触发其初始化。

3. 加载

加载,请注意是“加载”,加载是类加载中的一个阶段。加载需要完成3件事情。

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

需要注意的是第一点:通过类的全限定名来获取定义此类的二进制字节流。没有指明二进制字节流具体从哪获得,怎样获取,二进制字节流可以从许多类型文件中获取,许多举足轻重的技术都建立在这个基础上。

  • 从  zip 包中读取,例如:从 jar 、war、ear 包中读取。
  • 从网络中获取 ,例如: Applet 。
  • 运行时计算生成,例如:动态代理技术。
  • 其他文件生成,例如:JSP 生成对应的 Class 类 (Servlet)。
  • 从数据库中读取,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中

4. 验证

验证是为了确保 Class 文件的字节流包含的信息是符合当前虚拟机的要求,并且不会危害虚拟机,因为上一小节中说道 Class 文件文件可以从很多地方获取。

验证主要包括:文件格式验证,元数据验证,字节码验证 和 符号引用验证。

文件格式验证是验证 字节流 是否符合 Class 文件格式的规范。

元数据验证 是验证 字节流 的语义,如 : 是否继承了不能被继承的类。

字节码验证 是验证 字节流 的语义是符合逻辑的、合法的。例如:int 数据使用时却用 long 类型加载。

符号引用验证 是 验证虚拟机将符号引用转化为直接饮用是否有错。

5. 准备和解析

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的是方法区的内存,需要注意的是这里给是类变量(static 修饰的变量)分配内存,不包括实例变量,类变量的初始为0(类变量的初始值在 <clinit>() 方法中进行赋值),但是如果是类变量是常量则为初始值。

解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。

符号引用:符号引用以一组符号来描述所引用的目标,符号可以任何的字面变量,符号引用与内存布局无关,引用的目标不一定加载到内存中。

直接引用:直接饮用可以是直接指向目标的指针、相对偏移量或者是一个间接可以定位到目标的句柄,直接引用和虚拟机实现的内存布局相关的。如果有了直接引用那么引用的目标已经在内存中存在。

解析包括对类或接口的解析、字段解析、类方法解析和接口方法解析。

6. 初始化

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

初始化阶段是执行类构造器 <clinit>() 方法的过程,<clinit>() 方法是编译器自动收集类中的所有类变量和 静态语句块 (static{}块)中的语句合并产生的,编译器的收集顺序是按照语句在源文件中的顺序所决定的,静态语句块只能访问定义在静态语句块之前的变量,定义在之后的变量可以赋值,但不可以被访问

虚拟机在子类 <clinit>()  之前,对父类进行<clinit>() ,因此第一个执行 <clinit>()  方法的类是 Object (所有类的父类)。

父类的静态语句块早于 子类的语句块。

 <clinit>()  不是必须的,如果类中没有静态语句块,也没有对变量的赋值操作。

接口执行 <clinit>()  ,父接口不用执行 <clinit>() ,只有父接口定义的变量被使用时,父接口才会初始化,接口的实现类也是如此。

<clinit>() 方法是同步方法:虚拟机保证类的 <clinit>() 方法是在多线程的环境下被正确的加锁、同步。如果多个线程同时初始化一个类,那么只会有一个线程执行 <clinit>() 方法, 其他线程需要去阻塞等待,如果一个类的 <clinit>() 方法有耗时的操作,就可能会阻塞多个进程。

参考文献

  • 深入理解Java虚拟机:JVM高级特性与最佳实践 / 周志明著. —— 2 版 . —— 北京:机械工业出版社,2013.6 (2019.1重印)