死磕P7: JVM类加载那些事儿,一起探知类的前世今生(二)

news2024/9/28 20:54:19

这是「死磕P7」系列第 006 篇文章,欢迎大家来跟我一起 死磕 100 天,争取在 2025 年来临之际,给自己一个交代。

接上篇,上一篇介绍了 JVM 类加载过程及类的生命周期,回顾一下:

死磕P7: JVM类加载那些事儿,一起探知类的前世今生(一)-CSDN博客

类加载过程用到一个叫做类加载器的东西,所谓类加载器(Class Loader),其实就是一段代码。

这段代码的主要功能就是:通过一个类的全限定名来获取类的二进制字节流。

类与类加载器

对于任意一个类,都必须由其「类加载器」和「该类本身」共同确定它在 JVM 中的唯一性。

若要比较两个类是否相等,前提是这两个类必须是由同一个类加载器加载。

这里的「相等」,包括 equals、isAssignableFrom、isInstance 等方法,还有 instanceof 关键字。

即类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载入JVM中,同一个类就不会被再次载入了。

类加载器

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

  • 启动类加载器(Bootstrap ClassLoader)

  • 扩展类加载器(Extension ClassLoader)

  • 应用程序类加载器(Application ClassLoader)

当然,用户也可以自定义类加载器~有时候会有用,关注公&号:新质程序猿,后面会介绍到。

启动类加载器(Bootstrap ClassLoader)

负责加载 JAVA_HOME\lib 目录,或者 -Xbootclasspath 参数指定路径下,且被 JVM 识别的类库。

用来加载java的核心库,此类加载器并不继承于java.lang.ClassLoader,不能被java程序直接调用,代码是使用C++编写的,是虚拟机自身的一部分。

扩展类加载器(Extension ClassLoader)

sun.misc.Launcher$ExtClassLoader 类实现,负责加载 JAVA_HOME\lib\ext 目录,或者 java.ext.dirs 系统变量指定的路径中的类库。

应用程序类加载器(Application ClassLoader)

sun.misc.Launcher$AppClassLoader 类实现,加载用户类路径(ClassPath)下所有的类库,默认的系统类加载器(若没有自定义过类加载器,一般使用该类进行加载)。

说到类加载器,就不得不提「双亲委派模型」

双亲委派模型

双亲委派模型的工作流程大致如下:

若一个类加载器准备去加载类时,它首先不会自己尝试去加载这个类,而是将其委派给父类加载器,父加载器亦是如此,直至启动类加载器;仅当父加载器无法加载该类的时候,子加载器才会尝试自己进行加载。

有点啃老的意味哈,哈哈哈!(后面有实际示例)

双亲委派模型的实现代码在 java.lang.ClassLoader 类的 loadClass 方法中,如下:

类加载器加载Class经过如下步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。

  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。

  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。

  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。

  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。

  6. 从文件中载入Class,成功后跳至第8步。

  7. 抛出ClassNotFountException异常。

  8. 返回对应的java.lang.Class对象。

为什么要采用双亲委派模型?这样做有什么好处呢?

Java 类随着类加载器有了层级关系,把最基础的类,例如 java.lang.Object,交给最顶端的类加载器加载,保证在各个加载器环境中都是同一个 Object 类。

破坏双亲委派模型

双亲委派模型可以理解为一个规范,然而某些地方由于某些原因并未遵循这个规范。对于那些没有遵循该规范的地方,就是破坏了双亲委派模型。

