Java虚拟机类加载的过程

来源:互联网 发布:童装网络营销策划书 编辑:程序博客网 时间:2024/06/05 01:10

1. 类加载的时机:

  • 类从被加载到虚拟机内存开始到卸载出内存,整个生命周期包括以下七个阶段,其中加载,验证,准备,初始化,卸载这5个阶段的顺序是确定的。
    类生命周期

  • 类在什么情况下进行加载: 虚拟机对类的加载时机并没有明确的规定,是由具体的虚拟机实现的,当时明确规定了,在以下5种情况下(有且只有),类必须进行初始化(加载、验证、准备必须在初始化之前)。这5种方式也被称为主动引用,除此之外的其他引用都不会触发初始化,称为被动引用。

    • 遇到new(使用new关键字创建实例对象的时候)、getstatic(读取一个静态变量,被final修饰,已在编译把结果放入常量池的静态变量除外)、putstatic(设置一个静态变量,被final修饰,已在编译把结果放入常量池的静态变量除外)或invokestatic(调用一个类的静态方法)这四条字节码指令的时候,如果类没有进行初始化,则需要先触发其初始化。
    • 使用java.lang.reflect方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发器初始化
    • 初始化一个类时,发现其父类没有进行初始化,则需要先触发父类的初始化。
    • 虚拟机启动的时候,用户需要指定一个执行的主类(包含main方法的类),虚拟机会先对初始化这个主类
    • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法的句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(使用场景未知)

2. 类加载的过程:

2.1 加载:

  • 加载过程中虚拟机需要完成的几件事:
    • 通过一个类的全限定名类获取定义此类的二进制字节流–可从多种渠道获取(jar,war,网络等)
    • 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
    • 在内存中生成一个代表这个类的java.lang.Class对象,作为这个方法区这个类的各种数据的访问入口(HotSpot虚拟机将这个对象存储在方法区中,该对象将作为程序访问方法区中这些数据类型的外部接口)

2.2 验证:确保Class文件中的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 验证阶段大致会完成以下4个阶段的检验动作:
    • 文件格式验证:字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
      • 包括魔数校验:是否以0xCAFEBABE开头
      • 主次版本号是否在当前虚拟机的处理范围内
      • 。。。。
    • 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合java语言规范的要求。
      • 这个类是否有父类
      • 这个类的弗雷是否继承了不允许被继承的类(被final修饰的类)
      • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
      • 类中的字段,方法是否和父类产生矛盾(如覆盖率弗雷的final字段,或者出现不符合规则的方法的重载,例如方法参数都一致,但返回值类型却不相同等)
    • 字节码验证:通过数据流和控制流分析,确定程序的语义是否是合法的,符合逻辑的
      • 保证跳转指令不会跳转到方法体之外的字节码指令上
      • 保证方法体中的类型转换是有效的,例如可以把子类对象赋值给父类数据类型,这是安全的,但是不能吧父类对象赋值给子类数据类型,甚至是毫不相关的类型
      • 保证任意时刻操作数栈的数据类型和指令码序列都能配合工作。
    • 符号引用验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验, 这一阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个动作发生在连接的第三阶段(解析阶段)中发生。
      • 符号引用中通过字符串描述的全限定名是否能找到相应的类
      • 在指定的类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
      • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问

2.3 准备:正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配

  • 这个时候进行内存分配的只包括类变量(被static修饰的变量),并不包括实例变量,实例变量是在对象实例化时随对象一起分配在java堆中。
  • 分配的初始值为零值,假设一个变量定义为:public static int value = 123;则设置变量的初始值应该为0, 而不是123. 把value赋值为123的putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法之中,所以吧value赋值为123的动作将在初始化阶段才会执行。
  • 如果字段属性表中存在ConstantValue属性,那么在准备阶段会将value赋值为ConstantValue属性所指定的值
    • 例如:public static final int value = 123; 那么在准备阶段,则会将value赋值为123;

