ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

从 1 开始学 JVM 系列 | JVM 类加载器(一)

2021-09-17 08:34:50  阅读:115  来源: 互联网

标签:初始化 Java 触发 虚拟机 JVM 系列 class 加载


从 1 开始学 JVM 系列

类加载器,对于很多人来说并不陌生。我自己第一次听到这个概念时觉得有点“高大上”,觉得只有深入 JDK 源码才会触碰到 ClassLoader,平时都是传闻中的东西。

今天,就让我们一起来探索一下这”传闻“中的类加载器,看看它是何方神圣。

类生命周期

在正式聊类加载器之前,我们先正本清源,看看类的生命周期是什么样的。

为了方便后续解读,下面我贴了一张图展示了类的生命周期的 7 个步骤。

image

对于前 5 步,简单来说就是加载、链接、初始化,这是一个类最关键的加载步骤。

对照着上图,我们逐一来解释一下。

  1. 加载(Loading):找 Class 文件
  2. 验证(Verification):验证格式、依赖
  3. 准备(Preparation):静态字段、方法表
  4. 解析(Resolution):符号解析为引用
  5. 初始化(Initialization):构造器、静态变量赋值、静态代码块
  6. 使用(Using)
  7. 卸载(Unloading)

1.加载

所谓的加载,就是查找字节流,并根据字节流创建类的过程

  • 对于数组类,它没有对应的字节流,是由 Java 虚拟机直接生成的。
  • 对于其他的类,Java 虚拟机需要借助类加载器来完成查找字节流的过程。

以盖房子为例,Jack 想要要盖个房子,按照流程他要先找个建筑师,跟他说想要设计一个房型,比如说“一房一厅两卫”。这里的房型就相当于类,而建筑师就相当于类加载器。

启动类加载器

建筑界有许多的建筑师,他们等级分明,但都有着共同的祖师爷,叫「启动类加载器(boot class loader)」。由于启动类加载器是由 C++ 实现的,没有对应的 Java 对象,因此在 Java 中只能用 null 来指代。

// jdk中 BootstrapClass 是 native 实现
private native Class<?> findBootstrapClass(String name);

但是,祖师爷不喜欢像 Jack 这样的小角色来打扰他,所以谁也没有祖师爷的联系方式,也就相当于 null 指代。

除了启动类加载器之外,其他的类加载器都是 java.lang.ClassLoader 的子类,有对应的 Java 对象。这些类加载器需要先由另一个类加载器,比如说启动类加载器,加载至 Java 虚拟机中,方能执行类加载。

双亲委派模型

建筑师界有个潜规则:接到单子后自己不能着手干,得先给师傅过过目。师傅不接手的情况下,才能自己来。即等级高的师傅有优先选择权。

在 Java 虚拟机中,这个潜规则就是「双亲委派模型」。每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到请求的类时,这个类加载器才会尝试去加载。

加载器类型

加载器类型(Java 9 之前) 作用 加载路径
启动类加载器 负责加载最为基础、最为重要的类 比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)
扩展类加载器 (extension class loader) 父类加载器是启动类加载器。它负责加载相对次要、但又通用的类 比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)
应用类加载器 (application class loader) 父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。 默认情况下,应用程序中包含的类便是由应用类加载器加载的 这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。

Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为「平台类加载器(platform class loader)」。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。

除了由 Java 核心类库提供的类加载器外,我们还可以加入「自定义类加载器」,实现特殊的加载方式。

举个例子,我们可以对 class 文件进行加密,加载时再利用自定义类加载器对其解密。

类加载器的命名空间

除了加载功能之外,类加载器还提供了「命名空间」的作用。

打个比方,假设建筑界不讲版权,如果某个人剽窃了另一个建筑师的设计作品,只要你标上自己的名字,这两个房型就是不同的。

在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。

image

在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

2.链接

