类加载过程

Class文件中描述的各种信息最终都需要加载虚拟机中之后才能运行使用,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析初始化,最终形成可被虚拟机直接使用的Java类型

类型的加载、连接初始化过程都是在程序运行期间完成的,这虽然会令类加载时稍微增加一些性能开销,但能够提供高度的灵活性,其天生的动态扩展性就是依赖运行期动态加载动态连接面向接口的应用程序可以等到运行时再指定其实际的实现类,用户可以通过预定义的和自定义类加载器,让一个本地应用程序可以在运行时从网络其他地方加载一个二进制流作为程序代码的一部份,如Applet、JSP、OSGi等技术。

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存的整个生命周期加载验证准备解析初始化使用卸载7个阶段,验证、准备、解析3个部分统称为连接
类的生命周期

加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,但解析阶段在某些情况下可以在初始阶段之后,这是为了支持运行时绑定即动态绑定

初始化阶段虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,若类未进行初始化需先触发其初始化。
  • 使用java.lang.reflect包的方法对类进行反射调用时,若类未进行初始化需先触发其初始化。
  • 初始化类时其父类未初始化,需先触发其父类初始化。
  • 当虚拟机启动时,用户指定一个执行主类即包含main方法的类,虚拟机会先初始化该类
  • 若一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且该方法句柄所对应的类未进行过初始化需先触发其初始化。

以上5种场景称为对一个类的主动引用,除此之外所有引用类的方式都不会触发初始化,称为被动引用,被动引用的几种情况:

  • 对于静态字段,只有直接定义该字段的类才会被初始化,其子类来引用父类的中定义的静态字段只会触发父类的初始化而不会触发子类的初始化。
  • 通过数组定义来引用类
  • 常量的引用,常量在编译阶段会存入调用类的常量池种,本质上并没有直接引用到定义常量的类

接口加载过程的加载过程稍微有些不同接口中也有初始化过程,类中可以使用静态语句块static{},但是接口中不能使用static{}语句块,但编译器任然会为接口生成<clinit>类构造器,用于初始化接口中定义的成员变量;接口与类的真正区别是,当类在初始化时,其父类都已经完成初始化,但接口在初始化时,并不要求其父类接口全部都完成初始化,只有在真正使用到父类接口的时候才会初始化,例如引用父类接口中定义的常量

加载

加载阶段虚拟机要完成3件事:通过类的全限定名来获取定义此类的二进制字节流;将字节流所代表的静态存储结构转化为方法区运行时数据;在内存中生成一个代表该类的java.lang.Class对象,作为方法区该类的各种数据访问入口

虚拟机没有明确规定二进制字节流要从哪里获取怎样获取,相对于类加载过程的其他阶段,一个非数组类加载阶段中获取类二进制字节流的动作是开发人员可控性最强的,加载阶段既可以使用系统提供引导类加载器来完成,也可以由自定义的类加载器来控制字节流的获取方式。一些典型的使用场景如下:

  • ZIP包中读取JAREARWAR格式的基础
  • 网络中获取,例:Applet
  • 动态代理java.lang.reflect.Proxy
  • 由其他文件生成,例:由JSP生成对应的Class
  • 数据库中读取,例:中间件服务器可选择把程序安装到数据库中来完成在集群间的发布

但数组类有所不同,数组类本身不通过类加载器创建,而是由虚拟机直接创建,但数组类与类加载器任然关系很密切,数组类的元素类型最终是要靠类加载器去创建。数组类的创建过程遵循以下规则:

  • 若数组的组件类型引用类型,则递归采用加载过程加载该组件类型,数组在加载该组件类型类加载器类名称空间上被标识
  • 若数组的组件类型不是引用类型,虚拟机会将数组标记为与引导类加载器关联
  • 数组类的可见性与其组件类型一致,若组件类型不是引用类型,数组类的可见性默认为public

虚拟机外部二进制字节流加载阶段完成后按照虚拟机所需的格式存储在方法区,然后在内存中实例化一个java.lang.Class类的对象,但并没有明确规定Java堆中,对于HotSpot虚拟机而言,Class对象虽然是对象,但存储在方法区中,该对象将作为程序访问方法区中的这些类型数据外部接口

