从类加载到双亲委派:深入解析类加载机制与 ClassLoader

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 从类加载到双亲委派:深入解析类加载机制与 ClassLoader

前言

在 Java 编程中,类加载是一个关键的技术点,它负责将类引入 Java 虚拟机(JVM)使得程序能够正确地加载、链接、初始化类;类加载的过程是 Java 程序执行的基础,它涉及从磁盘或网络上加载类的字节码,解析类的符号引用,最终将类加载到内存中供程序使用

类加载的过程不仅仅是将类加载到内存中,它还涉及类的链接及初始化阶段;链接阶段包括验证、准备和解析,它们确保类的字节码符合安全和结构要求,并为类的变量分配内存并进行默认初始化;初始化阶段执行类的静态初始化程序,对静态变量进行赋值及执行其他必要的设置

理解类加载技术对于 Java 开发者来说至关重要,它不仅仅是一个概念,更是实现 Java 程序的关键步骤,深入了解类加载的原理及机制,可以帮助我们更好地理解 Java 程序的运行过程,并且能够应对一些高级的类加载场景和问题

Java 是一种想解释就解释,想编译就编译的语言

默认情况下采用混合模式(解释器、热点代码编译)执行,起始阶段采用解释执行,后面进行热点代码检测,发现在整个执行过程中有某段方法或某循环执行的频率特别高,就会把这段代码编译成本地代码,将来执行这段代码的时候就执行本地代码就好了,效率提升,这个就叫混合模式

本文将详细介绍类加载技术,探讨类加载的原理和过程,以及类加载过程中的关键概念和技术点。我们将深入了解类加载的各个阶段,解析类的符号引用和解决依赖关系,还将讨论类加载器的作用和分类

Class 文件介绍

如何生成 class 文件

当 Java 开发人员写好代码以后,在编译时将 Java 源代码由 Java 编译器(javac)将其转换为字节码文件(.class)这个编译过程将源代码转换为中间表示形式,而不是直接生成可执行的机器代码;整个 .class 文件生成后的格式就是个二进制的字节流,只要你通过文本文件打开 .class 文件,一般都会是如下所示:

它是由 Java 虚拟机来解释执行的,看到 CA FE BA BE 这就是 Java 编译完的 class 文件,四个字节,这部分叫做魔术(magic number)

观察 Bytecode 方法

Java 自带的 javac 命令帮助我们把 java 源文件生成 class 字节码文件,相反可以通过 javap 命令查看字节码有哪些内容

javap -v xxx.class

推荐一个更好的可视化工具,查看字节码的 IDEA 插件:jclasslib

可以很清晰的看到,常量池、接口、字段、方法、属性里面的字节码信息,比如:在对比循环体外实例化对象和循环体内实例化对象差异化时,可以通过该工具来查看两者的区别

class 文件到底是什么样的呢?

当一个 class 文件躺在硬盘上,该内容被 load 进内存以后,会创建两块内容

  1. 比如:String,将 String,class 二进制内容扔到了内存中
  2. 该内容于是乎生成了一个 class 对象(指令)该指令指向了生成好的二进制内容

class 存放在内存分区中,内存分区就是存常量、存 class 各种各样的信息,实际上它这块内容逻辑上叫 Method Area 方法区

JDK 8 之前该方法区实现落地在 PermGen 永久代中

JDK 8 之后该方法区实现落地在 Metaspace 元空间中

永久代、元空间两者之间的区别?

  1. 永久代:逻辑上属于堆,物理上不属于堆,存在于虚拟机中
  2. 元空间:逻辑上、物理上都属于堆,但其不存在于虚拟机中,它使用的是物理内存

Class 加载、链接、初始化

Loading 加载过程:将 class 文件加载进内存中,也就是对 class 文件中的一个个二进制字节码读取后进行装载

Linking 链接过程分为以下三部分:

  1. Verification:校验装载进来的 class 文件是不是符合 class 文件的标准、规范,假设 > 读取进来的 class 文件开头并非以 CA FE BA BE,那么在这个步骤就会被拒绝了
  2. Preparation:将 class 文件中静态成员变量赋予默认值,并非赋予初始值,假设 > static int i = 1,在这个步骤并不会把变量 i 赋值为 1,而是先赋值为 0
  3. Resolution:将类、方法、属性等符号引用解析为直接引用,要给它转换为直接内存地址,直接可以访问到的

