# 前言

在 JVM 综述里面,我们说,JVM 做了三件事情,Java 程序的内存管理,

Java Class 二进制字节流的加载(ClassLoader),Java 程序的执行(执行引擎)。我们也说,我们大部分情况下只关注前2个。在前面的文章中,我们已经分析了内存关系相关的,包括运行时数据区,GC 相关。今天我们要讲的就是类加载器。

JVM 综述 里,我们已经大致分析了一些概念。而今天的文章将详细的阐述类加载器。

首先,我们要了解类加载器,当然,了解的目的是为了更好的开发,通过对类加载器的解读,看看我们能不能做些什么,比如修改类加载器的加载逻辑,比如加入自定义的类加载器等等功能。

让我们开始吧!

# 1. 类加载器介绍

对于 Java 虚拟机来说,Class 文件是一个重要的接口,无论使用何种语言进行软件开发,只要能将源文件编译为正确的 Class 文件,那么这种语言就可以在 Java 虚拟机上运行。可以说,Class 文件就是虚拟机的基石。

如图所示:

从上图可以看出,虚拟机不拘泥于 Java 语言,任何一个源文件只要能编译成 Class 文件的格式,就可以在JVM 上运行!Class 文件格式就像是一个接口,只要遵守这个接口,就能够在 JVM 上运行。

# 2. 类加载器的工作流程

Class 文件通常是以文件的方式存在(任何二进制流都可以是 Class 类型),但只有能被 JVM 加载后才能被使用,才能运行编译后的代码。系统装在 Class 类型可以分为加载,链接和初始化三个步骤。其中,链接也可分为验证,准备和解析3步骤。如图所示:

其中,只有加载过程是程序员能够控制的,后面的几个步骤都是有虚拟机自动运行的。因此,我们的关注点主要放在加载阶段。

# 3. 类加载流程中的 “加载”

上面说了,类加载器3个流程中,唯一能让程序员 “做手脚” 的就是加载过程,上面是加载过程呢?其主要作用就是从系统外部获得 Class 二进制数据流。

JVM 不会无故装载 Class 文件,只有在必要的时候才装载,哪几个时候呢?

  1. 当创建一个类的实例是,比如使用 new 关键字,或者通过反射,克隆,反序列化。
  2. 当调用类的静态方法时,即当使用字节码 invokstatic 指令。
  3. 当使用类或接口的静态字段时(final 常量除外),比如,使用 getstatic 或者 pustatic 指令。
  4. 当时用 Java.lang.reflect 包中的方法反射类的方法时。
  5. 当初始化子类,要求先初始化父类。
  6. 作为启动虚拟机,含有 main()方法的那个类。

以上6种情况属于主动调用,主动调用会触发初始化,还有一种情况是被动调用,则不会引起初始化。

# 3.1 ClassLoader 抽象类介绍

Java 类加载器的具体实现就在 java.lang.ClassLoader,该类是一个抽象类,并且提供了一些重要的接口,用于自定义Class 的加载流程和加载方式。主要方法如下:

  1. public Class<?> loadClass(String name) throws ClassNotFoundException

    给定一个类名,加载一个雷,返回代表这个类的 Class 实例,如果找不到类,则返回异常。

  2. protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError

    根据给定的字节码流 b 定义一个类,off 表示位置,len 表示长度。该方法只有子类可以使用。

  3. protected Class<?> findClass(String name) throws ClassNotFoundException

    查找一个类,也是只能子类使用,这是重载 ClassLoader 时,最重要的系统扩展点。这个方法会被 loadClass 调用,用于自定义查找类的逻辑,如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。

  4. protected final Class<?> findLoadedClass(String name)

    同样的,这个方法也只有子类能够使用,他会去寻找已经加载的类,这个方法是 final 方法,无法被修改。

同时,在该类中,还有一个字段非常重要:parent,他也是一个 ClassLoader 的实例,这个字段所表示的 ClassLoader 也称为这个 ClassLoader 的双亲,在类加载的过程中,ClassLoader 可能会将某些请求交给自己的双亲处理。

# 3.2 类加载器的双亲委派模型

在标准的 Java 程序中,从虚拟机的角度讲,只有2种类加载器:

  1. 启动类加载器(BootStrap ClassLoader),C++ 语言实现,虚拟机自身的一部分
  2. 另一种就是所有其他的类加载器,由 Java 语言实现,独立于虚拟机外部,并且全部继承自抽身类 java.lang.ClassLoader。

从程序员的角度讲,虚拟机会创建 3 中类加载器,分别是:Bootstrap ClassLoader(启动类加载器),Extension ClassLoader(扩展类加载器)和 APPClassLoader(应用类加载器,也称为系统类加载器)。此外,每一个应用程序还可以拥有自定义的 ClassLoader,扩展 Java 虚拟机获取 Class 数据的能力。