2.4 解析 :将符号引用转换成直接引用的过程

  • 符号引用和直接引用的定义:

    • 符号应用:符号引用是一组以符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用是能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在java虚拟机规范的Class文件格式中。符号引用在Class文件中以CONSTANT_Class_info(类或接口的符号引用)、CONSTANT_Fieldref_info(字段的符号引用)、CONSTANT_Methodref_info(方法的符号引用)等类型的常量出现。

      符号引用主要包括以下三类常量:

      • 类和接口的全限定名:java.lang.String的全限定名为:java/lang/String
      • 字段的名称和描述符:java.lang.String[][]二维数组的描述符为:[[Ljava.lang.String
      • 方法的名称和描述符:java.lang.String.toString()方法的描述符为:()Ljava.lang.String
    • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接应用,那引用的目标必定已经在内存中存在
  • 解析阶段的发生时间:

    • 虚拟机规范中并没有规定解析阶段发生的具体时间,值要求类在执行:anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
  • 解析针对的符号引用类型:
    • 解析动作主要针对:类或接口(CONSTANT_Class_info)、字段(CONSTANT_Fieldref_info)、类方法(CONSTANT_Methodref_info)、接口方法(CONSTANT_InterfaceMethodref_info)、方法类型(CONSTANT_MethodType_info)、方法句柄(CONSTANT_MethodHandle_info)和调用点限定符(CONSTANT_InvokeDynamic_info)7类符号引用进行。(CONSTANT_String_info也需要解析过程)
  • 解析符号引用过程分析:

    • 类或接口的解析(CONSTANT_Class_info)
      • 假设当前代码所处的类为D,如果要吧一个未解析过的符号引用N解析为一个类或接口C的符号引用,虚拟机需要完成以下3个步骤:
        1. 如果C不是一个数字类型,那虚拟机将会吧代表N的全限定名传递个D的类加载器去加载这个类C。
        2. 如果C是一个数组类型?
        3. 如果加载没有异常,那么C在虚拟机中实际已经成为一个有效的类或接口了,但在解析完成之前还需要进行符号引用的校验,确认D是否具备对C的访问权限。如果不具备会抛出java.lang.IllegalAccessError异常。
    • 字段解析:解析一个未被解析过的字段符号引用,首先会对字段所属的类或接口的符号引用进行解析。如果解析成功,则将这个字段所属的类或接口用C表示,虚拟机规范要求安装如下步骤对C进行后续字段的搜索:

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

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

    • 类方法解析:首先会对方法所属的类或接口的符号引用进行解析。如果解析成功,则将这个方法所属的类或接口用C表示,虚拟机规范要求安装如下步骤对C进行后续的类方法搜索:

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

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

    • 接口方法的解析:接口方法也需要先解析出接口方法表的class_index项中索引的方法所属的类或接口的符号引用。如果解析成功,则将这个方法所属的类或接口用C表示,虚拟机规范要求安装如下步骤对C进行后续的类方法搜索:

      • 与类方法的解析不同,如果在接口方法中发现class_index中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
      • 否则,在接口C中查找是否有简单名称和描述符都与目标相陪陪的方法,如果有则返回这个方法的直接引用,查找结束。
      • 否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围会包含Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
      • 否则,宣告查找失败,抛出java.lang.NoSushMethodError异常。

      接口中的所有方法默认都是public的,所以不存在访问权限问题,因此接口方法的符号解析应当不会出现java.lang.IllegalAccessError异常

2.5 初始化:初始化阶段才真正开始执行类中定义的java程序代码(字节码)

  • 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

    <clinit>()方法解析:

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

      public class Test{    static    {        i = 0;//给变量赋值可以正常通过        System.out.println(i);//这句话会编译提示”非法向前访问“    }}
    • <clinit>方法与类的构造器(或者说实例构造器<init>()方法)不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中的一个被执行的<clinit>()方法的类肯定是java.lang.Object。

    • 由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量的操作。
    • <clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
    • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法,但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
    • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,知道活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能会造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。