加载阶段连接阶段的部分内容是交叉进行的,例如一部分字节码文件格式校验,加载阶段尚未完成,连接阶段可能就已经开始,但这些夹在加载阶段之中进行的动作任然属于连接阶段内容,两个阶段的开始时间任然保持固定先后顺序

验证

验证连接阶段的第一步,目的是为了确保Class文件字节流中包含的信息符合当前虚拟机要求,且不会危害虚拟机自身安全。

Class文件不一定要求用Java源码编译而来,可以使用任何途径产生,甚至包括十六进制编辑器直接编写来产生Class文件,虚拟机如果不检测输入的字节流,很可能会载入有害的字节流而导致系统崩溃

验证阶段工作量在虚拟机的类加载子系统中占了相当大的一部分,若验证到输入的字节流不符合Class文件格式的约束,虚拟机会抛出一个java.lang.VerifyError异常或其子类异常,验证阶段大致会完成4个阶段的检验动作:文件格式验证元数据验证字节码验证符号引用验证

对虚拟机类加载机制来说,验证阶段是一个非常重要不是一定必要的阶段,对程序运行期没有影响,若所运行的全部代码都已被反复使用和验证过,在实施阶段可以使用-XVerify:none参数来关闭大部分类验证措施,以缩短虚拟机类加载的时间。

文件格式验证

验证字节流是否符合Class文件格式的规范,且能被当前版本的虚拟机处理,包括是否以魔数0xCAFEBABE开头,主次版本号是否在当前虚拟机处理范围内,检查常量池常量tag标志的是否有不被支持的常量类型,指向常量的各种索引值中是否有指向不存在常量不符合类型的常量,CONSTANT_Utf8_info型常量中是否有不符合UTF8编码的数据,Class文件各个部分文件本身是否有被删除的或附加的其他信息,等等。

该阶段主要目的是保证输入的字节流能正确地解析并存储于方法区中,格式上符合描述一个Java类型信息的要求,该阶段的验证时基于二进制字节流进行的,只有通过了该阶段的验证,字节流才会进入内存方法区进行存储,后面的3个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流

元数据验证

该阶段对字节码描述的信息进行语义分析,包括验证类是否有父类,类的父类是否继承不允许被继承的类类是不是抽象类、是否实现了其父类接口要求实现所有方法类的字段、方法是否与父类产生矛盾

该阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息

字节码验证

字节码验证是整个验证阶段过程中最复杂的阶段,主要目的是通过数据流控制流分析,确定程序语义是否合法符合逻辑,在元数据验证阶段对元数据信息中的数据类型做完校验后,字节码验证阶段将对类的方法体进行校验分析,保证被校验类的方法运行时不会做出危害虚拟机安全的事件,保证任意时刻操作数栈数据类型指令代码序列都能配合工作,不会出现在操作栈放置int类型的数据,使用时按long类型来加载入本地变量表;保证跳转指令不会跳转到方法体以外的字节码指令上;保证方法体类型转换是有效的;

即使方法体通过了字节码验证,也不能明确一定就是安全的,通过程序校验程序逻辑是无法做到绝对准确的,不能通过程序准确地检测出程序是否能在有限时间内结束运行

数据流验证复杂性高,为了避免过多的时间消耗在字节码验证阶段JDK1.6之后Javac编译器Java虚拟机中进行了优化,给方法体Code属性属性表中增加了一项名为StackMapTable属性StackMapTable属性描述了方法体所有基本块开始时本地变量表操作栈应有的状态,在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法。

理论上StackMapTable属性存在错误被篡改的可能,在JDK1.6HotSpot虚拟机中提供-XX:-UseSplitVerifier选项类关闭这项优化,或使用-XX:+FailOverToOldVerifier参数来要求在类型校验失败退回旧的类型推导方式进行校验,JDK1.7后对于主版本号大于50Class文件,只能使用类型检查来完成数据流分析校验不允许退回到类型推导的校验方式。

符号引用验证

符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,该转化动作将在连接的第三阶段——解析阶段中发生,符号引用验证可看做是对类自身以外的信息进行匹配性校验,包括符号引用中通过字符串来描述的全限定名是否能找到对应的类,在指定的类中是否存在符合方法的字段描述符以及简单名称所描述方法字段符号引用中的类、字段、方法的访问权限是否可被当前类访问。