而这 3 个类加载器有着层次关系。

先来看一个著名的图:

如图所示:从 ClassLoader 的层次自顶向下为启动类加载器,扩展类加载器,应用类加载器和自定义类加载器,当系统需要适用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断,但系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。

注意,我们无法访问启动类加载器,当试图获取启动类加载器的时候,返回 null,因此,如果返回的是 null,并不意味没有类加载器为它服务,而是指哪个类为启动类加载器。

那么这些类加载路径是哪些呢?

  1. BootStrap 类加载器负责加载 <JAVA_HOME>/lib 目录中的,或者别-Xbootclasspath 参数指定的路径。并且是被虚拟机识别的,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会加载。

  2. 扩展类加载器有 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>/lib/ext 目录中的。或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。

  3. 应用类加载器由 sun.misc.Launcher$AppClassLoader 实现,由于这个类是 ClassLoader 中的 getSystemClassLoader 方法的返回值,也称为系统类加载器,负载加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。一般情况下,这个就是程序中默认的类加载器。

  4. 自定义类加载器用于加载一些特殊途径的类,一般也是用户程序类。

系统中的 ClassLoader 在协同工作时,默认会使用双亲委托模式,即在类加载的时候,系统会判断当前类是否已经被加载,如果已经加载,则直接返回,否则就尝试加载,在尝试加载时,会先请求双亲处理,如果双亲查找事变,则自己加载。代码如下:

 protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
} if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

代码中,如果双亲是 null,则使用启动类加载器加载,如果事变,则使用当前类加载器加载。

双亲为 null 一般有2种情况,1. 双亲是启动类加载器。2. 自己就是启动类加载器。

其中加载类的逻辑有2个注意的地方。

  1. 判断是否已经加载?当判断类是否需要加载时,是从底层开始判断,如果底层已经加载了,则不再请求双亲。

  2. 当系统准备加载一个类时。会先从双亲加载,也就是最顶层的启动类加载器,逐层向下,直到找到该类。和上面的是相反的。

# 3.3 类加载器的双亲委派模型缺陷和补充

双亲模型固然有着优点,能够让整个系统保持了类的唯一性。但在有些场合,却不适合,也就是说,顶层的启动类加载器的代码无法访问到底层的类加载器。如 rt.jar 无法中代码无法访问到应用类加载器。

你肯定要问,为什么需要访问呢?

在 Java 平台中,把核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,通常可以称为 Service Provider Interface,即 SPI。

在 rt.jar 中的抽象类需要加载继承他们的在应用层的子类实现,但是以目前的双亲机制是无法实现的。

因此 JDK 引用了一个不太优雅的设计,上下文类加载器。也就是讲类加载放在线程上下文变量中。通过 Thread.getContextClassLoader(), Thread.setContextClassLoader(ClassLoader) 这两个方法获取和设置 ClassLoader,这样,rt.jar 中的代码就可以获取到底层的类加载了。

# 3.4 突破双亲模式

双亲模式是虚拟机的默认行为,但并非必须这么做,通过重载 ClassLoader 可以修改该行为。事实上,很多框架和软件都修改了,比如 Tomcat,OSGI。具体实现则是通过重写 loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载。

# 4. 类加载的扩展---热替换

我们知道:由不同的 ClassLoader 加载的同名类属于不同的类型,不能相互转化和兼容。

而这个特性就是我们实现热替换的关键。过程如图所示:

# 总结

好了,到这里,基本的类加载器就介绍结束了。我们总结了类加载的工作流程,包括加载,连接,初始化。然后我们重点介绍了加载,因为加载阶段是我们程序员唯一有所作为的地方。然后介绍了加载阶段的一些细节,比如双亲委派,然后说了双亲委派的缺点和补充,然后探讨了如何修改默认的类加载方式,最后通过类加载的特性实现了热替换。当然也看了核心类 ClassLoader 的源码。不过,这肯定不是类加载器的全部。我们将在后面的文章中将类加载的其他特性一一解开。

good luck!!!!