链接,是指将创建好的类合并至 Java 虚拟机中,使之能够执行的过程。它可分为验证、准备以及解析三个阶段。

    1. 「验证」阶段的目的,在于确保被加载类能够满足 Java 虚拟机的约束条件

    这就好比 Jack 需要将设计好的房型提交给市政部门审核。只有当审核通过,才能继续下面的建造工作。

    通常而言,Java 编译器生成的类文件必然满足 Java 虚拟机的约束条件。

  • 2.「准备」阶段的目的,则是为被加载类的静态字段分配内存。Java 代码中对静态字段的具体初始化,则会在稍后的初始化阶段中进行。

    过了这个阶段,算是盖好了毛坯房。虽然结构已经完整,但没有装修之前不能住人。

    除了分配内存外,部分 Java 虚拟机会在此阶段构造其他跟类层次相关的数据结构,比如说用来实现虚方法的动态绑定的方法表。

    在 class 文件被加载至 Java 虚拟机之前,这个类无法知道其他类及其方法、字段所对应的具体地址,甚至不知道自己方法、字段的地址。因此,每当需要引用这些成员时,Java 编译器会生成一个「符号引用」。在运行阶段,这个符号引用一般都能够无歧义地定位到具体目标上。

    举个例子,对于一个方法调用,编译器会生成一个包含目标方法所在类的名字、目标方法的名字、接收参数类型以及返回值类型的符号引用,来指代所要调用的方法。(即方法签名)

  • 3.「解析」阶段的目的,正是将这些符号引用解析成为实际引用

    如果符号引用指向一个未被加载的类,或者未被加载类的字段或方法,那么解析将触发这个类的加载(但未必触发这个类的链接以及初始化。)

    • 符号引用就好比“Jack 的房子”这种说法,不管它存在不存在,我们可以用这种说法指代 Jack 的房子。

    • 实际引用则好比实际的通讯地址,如果我们想要与 Jack 通信,则需要启动盖房子的过程。

    Java 虚拟机规范并没有要求在链接过程中完成解析。它仅规定了:如果某些字节码使用了符号引用,那么在执行这些字节码之前,需要完成对这些符号引用的解析。

3.初始化

静态字段的赋值

Java 中如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

  • 如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成「常量值(ConstantValue)」,其初始化直接由 Java 虚拟机完成
  • 除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >

image

初始化

类加载的最后一步是初始化,便是为标记为常量值的字段赋值和执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次

只有当初始化完成之后,类才正式成为可执行的状态。

在盖房子的例子中,相当于房子装修好了,Jack 可以真正拎包入住了。

那么,类的初始化何时会被触发呢?

JVM 规范枚举了下述多种触发情况:

  1. 当虚拟机启动时,初始化用户指定的主类,就是启动执行的 main 方法所在的类;

  2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

  3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;

  4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;

  5. 子类的初始化会触发父类的初始化;

  6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

    和继承类似(5、6 条都是面向对象)

  7. 使用反射 API 对某个类进行反射调用时,初始化这个类;

  8. 初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

    和反射类似(7、8 条都是反射相关)

// 单例延迟初始化例子
public class Singleton {
  private Singleton() {}
  private static class LazyHolder {
    static final Singleton INSTANCE = new Singleton();
  }
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
}

只有当调用 Singleton.getInstance 时,程序才会访问 LazyHolder.INSTANCE,才会触发对 LazyHolder 的初始化(对应第 4 种情况),继而新建一个 Singleton 的实例。

由于类的初始化线程安全,并且仅被执行一次,因此程序可以确保多线程环境下有且仅有一个 Singleton 实例。

那么,什么时候不会初始化,但可能会加载?

  1. 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  2. 定义对象数组,不会触发该类的初始化。

    直到 new 才触发

  3. 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

    常量不是变量

  4. 通过类名获取 Class 对象,不会触发类的初始化,Hello.class 不会让 Hello 类初始化。

  5. 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。Class.forName (“jvm.Hello”)默认会加载 Hello 类。

  6. 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作(加载了,但不初始化)。

流程概览

为了方便查看,我画了一张流程图演示上面的步骤。

image

END

如果你觉得有用,欢迎关注 「小尹探世界」 微信公众号,希望我们一起打造一个有知识、有温度、有趣点、有价值的频道,探索技术之外的广袤世界。

类的唯一性

标签:初始化,Java,触发,虚拟机,JVM,系列,class,加载
来源: https://www.cnblogs.com/alan-yin/p/15302644.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有