符号引用验证的目的是确保解析动作能正常执行,若无法通过符号引用验证,将抛出java.lang.IncompatibleClassChangeError异常的子类,

准备

准备阶段正式类变量分配内存设置类变量初始值的阶段,类变量使用的内存都在方法区中进行分配。这个时候进行内存分配的仅包括被static修饰的类变量不包括实例变量,实例变量将会在对象实例化时随对象一起分配在中,且这里所说的初始值通常情况下是数据类型零值,例如一个类变量定义为:public static int value = 123;则变量在准备阶段后的初始值0而不是123,把value赋值为123putstatic指令是程序被编译后,存放于类构造器<clinit>()方法中,把value赋值为123的动作将在初始化阶段执行。

类字段属性表中存在ConstantValue属性,在准备阶段变量value会被初始化为ConstantValue属性所指定的值,如public static final int value = 123;编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性设置value赋值为123

数据类型 零值 数据类型 零值 数据类型 零值
int 0 boolean false char ‘\u0000’
long 0L float 0.0f reference null
short (short)0 double 0.0d byte (byte)0

解析

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

符号引用Class文件中以CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等类型的常量出现,符号引用以一组符号描述所引用的目标符号可以是任意形式字面量,只要使用时能无歧义定位到目标符号引用虚拟机实现的内存布局无关引用目标不一定已经加载内存中,各种虚拟机实现的内存布局不相同,但能接受的符号引用必须一致,因为符号引用字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用可以直接指向目标指针相对偏移量或是能间接定位到目标的句柄直接引用与虚拟机实现内存布局相关,如果存在直接引用,则引用的目标必定已存在内存中

虚拟机规范中并未规定解析阶段发生的具体时间,只要求在执行anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_wmultianewarraynewputstatic16条用于操作符号引用字节码指令之前,先对它们所使用的符号引用进行解析,虚拟机实现可以根据需求来判断是在类加载器加载时对常量池中的符号引用解析,还是等到符号引用将要被使用前才去解析

同一个符号引用进行多次解析invokedynamic指令外,虚拟机可以对第一次解析的结果进行缓存,在运行时常量池记录直接引用,并把常量标识已解析状态,从而避免解析动作重复进行,无论是否真正执行多次解析动作,虚拟机需要保证在同一实体中,若一个符号引用之前已经被成功解析过,则后续的引用解析请求应当一直成功,反之亦然。

invokedynamic指令目的是用于动态语言支持,其所对应的引用称为动态调用点限定符,动态的含义是指必须等到程序实际运行到这条指令的时候,解析动作才进行,且其解析结果对于其他invokedynamic指令并不生效其余可触发解析指令都是静态的。

类或接口的解析

