JVM学习笔记(四):JVM类加载机制

引子

在上一篇文章《JVM学习笔记(三):Java内存模型》中,我总结了Java内存模型相关的一些知识。接下来,我将继续,参考着周志明老师的《深入理解Java虚拟机》,以及一些自己查阅的书籍、资料,总结一下JVM类加载机制相关的知识。

概述

一个类型从 被加载到虚拟机内存 开始,到 卸载出内存未知,它的整个生命周期将会经历 加载、验证、准备、解析、初始化、使用 和 卸载 七个阶段,其中 验证、准备 和 解析 三个阶段统称为 连接。如下图:

JVM学习笔记(四):JVM类加载机制_第1张图片

上图中,加载、验证、准备 和 初始化 五个阶段的顺序是一定的:类的加载过程必须按照这个顺序依次 开始 ,而解析阶段却不一定。某些情况下,解析 可能在 初始化 之后再进行。

上面这句话,对于标红的 开始 的理解:开始的意思是,五个阶段依次开始,但并不一定按照这个顺序 进行或完成。这几个阶段通常都是交叉地混合进行的,会在一个阶段执行的过程中调用、激活另一个阶段。

类加载的过程

上面简单介绍了讲了类加载涉及到的几个过程,下面就分别介绍一下。

1、加载

在加载阶段,Java虚拟机需要完成三件事情:

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

2、验证

验证 的主要目的是:确保 Class 文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。

验证阶段大致上会完成下面四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证。

1、文件格式验证。

这一阶段主要验证字节流是否符合Class文件格式的规范,并且要确保能被当前版本的虚拟机处理。只有在验证通过之后,字节流才被允许进入Java虚拟机内存的方法区中进行存储。

2、元数据验证

这一阶段的主要目的是对类的元数据信息进行语义检验。比如,这个类是否有父类(java.lang.Object 除外)等。

3、字节码验证

在第二阶段 元数据验证完成后,就要进行字节码验证,以保证检验类的方法在运行时不会做出危害虚拟机安全的行为。

4、符号引用验证

符号引用验证,主要就是校验 该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。符号引用验证的主要目的是确保 解析 行为能正常进行。

3、准备

准备阶段是 正式为类中定义的静态变量(static)分配内存并设置 初始值 的阶段。

注意,这里说的设置的初始值,一般情况下是数据类型的“零值”。比如:

private static int value = 123;

上面这条语句,在准备阶段完成后,value 的值是 0 ,而不是123。将 value 赋值为 123 的操作在 初始化阶段 才会被执行。

而对于 final 修饰的静态变量,则会在准备阶段就被设置为对应的值。比如:

private static final int value = 123;

上面这条语句,在 javac 编译时,将会为 value 属性生成 ConstantValue 属性,在准备阶段虚拟机就回根据 ConstantValue 将 value 赋值为 123。

4、解析

解析阶段是 Java虚拟机将常量池内的符号引用替换为直接引用的过程。

这里,我也没看太懂,回头补充吧。

5、初始化

在准备阶段,变量已经被赋过一次系统要求的“零值”,而在初始化阶段会根据我们所写的代码去初始化 变量 和 其他资源。

《Java虚拟机规范》中规定了有且仅有下面六种情况,如果类还没初始化,则必须对类先进行初始化:

  1. 使用new关键字实例化对象、读取或设置一个类的静态字段(final修饰的除外)、调用类的静态方法的时候。
  2. 使用 java.lang.reflect 包下的方法对类进行反射调用的时候。
  3. 当初始化一个类,发现其父类还没初始化时。
  4. 当虚拟机启动时,虚拟机需要指定一个执行的主类(包含main()方法),虚拟机会先初始化这个主类。
  5. 当一个接口中定义了 被 default 关键字修饰的接口方法时,在该接口的实现类初始化之前,该接口要先被初始化。
  6. 当使用 JDK7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

上面是 有且仅有,也就是说:

  • 通过子类引用父类的静态字段,不会导致子类初始化,只有父类会初始化。
  • 通过数组定义来引用类,不会触发此类的初始化。
  • 常量在编译阶段会被存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载器与双亲委派模型

实现“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作的代码被称为“类加载器”。

类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。也就是说,判断两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两类就必定不“相等”。这里的相等,指的是 euqals()、instanceof、isInstance() 等情况。

从虚拟机的角度看,只存在两种不同的类加载器:

  • 一种是启动类加载器(Bootstrap ClassLoader),(在HotSpot虚拟机中)这个类加载器使用C++语言实现,是虚拟机自身的一部分。
  • 另一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader

从开发人员角度,Java一直保持着三层类加载器、双亲委派模型架构。

三层类加载器

绝大多数的Java程序都会使用到以下三个系统提供的类加载器来进行加载:

1、启动类加载器(Bootstrap ClassLoader)

这个类加载器用于加载 /lib 目录下或者被 -Xbootclasspath 参数指定的路径下存放的、指定名称(比如 rt.jar、tools.jar)的 类库到虚拟机的内存中。名称不符合即使放在 lib 目录下也不会加载。

启动类加载器 无法被Java程序直接引用。如果我们在自定义类加载器时,需要把请求委派给引导类加载器去处理,那直接 返回 null 代替即可。

2、扩展类加载器(Extension ClassLoader)

扩展类加载器 负责加载 /lib/ext 目录中,或者被 java.ext.dirs 系统变量指定的路径中的所有类库。

扩展类加载器 所有代码都是Java实现的,开发人员可以直接使用它来加载Class文件。

3、应用程序类加载器(Application ClassLoader)

由于 应用程序类加载器 是 ClassLoader类中getSystemClassLoader()方法的返回值,有时候它也被称为“系统类加载器”。它负责加载用户类路径(ClassPath)上的所有类库。开发者同样也可以直接使用应用程序类加载器,一般情况下它也是程序中默认的类加载器。

双亲委派模型

JDK9之前的Java应用都是由这三种类加载器互相配合来完成加载的,用户还可以加入自定义的类加载器来进行拓展。这些类加载器之间的协作关系通常如下图所示:

JVM学习笔记(四):JVM类加载机制_第2张图片

上图中,各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”。

双亲委派模型要求 除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器。不过,类加载器之间的父子关系不是通过继承来实现的,而是通过 组合 来复用父加载器的代码。

双亲委派模型的工作流程是:如果一个类加载器收到了加载类的请求,他不会尝试首先自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该被传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到指定的类)时,子加载器才会尝试自己去完成加载。

使用双亲委派模型来组织加载器之间的关系,一个显而易见的好处就是:Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。例如 java.lang.Object 类,它存放于 rt.jar 中,无论哪一个类加载器要加载这个类,最终都会被委派到 启动类加载器中进行加载,因此 Object 类在程序的各种加载器环境中都能保证是同一个类。

总结

抄了一遍周老师的书,感觉又有很多新的收获~

参考文档

1、《深入理解Java虚拟机》第三版,周志明 著,第七章。

你可能感兴趣的:(Java)