从 1 开始学 JVM 系列

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

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

类生命周期

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

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

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

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

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 >

初始化

类加载的最后一步是初始化,便是为标记为常量值的字段赋值和执行 < 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

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

从 1 开始学 JVM 系列 | JVM 类加载器(一)的更多相关文章

  1. Java虚拟机JVM学习05 类加载器的父委托机制

    Java虚拟机JVM学习05 类加载器的父委托机制 类加载器 类加载器用来把类加载到Java虚拟机中. 类加载器的类型 有两种类型的类加载器: 1.JVM自带的加载器: 根类加载器(Bootstrap ...

  2. JVM的艺术—类加载器篇(二)

    分享是价值的传递,喜欢就点个赞 引言 今天我们继续来深入的剖析类加载器的内容.上节课我们讲了类加载器的基本内容,没看过的小伙伴请加关注.今天我们继续. 什么是定义类加载器和初始化类加载器? 定义类加载 ...

  3. JVM的艺术—类加载器篇(三)

    JVM的艺术-类加载器篇(三) 引言 今天我们继续来深入的剖析类加载器的内容.上篇文章我们讲解了类加载器的双亲委托模型.全盘委托机制.以及类加载器双亲委托模型的优点.缺点等内容,没看过的小伙伴请加关注 ...

  4. jvm系列 (五) ---类加载机制

    类的加载机制 目录 jvm系列(一):jvm内存区域与溢出 jvm系列(二):垃圾收集器与内存分配策略 jvm系列(三):锁的优化 jvm系列 (四) ---强.软.弱.虚引用 我的博客目录 什么是类 ...

  5. 【JVM】JVM系列之类加载机制(四)

    一.前言 前面分析了class文件具体含义,接着需要将class文件加载到虚拟机中,这个过程是怎样的呢,下面,我们来仔细分析. 二.什么是类加载机制 把class文件加载到内存,并对数据进行校验.转换 ...

  6. Java虚拟机笔记 – JVM 自定义的类加载器的实现和使用2

    1.用户自定义的类加载器: 要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定类的名 ...

  7. jvm系列 (二) ---垃圾收集器与内存分配策略

    垃圾收集器与内存分配策略 前言:本文基于<深入java虚拟机>再加上个人的理解以及其他相关资料,对内容进行整理浓缩总结.本文中的图来自网络,感谢图的作者.如果有不正确的地方,欢迎指出. 目 ...

  8. JVM学习--(六)类加载器原理

    我们知道我们编写的java代码,会经过编译器编译成字节码文件(class文件),再把字节码文件装载到JVM中,映射到各个内存区域中,我们的程序就可以在内存中运行了.那么字节码文件是怎样装载到JVM中的 ...

  9. JVM启动过程 类加载器

    下图来自:http://blog.csdn.net/jiangwei0910410003/article/details/17733153 package com.test.jvm.common; i ...

随机推荐

  1. git的实用命令(撤回,合并)

    前言 在用开发项目的时候,经常会写着写着会发现写错的时候,人生没有后悔药,但是git有啊,大不了从头再来嘛. git的一些撤销操作 代码还没有存到暂存区 当我们修改了一个文件,还没有执行git add ...

  2. JavaScript学习02(js快速入门)

    快速入门 基本语法 JavaScript的语法和Java的语法类似,每个语句以;结束,语句块用{...}.但是JavaScrip并不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代 ...

  3. tomcat 配置http跳转https

    web.xml增加配置 <security-constraint> <web-resource-collection > <web-resource-name >S ...

  4. Mysql数据库优化(1)

    1.尽量不要留null select id from t where num is null,可以,但尽量不要留null,null也占空间:使用not null填充数据库,像varchar(100)这 ...

  5. Sqli-Labs less29-31

    Less-29 可以从介绍上看出,第29关被称为世界上最好的WAF,网上许多讲解的办法就是和第一关差不多,其实是不对的. sqli-labs文件夹下面还有tomcat文件,这才是真正的less,里面的 ...

  6. comm tools

    RTL:寄存器传输级别 LRM:语言参考手册 FSM:有限状态机 EDIF:电子数据交换格式 LSO:库搜索目录 XCF:XST 约束条件 1. par -ol. high  命令总是 '-'开头,参 ...

  7. SpringCloud升级之路2020.0.x版-20. 启动一个 Eureka Server 集群

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们的业务集群结构 ...

  8. visual studio code 中文

    1.按住ctrl+shift+p键,在框中输入configure,在下拉选项中选取language选项 2.打开locale.json文件,修改语言配置 3.修改完保存,然后重新启动vscode 4. ...

  9. .Net Core 踩坑记录--无法逐步调试类库文件

    前提 新建类库 在新项目中引用该类库 将类库对应的.PDB文件 拷贝至新项目的bin文件夹下 结果 无法进行跟踪调试 狗带 分析与解决 1: 打开 工具-->选项-->调试 2: 常规-- ...

  10. Qt简单的文件创建和读写

    1 QFile fp; //要包含必要的头文件,这里省略 2 QDir(dir); 3 QString path("./"),filename("test.txt&quo ...