从未解析过的符号引用解析为接口直接引用,虚拟机完成整个解析过程需要三个步骤:

  • 符号引用指向接口非数组类型,则虚拟机将会将符号引用全限定名传递给当前类类加载器加载这个接口,加载过程中,由于元数据验证字节码验证的需要,可能触发其他相关类的加载,如加载该类的父类或实现的接口。若加载过程出现任何异常解析过程将失败
  • 符号引用指向接口数组类型,且数组元素类型对象,即符号引用描述符类似[Ljava.lang.Integer的形式,将按照上面的规则加载数组元素类型,若符号引用描述符[Ljava.lang.Integer,则加载元素的类型java.lang.Integer,接着由虚拟机生成一个代表此数组维度元素数组对象
  • 若前面的步骤未出现任何异常,则符号引用指向接口在虚拟机中实际上已经成为一个有效接口,在解析完成之前需要进行符号引用验证,确认当前类是否具有对符号引用指向接口访问权限。若不具备将抛出java.lang.IllegalAccessError异常。
字段解析

解析一个未被解析过的字段符号引用,将对字段表class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属类或接口符号引用。若解析该类或接口符号引用的过程出现任何异常都将导致字段符号引用解析失败

解析成功,虚拟机将按照以下步骤对该字段所属的类或接口进行字段搜索。若查找过程返回直接引用成功,还将对字段进行权限验证,若不具备访问权限,将抛出java.lang.IllegalAccessError异常。

  • 字段所属的类或接口本身包含的简单名称字段描述符都与目标字段匹配,则返回该字段的直接引用,查找结束。
  • 字段所属的类或接口中实现了接口,将按照继承关系从下往上递归搜索各个接口和它的父接口,若接口中包含的简单名称字段描述符都与目标字段匹配,则返回该字段的直接引用,查找结束。
  • 字段所属的类或接口非java.lang.Object,将按照继承关系从下往上递归搜索其父类,若父类中包含的简单名称字段描述符都与目标字段匹配,则返回该字段的直接引用,查找结束。
  • 否则查找失败,抛出java.lang.NoSuchFieldError异常

实际应用中,虚拟机编译器实现比上述规范要求更加严格,若有同名字段同时出现在字段所属的类或接口的接口和父类中,或同时出现在自己和父类的多个接口中,编译器将拒绝编译

类方法解析

类方法解析需先解析类方法表class_index项中索引的方法所属的类或接口符号引用,若解析成功将按照如下步骤进行类方法搜索

  • 类方法接口方法符号引用常量类型定义是分开的,若在类方法表中发现class_index中索引的方法所属为接口,将抛出java.lang.IncompatibleClassChangeError异常。
  • 类方法所属的类或接口中查找是否有简单名称描述符都与目标相匹配的方法,若有则返回该方法的直接引用,查找结束。
  • 类方法所属的类或接口实现的列表及它们的父类接口递归查找是否有简单名称描述符都与目标相匹配的方法,若存在匹配的方法则说明该类是一个抽象类,查找结束,抛出java.lang.AbstractMethodError异常。
  • 否则,方法查找失败,抛出java.lang.NoSuchMethodError异常。

若查找过程成功返回了直接引用,将会对该方法进行权限验证,若不具备访问权限,将抛出java.lang.IllegalAccessError异常。

接口方法解析

接口方法解析需先解析接口方法表class_index项中索引的方法所属的类或接口的符号引用,若解析成功将按照如下步骤进行类方法搜索

  • 若在接口方法中发现class_index中的索引的方法所属为类而非接口,将抛出java.lang.IncompatibleClassChangeError异常。
  • 接口方法所属的类或接口中查找是否有简单名称描述符都与目标相匹配的方法,若有则返回该方法的直接引用,查找结束。
  • 接口方法所属的类或接口父接口递归查找,直到查找完java.lang.Object类为止,看是否有简单名称描述符都与目标相匹配的方法,若有则返回该方法的直接引用,查找结束。
  • 否则,方法查找失败,抛出java.lang.NoSuchMethodError异常。

接口中所有方法默认都是public,故不存在访问权限问题,故接口方法符号解析应该不会抛出java.lang.IllegalAccessError异常。

初始化

初始化阶段真正开始执行类中定义Java程序代码或者说是字节码,除加载阶段用户应用程序能通过自定义类加载器参与外,其余动作完全由虚拟机主导和控制

准备阶段变量已经赋值系统要求的初始值,在初始化阶段是执行类构造器<clinit>()方法的过程,主观计划去初始化类变量其他资源

<clinit>()方法是由编译器自动收集类中所有变量赋值动作和静态语句块static{}中的语句合并产生的,编译器收集顺序是由语句在源文件出现顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在其后的变量,在静态语句块可以赋值但不能访问

<clinit>()方法与实例构造器不同,它不需要显示调用父类构造器,虚拟机会保证子类<clinit>()方法执行前父类<clinit>()方法已执行完毕。故虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object

由于父类的<clinit>()方法先执行,故父类中定义的静态语句块要优先于子类的变量赋值操作

<clinit>()方法对于类或接口并非是必需的,若类中无静态语句块,也无对变量的赋值操作,编译器将不会为该类生成<clinit>()方法。

接口中不能使用静态语句块,但任然有变量初始化赋值操作,故接口和类一样都会生成<clinit>()方法,但执行接口<clinit>()方法不需要先执行父接口<clinit>()方法,只有父接口中定义的变量使用时才会初始化接口的实现类在初始化时也不会执行接口<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确枷锁、同步,若多个线程同时去初始化一个类只会有一个线程去执行该类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法方法完毕。

组件类型:数组去掉一个维度得类型
类必须与类加载器一起确定唯一性
不允许被继承的类:被fianl修饰的类
类的字段、方法是否与父类产生矛盾:覆盖了父类的final字段,或者出现不符合规则的方法重载
类自身以外的信息:例如常量池中各种符号引用