类的加载过程

来源:互联网 发布:windows 10 rs1 14393 编辑:程序博客网 时间:2024/06/07 04:01

概述

  类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:
  

  解析阶段在某些情况下可以在初始化阶段之后再开始。

初始化的发生时间

  虚拟机规范没有规定什么情况下进行加载过程,但规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行初始化,则需要先出发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象、读写静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)、调用一个类的静态方法
  2. 使用java.lang.reflect包的方法对类进行反射调用
  3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(main()方法所在类),虚拟机会先初始化这个主类
  5. 当使用JDK1.7以上的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应类没有进行初始化,则触发其初始化

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

  1. 使用子类名调用父类的静态字段,不会触发子类的初始化
  2. New一个类的数组,不会触发类的初始化,但会触发类所对应的数组类的初始化
  3. 在类A读取类B的静态final常量字段,在编译类A的时候,已经将类B的常量字段值存储到类A的常量池,在编译完成之后,A读取字段时是直接从常量池读取,不需要引用类B

加载阶段

加载普通类(不包括数组)

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

加载普通类既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成。
数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。数组类的创建过程遵循以下规则:


加载阶段还未完成时,连接阶段可能已经开始(如一部分字节码文件格式校验动作),但是这两个阶段的开始时间仍然保持着固定的先后顺序。

验证阶段

  验证class字节流的合法性,如数组是否越界访问、是否将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的检验。如果验证失败,虚拟机就应抛出一个java.lang.VerfyError异常或其子类异常。
验证阶段大致上会完成4个阶段的校验动作:

  1. 文件格式验证:基于二进制字节流进行验证,只有通过这个阶段的验证后,字节流才会进入方法区进行存储,后面的3个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流。
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

准备阶段

  为类变量(static字段)分配内存并设置类变量初始值(0或null)的阶段,这些变量的内存在方法区中进行分配。
  在通常情况下初始值为零值,但如果static字段的字段属性表中存在ConstantValue属性(static字段被fianl修饰),则在准备阶段虚拟机就会根据ConstantValue的设置将value复制为常量值。

解析阶段

解析是将class常量池中的符号引用替换为直接引用的过程

符号引用:存放于class文件中,可以定位到目标的字面量,与虚拟机实现的内存布局无关,各种虚拟机实现的内存可以各不相同,但是它们能接受的符号引用是一致的
直接引用:直接执行目标的指针、相对偏移量或一个能间接定位到目标的句柄,与虚拟机实现的内存布局相关

  虚拟机规范之中并未规定解析阶段发生的具体时间,只要求在执行anewarry、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、ldc、ldc_w、multianewarry、new、putfield和putstatic这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来判断到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
  对同一个符号引用进行多次解析请求时很常见的事情,除invokedynamic之外,虚拟机实现可对第一次解析的结果进行缓存。

类或接口的解析(加载类或接口)

符号引用:常量池的CONSTANT_Class_info
如果该符号引用不是数组类型,则将全限定名传递给当前所在类(class文件代表的类)的类加载器去加载这个符号引用的类。

字段解析

符号引用:CONSTANT_Fieldref_info
1)先解析字段所在类(通过字段表的class_index找到类符号引用)
2)如果类本身就包含了简称名称和字段描述符都与目标相匹配的字段,则返回这个字段额直接引用
3)如果类实现了接口,则按照继承关系从下往上,寻找各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段
4)如果类不是Object类型,则按照继承关系从下往上搜索祖先类,如果类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段
5)如果查找失败,则抛出java.lang.NoSuchFieldError异常

类方法的解析

符号引用:CONSTANT_Methodref_info

接口方法的解析

符号引用:CONSTANT_InterfaceMethodref_info

方法类型的解析

符号引用:CONSTANT_MethodType_info

方法句柄的解析

符号引用:CONSTANT_MethodHandle_info

调用点限定符的解析

符号引用:CONSTANT_InvokeDynamic_info

初始化阶段

  初始化阶段是类加载的最后一步,此阶段开始执行Java程序代码(字节码)。在前面的多个阶段中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。
  在准备阶段类变量被初始化0或null,在初始化阶段执行类构造器方法clinit,clinit是由编译器自动收集类中的所有static变量的赋值动作和static语句块中的语句合并产生的。如果一个类中没有静态语句块,也没有对变量的赋值操作,那也就不会生成clinit方法。
  静态语句块中只能访问到定义在静态语句块之前的变量,定义在它们之后的变量,在前面的静态语句块可以赋值,但不能访问。
  
  
  clinit不需要调用父类构造器,虚拟机会保证在子类的clinit方法执行之前,父类的clinit已经执行完毕。在执行接口的clinit方法时,不需要先执行父接口的clinit方法,只有当父接口定义的变量使用时,父接口才会初始化。

原创粉丝点击