Initializing 初始化过程:调用类初始化代码,静态变量在此时会赋予初始化

当该 class 对应所有对象都没有引用指向以后,当发生 GC 时,这些对象资源都会被回收掉

加载、类加载器

类加载器:class 文件从虚拟机加载到内存里都是通过 ClassLoader 类加载器加载进内存的,ClassLoader 是顶级父类,它是一个 abstract 类,它一定会有对应的子类实现,若想查看某个 class 是被哪个加载器加载进内存的,可以调用 类名.class.getClassLoader() 方法进行查看,示例代码如下:

/**
 * @author vnjohn
 * @since 2023/6/24
 */
public class ClassLoaderLevel {
    public static void main(String[] args) {
        System.out.println("String:"+ String.class.getClassLoader());
        System.out.println("HKSCS:"+ HKSCS.class.getClassLoader());
        System.out.println("DNSNameService:"+ DNSNameService.class.getClassLoader());
        System.out.println("ClassLoaderLevel:"+ ClassLoaderLevel.class.getClassLoader());
        System.out.println("DNSNameService#parent#classLoader:"+ DNSNameService.class.getClassLoader().getClass().getClassLoader());
        System.out.println("ClassLoaderLevel#parent#classLoader:"+ ClassLoaderLevel.class.getClassLoader().getClass().getClassLoader());
    }
}

执行结果及描述如下:

// 输出结果为 null,因为其是顶级加载器 Bootstrap 所加载的
String:null
// 输出结果为 null,因为其也是顶级加载器 Bootstrap 所加载的
HKSCS:null
// 该类位于 ext 目录某个 jar 包下,所以它由扩展类加载器 ExtClassLoader 所加载
DNSNameService:sun.misc.Launcher$ExtClassLoader@2ef1e4fa
// 该类是我们自己写的类,所以它由应用类加载器 AppClassLoader 所加载
ClassLoaderLevel:sun.misc.Launcher$AppClassLoader@18b4aac2
// 输出结果为 null,父加载器并不是类的加载器的加载器
DNSNameService#parent#classLoader:null
ClassLoaderLevel#parent#classLoader:null

通过示例代码演示过后,下面来介绍不同层次的类加载器,它们各自所负责的事情是什么

  1. Bootstrap:启动类加载器,又称之为引导类加载器,它用于加载 lib 里 JDK 最核心的内容,比如 > rt.jar、charset.jar 等包下的核心类,当在什么时候调用 getClassLoader 方法拿到的加载器结果是 null 值时,那么就代表是从最顶层的类加载器中进行加载的
  2. Extension:扩展类加载器,加载扩展包各种各样的文件,扩展包一般放在 jdk 安装目录下的 jre/lib/ext/*.jar 包
  3. App:应用加载器,平时经常会用到的类加载器,用它来指定加载 class path 指定的内容
  4. Custom:自定义类加载器

Custom ClassLoader 父类加载器 > application 父类加载器 > Extension 父类加载器 > Bootstrap

它们只是语义上的继承

双亲委派

如上图,描述类加载其实在内部是遵循双亲委派机制去进行加载的,那么下面来仔细描述当一个 class 文件要被 load 进内存,是怎样的一个加载过程?

  1. 任何一个 class,当你有自定义 ClassLoader 类加载器时,这时候就先尝试去自定义类加载器里面找,它内部维护着缓存,说你有没有已经帮我加载进来了,如果已经加载进来一遍就不需要加载第二遍,它如果没有在自己的自定义缓存找到的话,它并不是直接加载这块内存,它会先去它的父加载器 App 应用加载器中,问父亲你有没有把我这个类加载进来呢
  2. App 应用加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就继续委托给它的父加载器 Extension 扩展类加载器
  3. Extension 扩展类加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就继续委托给它的父加载器 Bootstrap 启动类加载器
  4. Bootstrap 启动类加载器,这时候就会去它的缓存里面找有没有这个类,如果有就返回,没有就往回委托给它的子加载器 Bootstrap Extension 扩展类加载器
  5. Extension 扩展类加载器,说我只负责加载扩展 jar 包里面的类,其他的我一概不知道也找不到,然后一直向下往回委托到 App 应用加载器、Custom ClassLoader 自定义类加载器中去找

整个加载过程,经过了一圈又一圈,才会真正把类加载进来,当我们能够把该类加载进来时叫做成功,若加载不进来,抛出异常 ClassNotFound 类找不到,这整个过程就叫做双亲委派

为什么要搞双亲委派机制?

主要是为了安全,若任何一个 Class 文件都可以把它加载进内存的话,那我就可以将 java.lang.String 交由给 ClassLoader,将密码存储成 String 类型对象,把 String load 进内存后,打包给客户,然后就可以偷偷摸摸把密码随便传递,这就造成了密码隐私泄露,极其不安全

当出现了双亲委派机制后,就不会这样了,自定义类加载器加载一次,java.lang.String 就产生了警惕性,它会先去上面查有没有加载过,若上面有加载过就不会返回给你,不给你进行重新加载

Launcher 核心类

Launcher 类是 ClassLoader 中的一个包装启动类,Bootstrap、Extension、App 类加载器它们所加载的路径都来自于 Launcher 核心类的源码

Bootstrap ClassLoader 加载路径:System.getProperty(“sun.boot.class.path”)

Extension ClassLoader 加载路径:System.getProperty(“java.ext.dirs”)

App ClassLoader 加载路径:System.getProperty(“java.class.path”)

如上面,示例代码执行的结果来看,可以得知

// 该类位于 ext 目录某个 jar 包下,所以它由扩展类加载器 ExtClassLoader 所加载
DNSNameService:sun.misc.Launcher$ExtClassLoader@2ef1e4fa
// 该类是我们自己写的类,所以它由应用类加载器 AppClassLoader 所加载
ClassLoaderLevel:sun.misc.Launcher$AppClassLoader@18b4aac2

Launcher 类来自于 sun,misc 包,ExtClassLoader、AppClassLoader 来自于 Launcher 源码,它默认显示为类名字后面 + 哈希 code 码,$ 符号 > 代表的意思就是 ExtClassLoader、AppClassLoader 都属于 Launcher 类的内部类!

下面通过一段小程序来看看这三个类加载器里到底加载了哪些文件,先通过指定的路径拿到属性值,然后再将指定符号替换为换行符

Windows 通过 ; 符号替换,Mac 通过 : 符号替换

/**
 * @author vnjohn
 * @since 2023/6/24
 */