破坏双亲委派模型的行为大致有三次(看不懂没关系,我也没太懂,看最后的示例即可

第一次

由于“双亲委派模型”是 JDK 1.2 引入的,但类加载和 java.lang.ClassLoader 类在此之前就已经存在了,为了兼容已有代码,双亲委派模型做了妥协。

由于 ClassLoader 类的 loadClass 方法可以直接被子类重写,这样的类加载机制就不符合双亲委派模型了。

如何实现兼容呢?在 ClassLoader 类添加了 findClass 方法,并引导用户重写该方法,而非 loadClass 方法。

第二次

双亲委派模型的类加载都是自底向上的(越基础的类由越上层的加载器来加载),但有些场景可能会出现基础类型要反回来调用用户代码,这个场景如何解决呢?

一个典型的例子就是 JNDI (启动类加载器加载)服务,其目的是调用其它厂商实现并部署在应用程序 ClassPath 下的服务提供者接口(Service Provider Interface,SPI)。启动类加载器是不认识这些 SPI 的,如何解决呢?

Java 团队引入了一个线程上下文类加载器(Thread Context ClassLoader),可以设置类加载器,在启动类加载器不认识的地方,调用其它类加载器去加载。这其实也打破了双亲委派模型。

比如 JDBC 的类加载机制。

第三次

第三次破坏是对程序动态性的追求导致的,代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。典型的如 IBM 的 OSGi 模块化热部署。

自定义类加载器

自定义 MyClassLoader

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class MyClassLoader extends ClassLoader {

    // 重写 findClass 方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        return defineClass(name, classData, 0, classData.length);
    }

    // 读取 class 文件
    private byte[] loadClassData(String className) {
        String fileName = "/Users/hyx/Documents/workspace/github/hello/out/production/hello" +
                File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try {
            FileInputStream inputStream = new FileInputStream(fileName);
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            return outputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

创建一个 Person 类

public class Person {
    static {
        // 当 Person 类初始化时,会打印该代码
        System.out.println("Person init!");
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

写一个 Hello 主类及 test1 方法

public class Hello {

    private static void test1() throws Exception {
        // 创建类加载器实例
        MyClassLoader myClassLoader1 = new MyClassLoader();
        // 加载 Person 类(注意这里是 loadClass 方法)
        Class<?> aClass1 = myClassLoader1.loadClass("Person");
        aClass1.newInstance(); // Person init!

        MyClassLoader myClassLoader2 = new MyClassLoader();
        Class<?> aClass2 = myClassLoader2.loadClass("Person");
        aClass2.newInstance();

        System.out.println("--->" + aClass1.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("--->" + aClass2.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("--->" + aClass1.equals(aClass2)); // true
    }

    public static void main(String[] args) throws Exception {
        System.out.println("Hello world!");
        test1();
    }
}

执行 main 方法

可以看到,这里虽然使用了两个类加载器实例加载 Person 类,但实际上 aClass1 和 aClass2 的类加载器并不是自定义的 MyClassLoader,而是 Launcher$AppClassLoader,即应用类加载器。为什么会是这个结果呢?

其实这就是前面分析的双亲委派模型,示意图如下:

大体流程分析:

  1. 使用 MyClassLoader 加载 Person 类时,它会先委托给 AppClassLoader;

  2. AppClassLoader 委托给 ExtClassLoader;

  3. ExtClassLoader 委托给启动类加载器;

  4. 但是,启动类加载器并不认识 Person 类,无法加载,于是就再反回来交给 ExtClassLoader;

  5. ExtClassLoader 也无法加载,于是交给了 AppClassLoader;

  6. AppClassLoader 可以加载 Person 类,加载结束。

非双亲委派模型类加载

还是上面的自定义类加载器,如何破坏双亲委派模型呢?把上面的 loadClass 方法换成 findClass 就行,示例代码:

Hello 主程序, 添加了一个 test2 方法

public class Hello {

    private static void test1() throws Exception {
        // 创建类加载器实例
        MyClassLoader myClassLoader1 = new MyClassLoader();
        // 加载 Person 类(注意这里是 loadClass 方法)
        Class<?> aClass1 = myClassLoader1.loadClass("Person");
        aClass1.newInstance(); // Person init!

        MyClassLoader myClassLoader2 = new MyClassLoader();
        Class<?> aClass2 = myClassLoader2.loadClass("Person");
        aClass2.newInstance();

        System.out.println("--->" + aClass1.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("--->" + aClass2.getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println("--->" + aClass1.equals(aClass2)); // true
    }

    private static void test2() throws Exception {
        MyClassLoader cl1 = new MyClassLoader();
        // 加载自定义的 Person 类
        Class<?> aClass1 = cl1.findClass("Person");
        // 实例化 Person 对象
        aClass1.newInstance(); // Person init!

        MyClassLoader cl2 = new MyClassLoader();
        Class<?> aClass2 = cl2.findClass("Person");
        aClass2.newInstance(); // Person init!

        System.out.println("--->" + aClass1); // class loader.Person
        System.out.println("--->" + aClass2); // class loader.Person

        System.out.println("--->" + aClass1.getClassLoader()); // loader.MyClassLoader@60e53b93
        System.out.println("--->" + aClass2.getClassLoader()); // loader.MyClassLoader@1d44bcfa

        System.out.println("--->" + aClass1.equals(aClass2)); // false
    }

    public static void main(String[] args) throws Exception {
        System.out.println("Hello world!");
        test1();
        test2();
    }
}

执行

这里创建了两个自定类加载器 MyClassLoader 的实例,分别用它们来加载 Person 类。

虽然两个打印结果都是 class loader.Person ,但类加载器不同,导致 equals 方法的结果是 false,原因就是二者使用了不同的类加载器。

根据 MyClassLoader 的代码,这里实际并未按照双亲委派模型的层级结构去加载 Person 类,而是直接使用了 MyClassLoader 来加载的。

总结

本文还是比较干的,类加载器其实就是用来加载 class 文件到内存,然后创建 Class 类实例的过程,Java 提供了 3 种自带的 类加载器,分别是 启动类加载器,扩展类加载器,应用程序类加载器,另外,用户可以自己自定义类加载器。

面试中常考的点之一就是「双亲委派模型」,其实也比较好理解,为了确保类的全局唯一性,只要有一个类加载器加载过了就不用再加载了。

破坏双亲委派的最典型也是最常见的就是自定义类加载器然后通过实现 findClass 方法了,你也可以去尝试一下,体验一下。


END

好了,今天的分享就到这里,关注公&号:新质程序猿,和我一起死磕 P7, 一起学习成长。

感谢大家的阅读,如果有任何异议的地方,欢迎指正,也欢迎大家公号找到我与我做朋友!

小福利

文末小福利,作为资深囤货达人,购置或转存了上千 T 的各种资源,反正我也学不完,如有需要,可以公号: 新质程序猿 找到我,直接送您,能帮助到大家也算是有所福报吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2174727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

周文强聚焦助学育人,爱心图书室项目圆满完成

日前&#xff0c;一场充满爱心与希望的公益活动在四川甘孜州乡城县尼斯寄宿制小学拉开帷幕。这次名为“520爱心图书室”的公益活动&#xff0c;旨在通过捐赠图书的方式&#xff0c;支持基层青少年的阅读成长。作为此次活动的积极参与者&#xff0c;周文强不仅向学校捐赠了价值1…

python 高效读取多个geojson 写入一个sq3(Sqlite) 、效率提高90%+

1.问题缘由&#xff1a; 由于工作需求&#xff0c;需要将多个&#xff08;总量10G&#xff09;geojson文件写入到sq3库&#xff0c;众所周知&#xff0c;sqlite 不支持多线程写入&#xff0c;那该怎么办呢&#xff0c;在网上也查了很多策略&#xff0c;都没有达到立竿见影的效果…

甄选范文“论分布式存储系统架构设计”,软考高级论文,系统架构设计师论文

论文真题 分布式存储系统(Distributed Storage System)通常将数据分散存储在多台独立的设备上。传统的网络存储系统采用集中的存储服务器存放所有数据,存储服务器成为系统性能的瓶颈,也是可靠性和安全性的焦点,不能满足大规模存储应用的需要。分布式存储系统采用可扩展的…

车辆重识别(去噪扩散概率模型)论文阅读2024/9/27

[2] Denoising Diffusion Probabilistic Models 作者&#xff1a;Jonathan Ho Ajay Jain Pieter Abbeel 单位&#xff1a;加州大学伯克利分校 摘要&#xff1a; 我们提出了高质量的图像合成结果使用扩散概率模型&#xff0c;一类潜变量模型从非平衡热力学的考虑启发。我们的最…

linux驱动设备程序(内核层、应用层)

一、linux驱动程序 1、分类 字符设备&#xff08;驱动&#xff09;、块设备&#xff08;驱动&#xff09;、网络设备&#xff08;驱动&#xff09;。 2、核心 应用程序运行在用户空间&#xff08;3G&#xff09;&#xff1b;<系统调用>——><陷入>——>&…

正则表达式在过滤交换机lldp信息的应用举例

#include <iostream> #include <string> #include <regex> #include <vector> #include <unordered_map> #include <sstream> #include <unistd.h> // For usleep// 假设存在的 LOG_INFO 和 LOG_WARNING 函数 #define LOG_INFO(...)…

17.第二阶段x86游戏实战2-线程发包和明文包

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要…

基于docker-compose部署openvas

目录 0.部署openvas 1.编辑docker-compose文件 2.运行compose 3.访问openvas 4.openvas扫描 5.创建任务 6.点击Task Wizard ​编辑 7.输入通讯的IP地址 8.下载报告 9.下载完成 0.部署openvas 1.编辑docker-compose文件 vim docker-compose.yaml version: 3service…

《论文阅读》 用于产生移情反应的迭代联想记忆模型 ACL2024

《论文阅读》 用于产生移情反应的迭代联想记忆模型 ACL2024 前言简介任务定义模型架构Encoding Dialogue InformationCapturing Associated InformationPredicting Emotion and Generating Response损失函数问题前言 亲身阅读感受分享,细节画图解释,再也不用担心看不懂论文啦…

通信工程学习:什么是MAI多址干扰

MAI:多址干扰 MAI多址干扰(Multiple Access Interference)是无线通信领域,特别是在码分多址(CDMA)系统中,一个关键的干扰现象。以下是对MAI多址干扰的详细解释: 一、定义 多址干扰是指在CDMA系统中,由于多个用户的信号在时域和频域上是混叠的,从而导…

区块链可投会议CCF C--FC 2025 截止10.8 附录用率

Conference&#xff1a;Financial Cryptography and Data Security (FC) CCF level&#xff1a;CCF C Categories&#xff1a;network and information security Year&#xff1a;2025 Conference time&#xff1a;14–18 April 2025, Miyakojima, Japan 录用率&#xff1…

阿里云oss配置

阿里云oss配置 我们可以使用阿里云的对象存储服务来存储图片&#xff0c;首先我们要注册阿里云的账号登录后可以免费试用OSS服务。 之后我们打开控制台&#xff0c;选择对象存储服务&#xff0c;就看到我们下面的画面&#xff1a; 我们点击创建Bucket,之后就会出现如下图界面…

退出系统接口代码开发

退出系统不需要传入参数 请求过滤404的错误--请求次数监听这些都不需要更改 从controller层开始开发代码&#xff0c;因为每个接口都需要增加接口防刷拦截&#xff0c;不然会恶意攻击&#xff0c;所以在这里增加退出系统接口防刷拦截&#xff1b;并退出系统接口没有header和t…

图像分割(九)—— Mask Transfiner for High-Quality Instance Segmentation

Mask Transfiner for High-Quality Instance Segmentation Abstract1. Intrudouction3. Mask Transfiner3.1. Incoherent Regions3.2. Quadtree for Mask RefinementDetection of Incoherent Regions四叉树的定义与构建四叉树的细化四叉树的传播 3.3. Mask Transfiner Architec…

修改Kali Linux的镜像网站

由于官方的镜像可能会出现连接不上的问题导致无法安装我们所需要的包&#xff0c;所以需要切换镜像站为国内的&#xff0c;以下是一些国内常用的Kali Linux镜像网站&#xff0c;它们提供了与Kali Linux官方网站相同的软件包和资源&#xff0c;但访问速度更快&#xff1a; 清华…

Feign:服务挂了也不会走fallback

Feign 本质上是一个 HTTP 客户端&#xff0c;用于简化微服务之间的 HTTP 通信。它允许开发者通过定义接口和注解来声明式地编写 HTTP 客户端&#xff0c;而无需手动编写 HTTP 请求和响应处理的代码。 今天在模拟微服务A feign调用微服务B的时候&#xff0c;把微服务B关了&#…

通过WinCC在ARMxy边缘计算网关上实现智能运维

随着信息技术与工业生产的深度融合&#xff0c;智能化运维成为提升企业竞争力的关键因素之一。ARMxy系列的ARM嵌入式计算机BL340系列凭借其高性能、高灵活性和广泛的适用性&#xff0c;为实现工业现场的智能运维提供了坚实的硬件基础。 1. 概述 ARMxy BL340系列是专为工业应用…

python爬虫案例——抓取链家租房信息(8)

文章目录 1、任务目标2、分析网页3、编写代码1、任务目标 目标站点:链家租房版块(https://bj.lianjia.com/zufang/) 要求:抓取该链接下前5页所有的租房信息,包括:标题、详情信息、详情链接、价格 如: 2、分析网页 用浏览器打开链接,按F12或右键检查,进入开发者模式;因…

【病理图像】如何获取全切片病理图像的信息python版本

1. QuPath 拿到一张全切片病理图像时,我们可以用QuPath查看,如下图: 随着鼠标滚轮的滑动,我们可以看到更加具体的细胞状态,如下图: 当然,我们也可以在Image看到当前全切片图像的一些信息,如下图: 如果是10张以内的图像还好,我们可以一张一张打开查看,但是我们在…

基于VUE的在线手办交易平台购物网站前后端分离系统设计与实现

目录 1. 需求分析 2. 技术选型 3. 系统架构设计 4. 前端开发 5. 后端开发 6. 数据库设计 7. 测试 8. 部署上线 9. 运维监控 随着二次元文化的兴起&#xff0c;手办作为一种重要的周边产品&#xff0c;受到了广大动漫爱好者的喜爱。手办市场的需求日益增长&#xff0c;…