深入浅出 JVM ClassLoader的更多相关文章

  1. jvm classLoader architecture :

    jvm classLoader architecture : a.Bootstrap ClassLoader/启动类加载器 主要负责jdk_home/lib目录下的核心         api 或 - ...

  2. 深入浅出 JVM GC(3)

    # 前言 在 深入浅出 JVM GC(2) 中,我们介绍了一些 GC 算法,GC 名词,同时也留下了一个问题,就是每个 GC 收集器的具体作用.有哪些 GC 收集器呢? Serial 串行收集器(只适 ...

  3. 深入浅出 JVM GC(2)

    # 前言 在 深入浅出 JVM GC(1) 中,限于上篇文章的篇幅,我们留下了一个问题 : 如何回收? 这篇文章将重点讲述这个问题. 在上篇文章中,我们也列出了一些大纲,今天我们就按照那个大纲来逐个讲 ...

  4. 【深入浅出-JVM】(76):classloader

    方法 public Class<?> loadClass(String name) throws ClassNotFoundException 通过类名发挥这个类的Class实例 prot ...

  5. JVM ClassLoader加载过程

    虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是Java虚拟机的类加载机制. 1)三个类加载器: bootstra ...

  6. 阿里架构师带你深入浅出jvm

    本文跟大家聊聊JVM的内部结构,从组件中的多线程处理,JVM系统线程,局部变量数组等方面进行解析 JVM JVM = 类加载器(classloader) + 执行引擎(execution engine ...

  7. 对JVM还有什么不懂的?一文章带你深入浅出JVM!

    本文跟大家聊聊JVM的内部结构,从组件中的多线程处理,JVM系统线程,局部变量数组等方面进行解析 JVM JVM = 类加载器(classloader) + 执行引擎(execution engine ...

  8. 深入浅出 JVM GC(4)常用 GC 参数介绍

    # 前言 从前面的3篇文章中,我们分析了5个垃圾收集器,还有一些 GC 的算法,那么,在 GC 调优中,我们肯定会先判断哪里出现的问题,然后再根据出现的问题进行调优,而调优的手段就是 JVM 提供给我 ...

  9. 深入浅出 JVM GC(1)

    # 前言 初级 Java 程序员步入中级程序员的有一个无法绕过的阶段------GC(Garbage Collection).作为 Java 程序员,说实话,很幸福,不用像 C 程序员那样,时刻关心着 ...

随机推荐

  1. 【轻松前端之旅】<a>元素妙用

    浏览器读取服务器内容时,通过URL(包含:协议+域名+绝对路径)如:https://www.baidu.com/index.html浏览器从本地读取内容时,会用file协议.如:file:///E:/ ...

  2. poj3304(是否存在一条直线与所有给出线段相交

    题意:给出n条线段,问你是否存在一条直线让他与所有线段相交. 思路:枚举两条直线的起点和终点做一条直线,看他是否与所有线段相交. #include<cstdio> #include< ...

  3. python发送邮箱

    一.Python发送邮件 import smtplib from email.mime.text import MIMEText def send(email_to,title,content): m ...

  4. OpenCV图像分割1

    1.基于阈值 1.1原理 灰度阈值化,假设输入图像为f,输出图像为g,则阈值化公式如下: g(i,j)=1  当f(i,j)>=T g(i,j)=0 当f(i,j)<T 1.2适用范围 当 ...

  5. 微服务日志之.NET Core使用NLog通过Kafka实现日志收集

    一.前言 NET Core越来越受欢迎,因为它具有在多个平台上运行的原始.NET Framework的强大功能.Kafka正迅速成为软件行业的标准消息传递技术.这篇文章简单介绍了如何使用.NET(Co ...

  6. 背水一战 Windows 10 (89) - 文件系统: 读写文本数据, 读写二进制数据, 读写流数据

    [源码下载] 背水一战 Windows 10 (89) - 文件系统: 读写文本数据, 读写二进制数据, 读写流数据 作者:webabcd 介绍背水一战 Windows 10 之 文件系统 读写文本数 ...

  7. 准备情人节礼物比写代码难?来看看IT直男给女友们的礼物

    今天是情人节,据说IT直男在每个这样弥漫着恋爱气息的日子里都能把礼物送成"辣眼睛"现场,为了反(zheng)驳(shi)这个观点,小编特意走访了网易云的架构师.工程师.产品经理.程 ...

  8. zookeeper日志级别

    查看源代码得知zookeeper(版本3.4.13)内部的日志用的slf4j,项目启动zk连接了之后一直在打debug日志(如下所示),甚是讨厌,logback日志级别调成info没用. 17:24: ...

  9. 从零开始的程序逆向之路 第一章——认识OD(Ollydbg)以及常用汇编扫盲

    作者:Crazyman_Army 原文来自:https://bbs.ichunqiu.com/thread-43041-1-1.html 0×00 序言: 1.自从上次笔者调戏完盗取文件密码大黑客后, ...

  10. Linux开机执行指定shell

    目的:希望linux在开机或重启后第一时间启动部分服务或者执行特定脚本 1.需要在/etc/init.d/目录下建立对应的shell脚本,示例如下: 开机启动docker-compose中的所有容器 ...