public class ClassLoaderScope {
    public static void main(String[] args) {
        String bootstrapProperty = System.getProperty("sun.boot.class.path");
        System.out.println("Bootstrap ClassLoader:");
        System.out.println(bootstrapProperty.replaceAll(":", System.lineSeparator()));
    System.out.println();
        String extProperty = System.getProperty("java.ext.dirs");
        System.out.println("Ext ClassLoader:");
        System.out.println(extProperty.replaceAll(":", System.lineSeparator()));
    System.out.println();
        String appProperty = System.getProperty("java.class.path");
        System.out.println("App ClassLoader:");
        System.out.println(appProperty.replaceAll(":", System.lineSeparator()));
    }
}

执行结果如下:

Bootstrap ClassLoader:
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/classes
Ext ClassLoader:
/Users/vnjohn/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
App ClassLoader:
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/deploy.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/dnsns.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/jaccess.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/localedata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_333.jdk/Contents/Home/jre/lib/ext/nashorn.jar
// ..... 省略其他

ClassLoader 相关源码

实现自定义类加载器之前,先阅读一下 ClassLoader 相关的源码部分

// 当前类加载器的父加载器
private final ClassLoader parent;
/**
 * name:当前要加载的全限定类名
 * resolve:是否将符号引用转换为可以直接访问的地址
 */
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  // getClassLoadingLock(name):为加载类时都获取一把锁
    synchronized (getClassLoadingLock(name)) {
        // 首先,先检查该类是否被加载过了,若加载过了直接返回
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
              // 若父加载器不为空
                if (parent != null) {
                  // 父类先进行加载
                    c = parent.loadClass(name, false);
                } else {
                  // 父加载器为空时,说明当前加载器是启动类加载器:Bootstrap
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 若类找不到就抛出 ClassNotFoundException 异常
            }
            if (c == null) {
                // 仍然未找到,调用 findClass 方法查找该类
                long t1 = System.nanoTime();
                c = findClass(name);
                // 定义类加载器,记录统计的数据
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        // 转换为可以执行访问的类
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

loadClass 方法执行过程说明了,当你要加载一个类时只需要调用 ClassLoader#loadClass 方法就能够把该类加载进内存,加载到内存以后它会返回一个 Class 类的对象

经过上面的源码分析,若在自己缓存中没有找到,父加载器中也没有加载成功,最后只能回来自己再去调用 ClassLoader#findClass 方法去加载,它由 protected 修饰受保护的,只能在子类里面去进行访问,如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

该方法实现只有一句话,只能在它的子类里面去进行访问,很简单,我们只需要实现这个方法,就可以自定义类加载器加载我们所需要的类,这个就是钩子函数 > 模版方法设计模式

ClassLoader 相关问题

1、什么时候需要自定义加载器去实现?

Tomcat 在加载自定义的那部分类时(WEB-INF/classes 目录、WEB-INF/lib 目录中的 JAR文件),肯定是需要自定义类加载器(WebAppClassLoader)去加载这些 class 文件的,热部署的实现也是基于此加载器去实现的

2、如何指定类加载器的 parent

通过 super(parent) 指定

如下是示例代码:

/**
 * @author vnjohn
 * @since 2023/6/24
 */
public class ClassLoaderParent extends ClassLoader{
    private static ClassLoaderParent PARENT = new ClassLoaderParent();
    private static class MyClassLoader extends ClassLoader {
        public MyClassLoader() {
            super(PARENT);
        }
    }
}

3、如何打破双亲委派机制?

1、JDK 1.2 之前,自定义类加载器都必须重写 ClassLoader#loadClass 方法

2、ThreadContextClassLoader 提供了一种机制来绕过双亲委派机制,以实现在特定的线程上下文中加载类,通过 Thread#setContextClassLoader(ClassLoader cl) 方法可以设置线程上下文类加载器

3、在 OSGi 模块化开发框架中,各自应用程序存在自己的模块化机制和类加载器,可以加载同一个类库的不同版本,并且可以加载同名的类。在这种情况下,打破双亲委派机制的主要方式是使用双亲委派模型的变种,即双亲委派模型的扩展

自定义简单 ClassLoader

1、定义一个测试类:HelloWorld 后,通过 javac 编译成 class 文件

/**
 * @author vnjohn
 * @since 2023/6/24
 */
public class HelloWorld {
    public void sayHello() {
        System.out.println("Hello Vnjohn");
    }
}

2、自定义类加载器:MyLoader,实现 URLClassLoader 类,重写 findClass 方法,在 findClass 方法块中会用到辅助方法


目录
相关文章
|
3月前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
124 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
6月前
|
存储 前端开发 Java
深入解析Java类加载机制:原理、过程与实践
深入解析Java类加载机制:原理、过程与实践
220 2
|
7月前
|
Java
【类加载机制深度解析】,附带学习经验
【类加载机制深度解析】,附带学习经验
|
缓存 Oracle 前端开发
解析Java类加载的运行机制和双亲委派模型
解析Java类加载的运行机制和双亲委派模型
|
前端开发 安全 Java
JVM-白话聊一聊JVM类加载和双亲委派机制源码解析
JVM-白话聊一聊JVM类加载和双亲委派机制源码解析
78 0
|
缓存 安全 前端开发
【类加载机制深度解析】
【类加载机制深度解析】
136 0
【类加载机制深度解析】
|
缓存 Java 编译器
Jvm 类加载机制解析,一起来了解神秘的类加载机制吧
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过 加载,连接,初始化,这三个步骤对类进行初始化,如果没有意外,JVM 将会连续完成这三个步骤,所以有时也称为类初始化。
123 0
Jvm 类加载机制解析,一起来了解神秘的类加载机制吧
|
存储 安全 前端开发
【JVM深度解析】类加载与类加载器
你了解类加载机制吗?类加载器能说一下是什么吗?如何破坏双亲委派呢,多说几种?...不懂?一文带你了解类加载与类加载器
【JVM深度解析】类加载与类加载器
|
安全 前端开发 Java
JVM 类加载过程解析
JVM 类加载过程解析
JVM 类加载过程解析
|
Java
【Java 虚拟机原理】Java 类加载过程 ( 加载 | 连接 - 验证 准备 解析 | 初始化 | 使用 | 卸载 )
【Java 虚拟机原理】Java 类加载过程 ( 加载 | 连接 - 验证 准备 解析 | 初始化 | 使用 | 卸载 )
141 0
【Java 虚拟机原理】Java 类加载过程 ( 加载 | 连接 - 验证 准备 解析 | 初始化 | 使用 | 卸载 )

推荐镜像

更多