前言

Java虚拟机规范定义了类加载、使用、以及卸载的过程,其中类加载分为三个大阶段,也可以细分为五个阶段。如下图所示:
未命名文件 (2).svg
按照上图中中所展示的生命周期,下面详细说明每一个阶段发生的操作。

加载

加载(Loading)是”类加载”过程中的第一个步骤,在加载阶段Java虚拟机需要完成以下操作:

  • 通过类的全限定名称获取定义此类的二进制子节流(开始准备读取Class文件)。
  • 将子节流所代表的静态存储结构转化为方法区的运行时数据结构(即Class文件被读入内存)。
  • 在内存中生成一个代表被加载类的java.lang.Class对象,作为方法区此类的数据访问入口。

Java虚拟机规范中并没有对这些流程做出明确的定义及限制,因此根据不同虚拟机的实现都有自己发挥的空间。例如准备读取Class文件时并没有指明二进制子节流必须从哪个Class文件获取,这里就衍生出各种方式。如:

  • 从ZIP压缩包读取:衍生出JAR、WAR等文件格式
  • 从网络中获取:典型应用场景就是Web Applet
  • 从运行时计算生成:这应该是Java Web中应用最多的场景,即动态代理
  • 由其他文件生成:典型应用JSP技术
  • 从加密文件获取:Class文件防止反编译的手段之一
  • 从数据库读取:某些中间件服务器如SAP Netweaver把程序安装在数据库完成集群中代码分发

    说明:在java.lang.reflect.Proxy中使用ProxyGenerator.generateProxyClass来为特定接口生成$Proxy代理二进制子节流,以及Spring中使用的cglib动态代理。

相对于类加载过程的其他阶段,加载(读取二进制子节流)这一步骤是开发人员最可控制的阶段,开发人员也可以使用自定义类加载器(重写自定义类加载器的findClass以及loadClass方法实现)去完成。需要注意的是,加载阶段与连接阶段部分动作(如字节码文件格式验证动作)是交叉进行的,加载在尚未完全完成时,连接动作可能已经开始执行,这些夹在加载阶段之中进行的动作,属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

连接

验证

验证(Verification)是连接过程的第一步,主要是校验加载的Class文件子节流包含的信息是否符合Java虚拟机规范。确保这些信息在运行时不会对虚拟机本身安全造成危害。
根据Class文件格式定义,Java作为强类型语言,严格定义了各种类型的空间占用,从Java层面来说无法做到访问数组边界以外的数据,也不会危害到内存的使用,如果尝试数组越界,编译器会抛出异常拒绝编译或者无法运行。
在早期Java虚拟机规范中并没有明确定义如何进行验证,如果不符合规范,直接抛出java.lang.VerifyError。Java虚拟机规范SE7之后版本中明确定义了验证阶段需要做的校验:

  1. 文件格式校验:校验Class文件是否符合Java虚拟机规范定义
    1. 校验魔数(0xCAFEBABE),主次版本号是否在当前Java虚拟机认可的范围内
    2. 常量池常量类型:检查tag是否在规定的范围内
    3. 常量池索引:校验指向是否不存在或者是类型不符合
    4. CONSTANT_Utf8_info:是否符合Utf8编码
    5. ……
  2. 元数据验证:字节码描述信息语义分析,是否符合Java语言规范
    1. 类是否包含父类:java.lang.Object类除外
    2. 类是否继承了不能继承的类:如String类被final修饰,不可继承
    3. 类是否实现了相关方法:如继承了接口或者抽象类的类
    4. 类中字段是否符合规范:如访问修饰符、final等
    5. ……
  3. 字节码验证:根据数据流和控制流分析,验证语义规范,是在元数据验证基础上的深层次验证
    1. 方法调用参数验证:如操作数栈放置int类型数据,保障不会按照long进行加载
    2. 方法作用域:保障任何跳转指令不会跳转到方法体以外区域
    3. 方法中类型转换:保障语法上类型转换的正确性,以及引用赋值的正确性,如父类引用指向子类对象
  4. 符号引用验证:符号引用转化为直接引用,具体转化过程在解析阶段进行,符号引用验证是类自身信息验证
    1. 符号引用通过字符串定义的全限定名称是否可以找到对应的类(找不到抛出ClassNotFoundException)
    2. 指定类是否可以找到对应的字段(编译找不到符号)、方法(抛出NoSuchMethodError)
    3. 访问修饰符

