虚拟机类加载机制

2019-01-23  本文已影响3人  ccoke

前言

从《类文件结构》一文,我们了解了类文件各数据项的组成,这些信息最终都需要被加载JVM中才能运行和使用。本文将为您讲述虚拟机的类加载过程。

本章知识点

类加载过程

类被加载到JVM内存中到从内存中卸载,会经历加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析3个阶段被统称为连接。一般来说,这7个阶段的顺序如下图所示:

类加载的阶段
上图中,加载、验证、准备、初始化、和卸载5个阶段的顺序是确定的,而解析阶段则不一定,为了支持Java语言动态绑定,在某些情况下会先执行初始化再执行解析。下面,我们来逐一看一下各阶段所执行的具体动作。

加载

在加载阶段,虚拟机需要完成以下3件事情:

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

在该阶段中,对于非数组类,是通过类加载器创建的,可以是系统提供的引导类加载器,也可以使用用户自定义的类加载器。对于数组类,是通过JVM直接创建的,但是数组类中的元素类型还是使用类加载器创建。
加载阶段完成后,虚拟机外部的二进制字节流按照虚拟机所需的格式存储在方法区之中,然后在方法区中实例化一个java.lang.Class类的对象,这个对象作为程序访方法区中的这些数据的外部接口。

验证

验证是虚拟机对自身保护的一项重要工作,大致会完成以下4个阶段的检验动作:

  1. 文件格式验证
    验证二进制字节流是否符合Class文件格式规范,并且能被当前版本的虚拟机处理,如是否以魔数0xCAFEBABE开头等。只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,后3个验证阶段全部是基于这一步,不会再直接操作字节流。
  2. 元数据验证
    对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,如这个类是否有父类,是否继承了不允许继承的类等。
  3. 字节码验证
    通过对数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,确保方法在运行时不会作出危害虚拟机安全的事件,如保证方法体中的类型转换是有效的等。
  4. 符号引用验证
    发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生,符号引用验证可以看做是对类自身以外的信息进行匹配性校验,如符号引用中通过全限定名是否能找到对应的类等。

准备

准备阶段是为类变量分配内存并设置类变量(被static修饰的变量)初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。需要注意以下情况:

public static int value = 123; //在准备阶段的值是0
public final static int value = 123; // 在准备阶段的值是123

解析

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

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

  1. 类或接口解析
    如果类或接口不是一个数组类型,则根据全限定名加载;如果是一个数组类型,会按照第1点的规则加载数组元素类型,接着虚拟机生成一个代表此数组维度和元素的数组对象;如果上述没有出现任何异常,还要判断是否具备访问权限。
  2. 字段解析
    在对字段解析前,会先对字段所属的类或接口(A)进行解析,再对字段符号引用进行解析。如果A中本身包含这个字段,则返回这个字段的直接引用;否则,如果A实现了类或接口,将会按照继承关系从下往上递归搜索类或接口,如果类或接口包含了这个字段,则返回这个字段的直接引用;如果上述没有出现任何异常,还要判断是否具备访问权限。
  3. 类方法解析
    在对类方法解析前,会先对方法所属的类(A)进行解析,再对类方法符号引用进行解析。如果A中本身包含这个方法,则返回这个方法的直接引用;否则,如果A实现了类,将会按照继承关系从下往上递归父类,如果类包含了这个方法,则返回这个方法的直接引用;否则,在A中实现的接口列表以及它们的父接口中递归查找是否包含这个方法,说明A是一个抽象类,排除java.lang.AbstractMethodError异常;如果上述没有出现任何异常,还要判断是否具备访问权限。
  4. 接口方法解析
    在接口方法解析前,会先对方法所属的接口(A)进行解析,再对接口方法符号引用进行解析。如果A中本身包含这个方法,则返回这个方法的直接引用;否则,在A中实现的接口列表以及它们的父接口中递归查找是否包含这个方法,则返回这个方法的直接引用;因为接口中所有方法默认是public的,所以不需要判断权限。

初始化

在该阶段,会根据程序员通过程序制定的主观计划去初始化类变量和其他资源。JVM规范对类初始化有着严格的规定,有且只有出现以下5种主动引用的情况,才会进行类的初始化:

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候。
  3. 当初始化一个类,发现其父类没有初始化,则需要先触发父类的初始化。
  4. 当虚拟机启动时,需要指定一个要执行的主类,虚拟机会先初始化这个类。
  5. 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

所有引用类的方法都不会触发初始化,称为被动引用。如:

  1. 对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会除法父类的初始化而不会触发子类的初始化。
  2. 当由类A生成数组时,在定义时,只会执行newarray指令,不会初始化多个类A。
  3. 当外部类A引用类B中的常量时,并不会造成B的初始化,因为在编译阶段,常量已经被存储到了NotInitialization类的常量池中,以后的每次调用,都是引用常量池中的数据。

接口初始化与类不同的是,并不要求父接口全部都完成初始化。

类加载器

“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到JVM外部实现,以便让应用程序自己决定如何区获取所需要的类,实现这个动作的代码模块称为“类加载器”。比较两个类是否相等,只有在着两个类是由同一个类加载器加载的前提下才有意义。

双亲委托模型

对于绝大部分Java程序都会用到以下3种类加载器:

类加载器的双亲委托模型如下图所示,这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。


类加载器双亲委托模型

类加载器的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委托给父类加载器去完成,循环反复,最终委托到启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(搜索范围没有找到所需的类),子加载器才会尝试自己去加载。这样做的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

总结

类的加载过程:加载、验证、准备、解析、初始化、使用、卸载,其中验证、准备、解析三个阶段被统称为连接加载阶段负责将类的二进制字节流转换为方法区的数据结构并存入方法区,并实例化一个java.lang.Class类的对象作为外部接口供程序访问;验证阶段会对文件格式、元数据、字节码、符号引用进行验证来保护JVM;准备阶段用来为类的静态变量赋初值;解析阶段负责将类中的符号引用转为直接引用;初始化阶段负责按照程序员的计划初始化变量或其他资源。
通常类加载器从“辈份”分为启动类加载器扩展类加载器应用程序加载器,类的加载过程是先找父加载器,直到父加载器反馈无法完成加载,子加载器才会尝试区加载,形成的模型是双亲委托模型,这样的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系

...
// hey guy!
if( isValuable(this.article) && (like(this.article) || follow("ccoke"))) {
  System.out.println("Thank you! XD");
} else {
  System.out.println("I will continue to work hard!T.T");
}
...
上一篇下一篇

猜你喜欢

热点阅读