从 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. Kerberos委派攻击

    域委派 就是指将域内用户的权限委派给服务账号,使得服务账号能以用户的权限在域内展开活动. 在域中一般只有主机账号和服务账号才具有委派属性 主机账号:主机账号就是AD(活动目录)中Computers中的 ...

  2. c++ 跨平台线程同步对象那些事儿——基于 ace

    前言 ACE (Adaptive Communication Environment) 是早年间很火的一个 c++ 开源通讯框架,当时 c++ 的库比较少,以至于谈 c++ 网络通讯就绕不开 ACE, ...

  3. Send Excerpts from Jenkins Console Output as Email Contents

    Sometimes we need to send some excerpts from Jenkins console output (job logs) as email, such as tes ...

  4. DAY04 与用户交 互格式化输出与运算符

    与用户交互 输入: input # python2与python3的区别 # python3 res = input('please in put your username>>>& ...

  5. SQL 练习34

    求每门课程的学生人数 SELECT cid,COUNT(cid) 课程人数 from sc GROUP BY cid

  6. 【AI】Pytorch_预训练模型

    1. 模型下载 import re import os import glob import torch from torch.hub import download_url_to_file from ...

  7. git flow版本

    feature 分支:开发者进行功能开发的分支. develop 分支:对开发的功能进行集成的分支. release 分支:负责版本发布的分支. hotfix 分支:对线上缺陷进行修复工作的分支,热修 ...

  8. 容器平台(kubernetes)架构设计

    一眨眼距离上次发文好几年过去了,今天翻未读邮件看到博客有文章回复,猛然想起将博客遗忘在角落好几年了,赶紧访问博客.找回密码.翻翻文章,想写点什么但是又不知道从哪下手,N年前的第一篇文章是一个crm设计 ...

  9. HttpURLconnection的介绍

    一,HttpURLconnection的介绍 在Android开发中网络请求是最常用的操作之一, Android SDK中对HTTP(超文本传输协议)也提供了很好的支持,这里包括两种接口: 1.标准J ...

  10. 【springcloud】Eureka服务注册中心搭建

    转自:https://blog.csdn.net/pengjunlee/article/details/86538997 Spring Cloud是一系列框架的集合,它利用Spring Boot的开发 ...