无论是字节码格式验证还是在语法语义上的验证,都不能保证在运行时一定是安全的,在类加载过程中过多的验证一定程度上也会过多的消耗时间,对性能造成影响,因此Java中很多的验证都被尽可能的迁移到编译器完成,很多东西都是在编译器就可以确定的,比如异常(受检异常和运行时异常),类占用空间大小。
验证阶段对于虚拟机来说是一个重要非必要的机制,验证的结果只有通过或者不通过,对于频繁使用的第三方工具或者自定义类其实也就没必要进行反复验证。如果想缩短类加载的时间可以使用参数-Xverify:none关闭类加载过程的验证阶段。

准备

准备(Preparation)阶段是正式为类变量(静态static变量)分配内存并设置初始值的阶段,这些变量会被分配在方法区上。需要明确的是方法区本身是一个逻辑区域,Java虚拟机规范也并没有明确定义这块区域,不同虚拟机厂商实现也各有不同。即便是HotSpotVM在JDK7前后实现也有较大变化,JDK7之前使用永久代(PermGen)作为方法区的实现,而在JDK8中使用元空间(MetaSpace)替换永久代,并迁移至Java堆内存中。

说明:这里需要明确几个概念

  • 内存分配:准备阶段的内存分配仅为类变量进行内存分配,实例变量的内存分配在对象创建阶段进行。
  • 方法区:方法区是Java虚拟机中定义的规范,永久代和元空间是HotSpotVM的两种实现,IMB J9、BEA JRockit虚拟机实现在设计之初就没有永久代,使用的是元空间的设计,在JDK8之后,HotSpotVM完全废除永久代的概念,改用与JRockit、J9一样在堆内存中实现的元空间(Metaspace)。
  • 设置初始值:数据类型的零值设置并不是一个原子操作,而是分步骤进行。如:static int a = 1。变量a在准备后的初始阶段值并不是1,而是0,这时还未执行任何Java方法,把值1赋给a的putstatic指令在程序编译后存放于类构造方法中,所以赋值动作是在类初始化阶段才会执行。但是对于final修饰的类变量在准备阶段会生成ConstantValue属性,并直接进行赋值。

解析

解析(Resolution)阶段是Java虚拟机通常将常量池中的符号引用替换为直接引用的过程。Class文件中所定义的符号引用包含CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。

  • 符合引用:Symbolic References,以一组符号来描述所引用的目标,符号可以是任何形式的字面量。
  • 直接引用:Direct References,可以直接指向目标的指针、相对偏移量或者是能进行访问定位的目标句柄。

相同符号引用在不同虚拟机翻译出来的直接引用一般不同,如果存在直接引用,那么引用的目标必然存在虚拟机内存中。

初始化

初始化(Initialization)是类加载过程中的最后一个阶段。在加载阶段用户对过程的可控制性较强,可以自定义加载阶段,此后直到初始化阶段,中间阶段均有Java虚拟机进行主导,而到了初始化阶段,整个过程的主导权则交由应用程序来控制。
在准备阶段类变量已经按照要求统一进行了初始零值设置,在初始化阶段会根据应用程序中定义的主观结果进行赋值。初始化阶段也可以看作是执行类构造器的阶段,是javac的自动生成产物,编译器会自动收集类中所有类变量的赋值动作和静态代码块(static{ }语句),编译器收集的顺序由源代码中的顺序决定。方法对于接口来说是非必须的,如果一个类中没有类变量赋值,也没有静态代码块,javac就不需要为类生成该方法。
此外在类加载过程中,Java虚拟机必须保证当多个线程同时去初始化一个类时,只能由一个线程进行这个动作,其他线程都需要等待阻塞。如果一个类的方法消耗时间较长,那么有可能造成多个进程阻塞。

使用

使用(Using)阶段,一般最多的应用就是根据方法区中存储的类信息在堆中进行对象的创建。对象创建过程会从类中定义的元数据加载并且完成对象的实例化,对象头中也会存储所属类的引用信息。

卸载

卸载(Unloading)是类加载生命周期中的最后一个阶段,但并不意味着类一定会进行卸载,类加载触发有一定的条件,类卸载也要经过严苛的条件,一般来说被虚拟机加载的类不会轻易卸载,而且只有用户自定义类加载器加载的类才会被卸载。类卸载须满足以下几个条件:

  • 加载这个类的加载器已经被回收了
  • 堆中没有这个类的实例
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法