类加载机制

1,概述

  java的类加载机制指的是虚拟机吧描述类的文件从Class文件加载到内存,并对数据进行校验、转换解析和初始化,并最终行程可以被虚拟机直接使用的Java类型

2、类加载器的类型

 

                      

  • 启动类加载器:Boostrap ClassLoader负责加载存放在${JAVA_HOME}\jre\lib下或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
  • 扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载${JAVA_HOME}\jre\lib\ext目录中,或有java.ext.dirs系统变量指定的路径中的类库
  • 应用程序类加载器:Application ClassLoader,该加载器由sun.misc.Launcher $App-ClassLoader实现,由于这个加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称作系统类加载器。它负责加载用户类路径指定的类库

  (对于Sun HotSpot虚拟机来说,可通过-XX:+TraceClassLoading参数打印类加载的过程)

3、类加载过程

  类从被加载大屏虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(loading)、验证(verification)、准备(Preparamtion)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备、解析3个部分同城为连接(linking),这七个阶段的发生顺序入如下图所示。其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这个顺序按部就班的开始(通常会在一个阶段执行的过程中调用、激活另一个阶段),而解析阶段则不一定:它在某些情况下可以再初始化阶段之后在开始,这是未知支持Java语言的运行时绑定(也成动态绑定或晚起绑定)。

            

3.1 加载

  在类的加载阶段,虚拟机需要完成以下三件事:

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

3.2 验证

  这一阶段的目的是确保Class文件的字节流中包含的信息符合当前虚拟的要求,并且不会威胁虚拟机本身的安全。验证阶段大致会完成以下四个阶段的检查动作:文件格式校验、与元数据校验、字节码校验、符号引用校验。

3.2.1 文件格式校验:

  这个阶段主要是验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这个阶段的验证是基于二进制字节流进行的,只有通过这个阶段的验证后,字节流才会进入内存中的方法区进行存储。这一阶段可能包含(但不限于)以下校验:

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

3.2.2 元数据校验 

  这个阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。这个阶段可能包含(但不限于)的验证如下:

  • 这个类是否有父类(除了java.lang.Object外,应当所有的类都有父类)
  • 这个类的父类是否继承了不允许继承的类(如final 修饰的类)
  • 如果这个类不是抽象类,是否实现了器父类或接口之中要求实现的所有方法
  • 类中的变量、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或出现不符合规则的方法重载,如方法名和参数一直但是返回值不一样)  

3.2.3 字节码校验

  这个阶段是整个验证过程中最复杂的一个阶段,目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将会对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的行为。但是通过了字节码验证也不一定是安全的,因为通过程序去校验程序逻辑是无法做到绝对准确的,这个阶段可能包含(但不限于)的验证如下:

  • 验证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(例如不会出现类似的情况:在操作站放置了一个Int类型的数据,使用时却按long类型)  
  • 验证跳转指令不会挑战到方法体以外的字节码中去
  • 验证方法体重的类型转换是有效的

3.2.4 符号引用校验

  符号引用校验是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。通常需要校验以下(但不限于)内容:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段以及简单名称所描述的方法和字段
  • 符号引用中类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问

  对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段。如果所运行的全部代码都已经被反复使用和验证过,那么在实施阶段可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间

3.3 准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包含类变量(static修饰),而且初始值通常情况下是数据类型的零值,真正的赋值会在初始化阶段完成(如private static value=3;在准备阶段value的值是零值,在初始化阶段才会赋值3)。但是当final和static同时修饰类变量的时候,在准备阶段就会直接赋值。因为final修饰的变量在常量池中,所以在准备阶段就会直接被初始化为常量池中的值

        

3.4 解析

  解析阶段是虚拟机将常量池中的符号引用替换为直接应用的过程,符号引用在Class文件中以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现。

3.4.1 直接引用与符号引用

  符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机的内存布局无关,引用的目标不一定已经加载到内存中,各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中

  直接引用(Direct References):直接引用可以试直接指向目标的、相对偏移量或是一个能够间接定位到目标的句柄。直接引用和虚拟机的内存布局是相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经存在在内存中。

3.4.2 解析动作

  解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行,分别对应常量池的CONSTANT_Class_infoCONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_infoCONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info 7种常量类型

3.4.2.1 类或接口的解析

  假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机的解析过程需要以下三个步骤

  1. 如果C不是数组类型,那虚拟机将会把代表N的全限定名传给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又有可能触发其他相关类的加载动作(如加载父类或接口)。一旦这个过程出现任何的一场,解析过程就宣告失败。
  2. 如果C是数组类型,并且数据的元素类型是对象,那将会按照第一点的规则加载数组元素类型。
  3. 如果上面的两步没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,则会抛出java.lang.IllegalAccessError异常。
3.4.2.2 字段解析

  要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口的符号引用过程中出现了异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索:

  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用, 查找结束
  2. 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用, 查找结束
  3. 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索各个接口和它的父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用, 查找结束
  4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。

  如果查找过程中成功的返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。

3.4.2.3 类方法解析

  类方法解析的第一个步骤和字段解析一样,也需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,虚拟机将会按如下的步骤进行后续的类方法的搜索:

  1. 类方法和接口方法的符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常
  2. 如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则直接返回这个方法的直接引用,查询结束
  3. 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则直接返回这个方法的直接引用,查询结束
  4. 否则,在类C实现的接口列表及它们的父接口中递归查找是否有简单名称和描述符都与目标匹配的方法,如果存在,说明类C是一个抽象类,这时查询结束,抛出java.lang.AbstractMethodError异常
  5. 否则,宣告方法查询失败,抛出java.lang.NoSuchMethodError。

  如果查找过程中成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。

3.4.2.4 接口方法解析

  接口方法也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功将会按照以下步骤继续查询:

  1. 与类方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常
  2. 否则,在接口C中查找是否有简单名称和描述符都与目标匹配的方法,如果有直接返回这个方法的直接引用,查询结束
  3. 否则,在接口C的父接口中递归查找是否有简单名称和描述符都与目标匹配的方法,如果有则返回这个方法的直接引用,查询结束
  4. 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError异常。

3.5 初始化

   类初始化是类加载过程的最后一步,主要是给类变量赋值和执行静态代码块,执行的顺序有代码的编写顺序决定。前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与外,其余的动作完全由虚拟机主要和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。

  在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的主管计划去初始化类变量和其他资源。 

posted @ 2018-10-25 08:46  孤*狼  阅读(194)  评论(0)    收藏  举报