详谈类加载的全过程

/ JVM / 0 条评论 / 2311浏览

前言

上一篇文章简单介绍了一个Java类的生命周期,一个类的生命分成7个阶段,在这7个阶段中除了使用和回收之外,剩下的五个阶段都属于加载的过程,也是最重要最复杂的几个过程,今天就深入了解一下一个类的加载过程,也就是加载、验证、准备、解析和初始化5个阶段。

本文是我对《深入理解Java虚拟机》一书7.3节类加载过程的知识总结。

一、加载

加载是类加载的过程,也就是Class Loading,在此阶段主要完成3件事:

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

以上是《Java虚拟机规范》的要求,规范的定义并没有限定死一些条件,比如根据类的全限名获取二进制字节流,但是并没有说从哪儿获取,这提供给开发者无限的发挥空间,因此目前加载一个class字节流不一定只从本地文件中加载,还有很多其他方式,具体方式如下:

在整个类加载过程中,此阶段是开发中控制能力最强的,一个非数组类的加载,可以使用系统提供的默认加载器来完成,也可以由用户自定义的类加载器去完成,开发者可以自定义类加载器去控制字节流的获取方式。但是相对于数组类则有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,在上一篇文章中也有所提到数组类,数组类创建过程遵循以下规则:

加载阶段与连接阶段的部分内容(比如验证)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始了,但这些夹在夹在阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然需要保持固定的先后顺序。

二、验证

验证是连接阶段的第一步,这一阶段是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害到虚拟机自身的安全。这个阶段至关重要,该阶段是否严谨,直接决定了虚拟机是否能够承受恶意代码的攻击。

《Java虚拟机规范》规定,如果验证到输入的字节流不符合class文件格式的约束,虚拟机就应该抛出一个java.lang.VerifyError异常或其子类异常。从整体上来看,验证阶段大致分成4个阶段来完成检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

1.文件格式验证

文件格式验证是验证的第一步,其需要操作的步骤有很多很多,这个阶段主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这个阶段是基于二进制字节流进行的,只有通过这个阶段验证之后才会进入方法区存储,即之后的三个验证操作都是基于方法区的存储结构进行的。

列举几个验证的内容:

2.元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,保证不存在不符合Java语言规范的元数据信息。

列举几个验证的内容:

3.字节码验证

第三阶段是整个验证过程最复杂的一个阶段,主要是通过数据流和控制流分析,确定程序语义是否合法、符合逻辑。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事。

如果一个方法体通过了字节码校验,也不一定说明其一定是安全的,因为通过程序去校验程序逻辑是无法做到绝对准确的,即不能通过程序准确地检查出程序是否能在有限时间之内结束运行。

此过程的数据流验证是非常复杂的,相对耗时也很高,但是在JDK 1.6之后java编译器和虚拟机对其进行了优化,给方法体的Code属性的属性表上增加一个名为“StackMapTable”的属性,这个属性描述了方法体中所有的基本块,在验证期间不需要根据程序推导来判断合法性,只需要检查StackMapTable属性中的记录是否合法即可,节省了很多时间。同时HotSpot虚拟机还提供了-XX:-UseSplitVerifier选项来关闭这项优化,或者使用参数-XX:+FailOverToOldVerifier要求在类型校验失败的时候退回到旧的类型推导方式进行校验。在JDK 1.7之后,主版本号大于50的class文件不允许再回退到类型推导的校验。

4.符号引用验证

这一阶段的验证发生在虚拟机将符号引用转化为直接引用的时候,而这个转化动作发生在连接的第三个阶段:解析。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,其目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么会抛出IncompatibleClassChangeError的子类,比如IllegalAccessError、NoSuchFieldError、NoSuchMethodError等。

列举几个验证的内容:


对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段,如果你已经完全信任class文件,保证它是符合要求的,可以不必通过验证阶段,通过-Xverify:none参数来关闭大部分类验证措施,以缩短虚拟机类加载的时间。

三、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。但是注意这里的默认值并不是赋给它的默认值,而是数据类型的零值,例如下面代码

public static boolean flag = true;

变量flag在准备阶段赋的值不会是true,而是false,因为boolean类型的值默认值是false,这个阶段的默认值均是数据类型的零值,比如int默认值是0、float默认值是0.0、引用类型是null等。

在这个阶段还有一个特例,就是被final修饰的常量,常量在准备阶段是直接赋给它对应的值

public static final boolean flag = true;

编译时Javac将会为flag生成ConstantValue属性,在准备阶段flag将会被直接赋值为true。

四、解析

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

对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。

解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_InvokeDynamic_info七种常量类型。

五、初始化

到了类初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,变量已将赋过一次“零值”,而在初始化阶段,则根据代码逻辑去初始化类变量和其他资源,相当于一个类在实例化时执行类构造器<clinit>()方法的过程,而在观察类的反编译的时候时长会看到<clinit>()方法,该方法并不是单纯的无参构造函数,它的形成和初始化阶段息息相关。

微信公众号浏览体验更佳,在这里还有更多优秀文章为你奉上,快来关注吧!

北风IT之路