Java虚拟机--虚拟机类加载机制

虚拟机类加载机制

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

类的生命周期如下:

加载 --> 验证 --> 准备 --> 解析 --> 初始化 --> 使用 --> 卸载

其中验证、准备、解析三个阶段属于连接过程。解析可以发生在初始化之后,这是因为Java支持动态绑定,或者说运行时绑定。

发生下面5种情况,必须立即对类进行初始化:

  • 遇到new、getstatic、putstatic或invokestatic这些字节码指令。具体来说就是使用了new关键字实例化对象,读取或设置一个类的静态字段(被final修饰、已在编译期吧结果放入常量池的静态字段除外,如public final static String HELLO = "Hello";
  • java.lang.reflect包的方法对类进行反射调用时,若类还没有初始化,则需要先触发其初始化。
  • 初始化一个类时,若这个类有父类,必须先对父类进行初始化
  • 虚拟机启动,用户需指定一个要执行的主类(含main方法的那个类),虚拟机会先初始化这个主类,该类时程序的入口。
  • 一个java.lang.invoke.MethodHandle实例最后解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法句柄对应的类还没有初始化,则需要先触发其初始化。

以上5种情况,称为对一个类的主动引用。其余情况都被称为被动引用。来看几中被动引用的情况;

  • 子类继承了父类的某个静态字段,当通过子类访问这个继承自父类的静态字段时,只会触发父类的初始化而不会触发子类的初始化,只有直接定义了这个字段的类才会被初始化。
  • 通过数组定义来引用类,不会触发此类的初始化,如MyObject[] objs = new MyObject[10]
  • 常量请在编译阶段直接存入调用类的常量池中,本质上没有直接引用到定义常量的类,不会触发类的初始化。如MyObject类中有一个静态字段public final static String HELLO = "Hello",当调用HELLO时,并不会触发初始化。

接口也有初始化过程,和类的初始化的区别在于:接口在初始化时,不要求求父接口必须先初始化完毕,而是等用到父接口时,在初始化父接口。

加载

加载阶段要完成以下三件事:

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

非数组类的加载可以使用系统提供的引导类加载器,也可以使用自定义的类加载器去完成。

数组类本身不通过类加载器创建,有Java虚拟机直接创建,但是数组类的元素类型(如Object[]中的Object,Object[][]中的Object)最终还是由类加载器创建。

元素类型分两种,引用类型和基本类型。

  • 若数组的组件类型(数组去掉一个维度的类型)是引用类型,则递归加载这个组件类型,数组类将在加载该组件类型的类加载器的类名称空间上被标识。
  • 若数组的组件类型不是引用类型,如int[],Java虚拟机将会把数组类标记为与引导类加载器关联。
  • 数组类的可见性与它的组件类型的可见性一致,若组件类型不是引用类型,那数组的可见性默认是public。

验证

验证阶段包括以下4个检验:

  • 文件格式验证。主要检查字节流是否符合Class文件的规范,如是否以0xCAFEBABE开头,主、次版本号是否在当前虚拟机处理范围之内等...
  • 元数据验证。主要是对字节码描述的信息作语义分析,如检查该类是否有父类,这个类的父类是否继承了不允许继承的类。类中的字段、方法是否和父类产生矛盾等...
  • 字节码验证。通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。该阶段对类的方法体进行校验分析,保证Java虚拟机的安全。如保证跳转指令不会跳转到方法体以外的字节码指令上,保证方法体中的类型转换有效等...
  • 符号引用验证。可以看作是对类自身以外(常量池中各种符号引用)的信息进行匹配性校验。如符号引用中通过字符串描述的全限定名是否能找到对应的类,符号引用中的类、字段、方法的访问性(public、private等)是否可以被当前类访问。

准备

正式为类变量分配内存并设置类变量(被static修饰)的初始值,初始值一般为“零值”,比如对于int型来说是0,对于boolean来说是false。下面的例子:

public static int val = 9;

准备阶段后初始值为0而不是9,val被赋值为9是在初始化阶段

但如果是

public final static int val = 9;

因为有final修饰,所有在准备阶段就会将val赋值为9。

解析

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

  • 符号引用。以一组符号来描述锁引用的目标,,符号可以是任何形式的字面量请,只要使用时能无歧义地定位到目标即可。
  • 直接引用。可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、和调用点限定符7类符号引用进行。

类或接口的解析

如果当前类是D,要把一个从未解析过的符号引用N解析成一个类或者接口的直接引用,需要如下三个步骤:

  • C不是一个数组类型。虚拟机会将代表N的全限定名传递给D的类加载器去加载C。如果C有父类或实现的接口,还会触发额外的加载。
  • C是一个数组类型,且元素类型是对象,如果Integer[10]。则按照上述规则加载数组元素类型(在这个例子中就是Integer),然后会由虚拟机生成一个代表此数组维度和对象(在这里分别是10和Integer)的数组对象。
  • 上述过程如果没有异常,C在虚拟机中已经成为一个有效的类或接口了。但在解析完成前,还要检查D是否具备对C的访问权限,否则会抛出异常。

字段解析

首先会解析字段所属的类或接口的符号引用。如果解析成功,将字段所属的类或接口用C表示。之后对C进行后续字段的搜索

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

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

类方法解析

首先会解析方法所属的类或接口的符号引用。如果解析成功,将字段所属的类用C表示。之后对C

  • 类方法和接口方法的符号引用的常量定义是分开的,若在类方法表中发现class_index中索引的C是一个接口,抛出异常。
  • 如果通过上面的步骤,和字段解析一样,先在类C中查找,如果有简单名称和描述符斗鱼目标相匹配的方法,则返回这个方法的直接引用,查找结束;
  • 否则,在类C的父类中递归查找;
  • 否则,在类C实现的接口列表以及它们的父接口总递归查找,如果存在匹配的方法,说明C是一个抽象类,抛出异常,查找结束。
  • 否则,查找失败。

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

接口方法解析

首先会解析方法所属的类或接口的符号引用。如果解析成功,将字段所属的接口用C表示。之后对C

  • 若在接口方法表中发现class_index中索引的C是一个类,抛出异常。
  • 否则,在接口C中查找;
  • 否则,在接口C的父接口中递归查找,直到java.lang.Object为止(包括了Object)
  • 否则,查找失败。

因为接口中的方法都是public,所以不存在像上面一样的访问权限问题。

初始化

初始化阶段才真正开始执行类中定义的Java程序代码。初始化阶段是执行类构造器<clinit>()方法的过程(不是实例构造器<init>())。

public static int val = 9;

在准备阶段val是0,而在初始化阶段val才被赋值为9。

  • <clinit>()的产生:有编译期自动收集类中的所有类变量的赋值动作和静态语句块即static{}块的语句合并产生。自动收集的顺序和语句在文件中出现的顺序一致
  • 类构造器<clinit>()不同于<init>(),不需要显式调用父类构造器,虚拟机保证子类的<clinit>()在执行前,父类的<clinit>()已执行完毕;
  • <clinit>()对于类或接口不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,就不会为这个类生成<clinit>()方法。
  • 父类的<clinit>()先执行,因此父类的静态静态语句块会先于子类的执行;
  • 接口中不能使用静态语句块,也会生成<clinit>()方法,但是和类不同的是,执行接口的<clinit>()不会触发父类接口的<clinit>(),只有当使用到父类中定义的变量时,父类才会初始化;
  • 在多线程中,如果有多个线程同时初始化一个类,只会有一个县城能执行这个类的<clinit>()方法,其他线程会阻塞,直到该线程的<clinit>()执行完毕。

类加载器

类加载器:通过一个类的全限定名来获取描述此类的二进制字节流。

任何一个类,都需要由加载它的类加载器和类本身一同确定在Java虚拟机的唯一性,每一个类加载器都拥有一个独立的类名称空间,通俗地说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器的前提下才有意义。即使两个类来自同一个Class文件,且被同一个Java虚拟机加载,只要它们不是被同一个类加载器加载的,这两个类就不相等。

双亲委派模型

对JVM来讲,只存在两种不同的类加载器:

  • 启动类加载器,是虚拟机的一部分;
  • 其他类加载器,独立于虚拟机外部,都继承自java.lang.ClassLoader

再细分的话

  • 启动类加载器,将
  • 扩展类加载器,负责加载
  • 应用程序加载器,负责加载器用户类路径ClassPath上指定的类库,通常是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,这些类加载器的关系一般是以双亲委派模型呈现。双亲委派模型:除了顶层的启动类加载器外,其余类加载器都应当有自己的父类加载器。这里的父子关系一般是以组合关系而不是继承。

20160506184936657

双亲委派模型的工作过程:一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是委派给父类去完成,所以最终的请求被传送到了顶层的启动类加载器中;只有当父类反馈自己无法完成这个加载请求时,子类才会尝试自己去加载。

双亲委派模型的优点:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。比如java.lang.Object类,存放在rt.jar中,无论哪一个类加载器都要加载这个类,最后都委派给了顶端的启动类加载器进行加载了,所以Object类在程序的各种加载器环境种都是同一个类

双亲委派模型的实现:先检查是否被加载过,若没有就调用父加载器的loadClass()方法,若父加载器为空就使用启动类加载器作为父加载器,如果父加载器加载失败,抛出异常后,在调用自己的findClass()进行加载。


by @sunhaiyu

2018.6.11

猜你喜欢

转载自www.cnblogs.com/sun-haiyu/p/9204282.html