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

时间:2021-09-17 10:45:41   收藏:0   阅读:49

从 1 开始学 JVM 系列

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

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

类生命周期

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

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

技术分享图片

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

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

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

1.加载

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

以盖房子为例,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 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的。即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。

技术分享图片

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

2.链接

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

3.初始化

静态字段的赋值

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

技术分享图片

初始化

类加载的最后一步是初始化,便是为标记为常量值的字段赋值和执行 < 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 方法,也不会触发初始化动作(加载了,但不初始化)。

流程概览

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

技术分享图片

END

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

技术分享图片

原文:https://www.cnblogs.com/alan-yin/p/15302644.html

评论(0
© 2014 bubuko.com 版权所有 - 联系我们:wmxa8@hotmail.com
打开技术之扣,分享程序人生!