摘要:本文围绕 Java 字节码与类加载机制展开,详解字节码文件组成、类的生命周期介绍类加载器分类双亲委派机制及打破该机制的方式,还阐述了线程上下文类加载器与 SPI 机制在 JDBC 驱动加载中的应用,帮助深入理解 Java 类加载核心原理。

1. Java 字节码文件与类加载机制

1.1 Java 虚拟机的组成

Java 虚拟机主要分为以下几个组成部分:

  • 类加载子系统:核心组件是类加载器,负责将字节码文件中的内容加载到内存中

  • 运行时数据区JVM 管理的内存创建出来的对象、类的信息等内容都会放在这块区域中。

  • 执行引擎:包含即时编译器、解释器、垃圾回收器。执行引擎使用解释器将字节码指令解释成机器码,使用即时编译器优化性能,使用垃圾回收器回收不再使用的对象。

  • 本地接口:调用本地使用 C/C++ 编译好的方法,本地方法在 Java 中声明时,都会带上native关键字。

1.2 字节码文件的组成

1.2.1 以正确的姿势打开文件

字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读。通过 NotePad++ 使用十六进制插件查看 class 文件:

无法解读出文件里包含的内容,推荐使用 jclasslib 工具查看字节码文件。

1.2.2 字节码文件的组成

字节码文件总共可以分为以下几个部分:

  1. 基础信息:魔数、字节码文件对应的 Java 版本号、访问标识、父类和接口信息

  2. 常量池:保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用

  3. 字段:当前类或接口声明的字段信息

  4. 方法:当前类或接口声明的方法信息,核心内容为方法的字节码指令

  5. 属性:类的属性,比如源码的文件名、内部类的列表等

1.2.2.1 基本信息

基本信息包含了 jclasslib 中能看到的 “一般信息” 相关内容,具体如下:

Magic 魔数

每个 Java 字节码文件的前四个字节是固定的,用 16 进制表示为0xcafebabe文件无法通过扩展名确定类型(扩展名可随意修改)软件会通过文件头(前几个字节)校验类型,不支持则报错。

常见文件格式的校验方式如下:

文件类型字节数文件头
JPEG (jpg)3FFD8FF
PNG (png)489504E47(文件尾也有要求)
bmp2424D
XML (xml)53C3F786D6C
AVI (avi)441564920
Java 字节码文件 (.class)4CAFEBABE

Java 字节码文件的文件头称为 magic 魔数,Java 虚拟机会校验字节码文件前四个字节是否为0xcafebabe,若不是则无法正常使用,会抛出错误。

主副版本号

主副版本号指编译字节码文件时使用的 JDK 版本号:

  • 主版本号:标识大版本号,JDK1.0-1.1 使用 45.0-45.3,JDK1.2 为 46,之后每升级一个大版本加 1;1.2 之后大版本号计算方法为 "主版本号 – 44",例如主版本号 52 对应 JDK8。

  • 副版本号:主版本号相同时,用于区分不同版本,一般只需关注主版本号。

版本号的作用是判断当前字节码版本与运行时 JDK 是否兼容。若用较低版本 JDK 运行较高版本 JDK 编译的字节码文件,会显示错误:

类文件具有错误的版本 52.0,应为 50.0,请删除该文件或确保该文件位于正确的类路径子目录中。

解决兼容性问题的两种方案:

其他基础信息

其他基础信息包括访问标识、类和接口索引,具体说明如下:

名称作用
访问标识标识是类 / 接口 / 注解 / 枚举 / 模块;标识 public、final、abstract 等访问权限
类、父类、接口索引通过这些索引可找到类、父类、接口的详细信息
1.2.2.2 常量池

字节码文件中常量池的作用是避免相同内容重复定义,节省空间。例如,代码中编写两个相同的字符串 “我爱北京天安门”,字节码文件及后续内存使用时只需保存一份,将该字符串及字面量放入常量池即可实现空间节省。

常量池中的数据都有编号(从 1 开始),例如 “我爱北京天安门” 在常量池中的编号为 7,字段或字节码指令中通过编号 7 可快速找到该字符串。字节码指令中通过编号引用常量池的过程称为符号引用,示例如下:

  • 字节码指令:ldc #7(符号引用编号 7 对应的字符串)

  • 常量池:编号 7 对应数据 “我爱北京天安门”

为什么需要符号引用?

编译期(如 javac 编译 .java 为 .class)根本不知道:

  • 被引用的类 / 方法在运行时会被加载到内存的哪个位置(内存地址由 JVM 动态分配);
  • 同一资源在不同 JVM、不同操作系统中的内存地址可能完全不同。

符号引用通过 “延迟绑定” 解决这个问题:编译期只记录 “要引用什么”,等到运行期类加载的 “解析阶段”,JVM 再根据符号引用的信息,在内存中找到对应的资源,将其转换为 “直接引用”(即内存地址)。

1.2.2.3 字段

字段中存放当前类或接口声明的字段信息,包含字段的名字描述符(字段类型:int,long),访问标识(修饰符:public、static、final 等)

1.2.2.4 方法

字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的 Code 属性中。例如,分析以下代码的字节码指令:

要理解字节码指令执行过程,需先了解操作数栈局部变量表

  • 操作数栈存放临时数据的栈式结构,先进后出

  • 局部变量表存放方法的局部变量(含参数、方法内定义的变量)

1. iconst_0:将常量 0 放入操作数栈,此时栈中只有 0。

2. istore_1:从操作数栈弹出栈顶元素(0),放入局部变量表 1 号位置编译期确定为局部变量 i 的位置),完成 i 的赋值

3. iload_1:将局部变量表 1 号位置的数据(0)放入操作数栈,此时栈中为 0。

4. iconst_1:将常量 1 放入操作数栈,此时栈中有 0 和 1。

5. iadd:将操作数栈顶部两个数据(0 和 1)相加,结果 1 放入操作数栈,此时栈中只有 1。

6. istore_2:从操作数栈弹出 1,放入局部变量表 2 号位置(局部变量 j 的位置)。

7. return:方法结束并返回。

同理,可分析i++++i的字节码指令差异:

i++ 字节码指令:iinc 1 by 1将局部变量表 1 号位置值加 1,实现 i++ 操作。

++i 字节码指令:仅调整了iinciload_1的顺序。

面试题int i = 0; i = i++; 最终 i 的值是多少?

:答案是 0。通过字节码指令分析:i++ 先将 0 取出放入临时操作数栈,接着对 i 加 1(i 变为 1),最后将操作数栈中保存的临时值 0 放入 i,最终 i 为 0。

1.2.2.5 属性

属性主要指类的属性,如源码的文件名、内部类的列表等。例如,在 jclasslib 中查看 SimpleClass 的属性,会显示 SourceFile 属性:

1.2.3 玩转字节码常用工具

1.2.3.1 javap

javap 是 JDK 自带的反编译工具,可通过控制台查看字节码文件内容,适合在服务器上使用

  • 查看所有参数:直接输入javap

  • 查看具体字节码信息:输入javap -v 字节码文件名称

  • 若为 jar 包:需先使用jar –xvf jar包名称命令解压,再查看内部 class 文件。

1.2.3.2 jclasslib 插件

jclasslib 有 Idea 插件版本,开发时使用可在代码编译后实时查看字节码文件内容。

1. 打开 Idea 的插件页面,搜索 “jclasslib Bytecode Viewer” 并安装。

2. 选中要查看的源代码文件,选择 “视图(View)- Show Bytecode With Jclasslib”,右侧会展示对应字节码文件内容。

3. 文件修改后需重新编译,再点击刷新按钮查看最新字节码。

1.2.3.3 Arthas

Arthas 是一款线上监控诊断产品,可实时查看应用 load、内存、gc、线程状态信息,且能在不修改代码的情况下诊断业务问题,提升线上问题排查效率。

安装方法

1. 将下载好的 arthas-boot.jar 文件复制到任意工作目录。

2. 使用java -jar arthas-boot.jar启动程序。

3. 输入需要 Arthas 监控的进程 ID(启动后会列出当前运行的 Java 进程)。

​​​

常用命令

dump:将字节码文件保存到本地。

示例:将java.lang.String的字节码文件保存到/tmp/output目录:

jad:将类的字节码文件反编译成源代码,用于确认服务器上的字节码是否为最新。

示例:反编译demo.MathGame并显示源代码

1.3 类的生命周期

类的生命周期描述了一个类加载、使用、卸载的整个过程,整体分为:

  1. 加载(Loading)

  2. 连接(Linking):包含验证、准备、解析三个子阶段

  3. 初始化(Initialization)

  4. 使用(Using)

  5. 卸载(Unloading)

类加载本身是一个过程,这个过程又细分为多个阶段,包含加载,连接和初始化阶段

1.3.1 加载阶段

1. 加载阶段第一步:类加载器根据类的全限定名,通过不同渠道以二进制流的方式获取字节码信息,程序员可通过 Java 代码拓展渠道,常见渠道如下:

2. 类加载器加载完类后,Java 虚拟机会将字节码中的信息保存到方法区,生成一个InstanceKlass对象,该对象保存类的所有信息(含实现多态的虚方法表等)。

3. Java 虚拟机同时会在上生成与方法区中数据类似的java.lang.Class对象,作用是在 Java 代码中获取类的信息,以及存储静态字段的数据(JDK8 及之后)。

步骤 1:类的 “来源获取”

类的字节码可以从多种来源被加载,如图 1 所示:

  • 本地文件:最常见的情况,类的.class文件存储在本地磁盘(如项目的classes目录、jar包中),类加载器从本地文件系统读取这些字节码文件。
  • 网络传输:在分布式应用(如 Applet、远程服务调用)中,类的字节码可通过网络(如 HTTP、RPC)从远程服务器传输到本地 JVM。
  • 动态代理生成:运行时通过字节码生成库(如 JDK 动态代理、CGLIB)动态生成类的字节码,无需预先存在物理文件。

步骤 2:类加载器(ClassLoader)的 “加载动作”

类加载器(如图 1 右侧的ClassLoader)是加载阶段的核心执行者,它的工作是:

  • 根据类的 “全限定名”(如java.lang.String),找到对应的字节码数据。

       JVM 不仅要加载我们自己写的应用类,还必须加载像java.lang.String这样的核心类

  • 将字节码数据以二进制流的形式读取到 JVM 中

步骤 3:生成InstanceKlass对象(方法区存储类元数据)

如图 2 所示,JVM 在方法区生成一个InstanceKlass对象:

  • InstanceKlass是 JVM 内部用于表示类的核心数据结构,包含类的全部元数据
    • 基本信息:类的访问修饰符(public、final 等)、类名、父类、接口等。
    • 常量池:存储类中用到的常量(如字符串、符号引用等)。
    • 字段(Field):类中定义的成员变量信息。
    • 方法:类中定义的方法信息(包括方法名、参数、返回值、字节码指令等)。
    • 虚方法表:支持多态的关键结构,存储方法的动态调用入口。

步骤 4:生成java.lang.Class对象(堆中供开发者访问)

如图 3、图 4 所示:

  • JVM 在堆区生成一个java.lang.Class对象,这个对象是开发者(Java 代码)能直接访问的 “类的镜像”。
  • Class对象与方法区的InstanceKlass对象关联Class对象中保存了访问InstanceKlass的 “入口”,但屏蔽了底层复杂的元数据细节。

步骤 5:开发者与Class对象的交互(访问控制)

如图 5 所示:

  • 开发者无需直接操作方法区的InstanceKlass(包含 JVM 内部实现的敏感 / 复杂信息)。
  • 开发者只需通过堆中的Class对象,就能获取类的公开可访问信息(如通过Class.getMethods()获取方法、Class.getFields()获取字段等)。【反射】
  • 这种设计既让开发者能便捷地反射(Reflection)操作类,又由 JVM 控制了访问范围(避免开发者直接篡改方法区的核心元数据)。

1.3.2 连接阶段

连接阶段分为三个子阶段:

验证(Verification)

验证的主要目的是检测 Java 字节码文件是否遵守《Java 虚拟机规范》的约束,无需程序员参与,主要包含四部分(具体详见《Java 虚拟机规范》):

  1. 文件格式验证:如文件是否以0xCAFEBABE开头,主次版本号是否满足要求。  

  1. 元信息验证:例如类必须有父类(super 不能为空)。

  2. 语义验证:验证程序执行指令的语义,如方法内指令跳转至不正确的位置。

  3. 符号引用验证:例如是否访问了其他类中 private 的方法。

JDK8 源码中对版本号的验证逻辑如下:

编译文件主版本号不高于运行环境主版本号;若相等,副版本号不超过运行环境副版本号。

准备(Preparation)

准备阶段为静态变量(static)分配内存并设置初值

不同数据类型的初值如下:

解析(Resolution)

解析阶段主要是将常量池中的符号引用替换成指向内存的直接引用

  • 符号引用:字节码文件中使用编号访问常量池中的内容

  • 直接引用:使用内存地址访问具体数据,无需依赖编号。

1.3.3 初始化阶段

初始化阶段会执行字节码文件中clinit(class init,类的初始化)方法的字节码指令,包含静态代码块中的代码并为静态变量赋值。

1. iconst_1:将常量 1 放入操作数栈。

2. putstatic #2:弹出操作数栈中的 1,放入堆中静态变量value的位置#2指向常量池中的value,解析阶段已替换为变量地址),此时value=1

3. iconst_2:将常量 2 放入操作数栈。

4. putstatic #2:弹出 2,更新value为 2。

5. returnclinit方法执行结束,最终value=2

触发类初始化的场景

clinit 不执行的情况
  1. 无静态代码块且无静态变量赋值语句。

  2. 有静态变量的声明,但没有赋值语句(如public static int a;)。

  3. 静态变量的定义使用 final 关键字(这类变量在准备阶段直接初始化)。

面试题 1

分析步骤

步骤 1:类加载时执行静态代码块

当 JVM 首次加载Test1类时,会执行静态代码块(被static修饰的代码块)。静态代码块在类加载阶段执行,且只执行一次(无论创建多少个类的实例,静态代码块都只执行一次)。

所以,程序启动后,JVM 加载Test1类,首先执行static块中的代码:此时输出:D

步骤 2:执行main方法中的代码

main方法是程序入口,加载完类后,执行main方法内的代码:

  • 第一行:System.out.println("A"); → 输出:A
  • 第二行:new Test1(); → 创建Test1的实例,触发实例初始化
  • 第三行:new Test1(); → 再次创建Test1的实例,再次触发实例初始化

步骤 3:实例初始化的顺序(重点)

每次创建Test1实例时,实例初始化的顺序是:

  1. 执行实例初始化块(类中直接用{}包裹的代码块);
  2. 执行构造方法

所以,每次new Test1()时,执行顺序为:

  • 实例初始化块:System.out.println("C"); → 输出:C
  • 构造方法:System.out.println("B"); → 输出:B

两次new Test1()的输出

第一次new Test1()

  • 实例初始化块输出:C
  • 构造方法输出:B

第二次new Test1()

  • 实例初始化块再次输出:C
  • 构造方法再次输出:B

最终输出顺序
D(静态代码块,类加载时执行)→ Amain方法第一行)→ C(第一次实例的初始化块)→ B(第一次实例构造方法)→ C(第二次实例的初始化块)→ B(第二次实例构造方法)

面试题 2

分析步骤

  1. 调用new B02()创建对象,需初始化 B02,优先初始化父类 A02。

  2. 执行 A02 的初始化代码,a赋值为 1。

  3. 执行 B02 的初始化代码,a赋值为 2。

  4. 输出B02.a,结果为 2。

变化:若注释new B02();,仅访问B02.a(父类 A02 的静态变量),则只初始化父类 A02,a=1,输出结果为 1。

1.4 类加载器

1.4.1 什么是类加载器

类加载器(ClassLoader)是 Java 虚拟机提供给应用程序,用于实现获取类和接口字节码数据的技术。类加载器仅参与加载过程中 “字节码获取并加载到内存” 这一部分,具体流程如下:

  1. 类加载器通过二进制流获取字节码文件内容。

  2. 将获取的数据交给 Java 虚拟机。

  3. 虚拟机会在方法区生成InstanceKlass对象,在堆上生成java.lang.Class对象,保存字节码信息。

1.4.2 类加载器的分类

JDK8 及之前的默认类加载器

JDK8 及之前版本中,默认类加载器有三种,其关系如下:

  • 启动类加载器(Bootstrap):无父类加载器,加载 Java 最核心的类

  • 扩展类加载器(Extension):父类加载器为启动类加载器,允许扩展 Java 中通用的类

  • 应用程序类加载器(Application):父类加载器为扩展类加载器,加载应用使用的类

可通过 Arthas 的classloader命令查看类加载器信息

1.4.3 启动类加载器

  • 实现方式:由 Hotspot 虚拟机提供,使用 C++ 编写。

  • 默认加载路径:Java 安装目录/jre/lib下的类文件(如 rt.jar、tools.jar、resources.jar 等)。

  • 扩展示例:-Xbootclasspath/a:D:/jvm/jar/classloader-test.jar

说明:String类由启动类加载器加载,但 JDK8 中启动类加载器用 C++ 编写,Java 代码中无法直接获取,故返回 null。

1.4.4 扩展类加载器和应用程序类加载器

扩展类加载器

扩展类加载器加载用户 jar 包示例

  • 扩展示例:-Djava.ext.dirs="C:\Program Files\Java\jdk1.8.0\_181\jre\lib\ext;D:\jvm\jar"

应用程序类加载器

应用程序类加载器会加载classpath下的类文件,默认加载的是项目中的类以及通过maven引入的第三方jar包中的类。

  • 默认加载路径:classpath 下的类文件(项目中的类、maven 引入的第三方 jar 包中的类)。

  • 说明:项目类和第三方依赖类均由应用程序类加载器加载。

可通过 Arthas 的classloader -c 类加载器hash值 查看加载路径

1.5 双亲委派机制

双亲委派机制指:当一个类加载器接收到加载类的任务时,会自底向上查找是否已加载,再由顶向下尝试加载

类加载器的父子关系

详细流程

1. 类加载器接收到加载任务后,先检查自身是否已加载该类,若已加载则直接返回。

2. 若未加载,将任务委派给父类加载器,父类加载器重复步骤 1-2。

3. 若父类加载器(直至启动类加载器)均未加载,且启动类加载器无法加载(类不在其加载路径),则由扩展类加载器尝试加载。

4. 若扩展类加载器也无法加载,由应用程序类加载器尝试加载。

案例分析

案例 1:类在启动类加载器路径中

假设com.itheima.my.A在启动类加载器加载目录(如/jre/lib),应用程序类加载器接收到加载任务:

1. 应用程序类加载器未加载过A,委派给父类(扩展类加载器)。

2. 扩展类加载器未加载过A,委派给父类(启动类加载器)。

3. 启动类加载器已加载过A,直接返回。

案例 2:类在扩展类加载器路径中

假设com.itheima.my.B在扩展类加载器加载目录(如/jre/lib/ext),应用程序类加载器接收到加载任务:

1. 应用程序类加载器未加载过B,委派给扩展类加载器。

2. 扩展类加载器未加载过B,委派给启动类加载器。

3. 启动类加载器未加载过BB不在其加载路径,委派给扩展类加载器。

4. 扩展类加载器加载B成功,返回。

补充问题:

双亲委派机制的作用

  1. 保证类加载安全性:避免恶意代码替换 JDK 核心类库(如java.lang.String),确保核心类库完整性和安全性。

  2. 避免重复加载:同一类不会被多个类加载器重复加载。

如何指定类加载器加载类

在 Java 中可通过两种方式主动加载类

1.使用Class.forName方法:使用当前类的类加载器加载指定类,示例:

Class<?> clazz = Class.forName("com.itheima.my.A");

2.获取类加载器,调用loadClass方法:指定类加载器加载,示例:

// 获取应用程序类加载器
​
ClassLoader classLoader = Demo1.class.getClassLoader();
​
// 使用应用程序类加载器加载com.itheima.my.A
​
Class<?> clazz = classLoader.loadClass("com.itheima.my.A");
  • Class.forName()java.lang.Class类的静态方法,加载指定全类名的类时会主动执行类的初始化(如静态代码块、静态变量初始化),常用于反射或需触发类初始化的场景。
  • loadClass():java.lang.ClassLoader类的实例方法,仅将类加载到 JVM 但默认不进行初始化,主要用于类加载器自定义实现与类加载控制。
  • 区别:二者均可能抛出ClassNotFoundException,核心区别在于是否主动初始化类及调用主体、适用场景不同。

面试题

:若一个类重复出现在三个类加载器的加载位置,由谁加载?

:启动类加载器加载,双亲委派机制中启动类加载器优先级最高。


:String 类能覆盖吗?在项目中创建java.lang.String类,会被加载吗?

:不能。启动类加载器会优先加载rt.jar中的java.lang.String类,项目中的String类不会被加载。


:类的双亲委派机制是什么?

:当类加载器加载类时,自底向上查找是否已加载,若均未加载则由顶向下尝试加载。应用程序类加载器父类是扩展类加载器,扩展类加载器父类是启动类加载器。好处是保证核心类库安全、避免重复加载。

1.6 打破双亲委派机制

打破双亲委派机制历史上有三种方式,本质上仅第一种真正打破:

  1. 自定义类加载器并重写loadClass方法(如 Tomcat 实现应用间类隔离)。

  2. 线程上下文类加载器(如 JDBC、JNDI 使用)。

  3. Osgi 框架的类加载器(历史方案,目前很少使用)。

自定义类加载器

背景

原理

ClassLoader核心方法

1. public Class<?> loadClass(String name):类加载入口,实现双亲委派机制,内部调用findClass

2. protected Class<?> findClass(String name):子类实现,获取二进制数据并调用defineClass

3. protected final Class<?> defineClass(String name, byte[] b, int off, int len):校验类名,调用虚拟机底层方法将字节码加载到内存。

4. protected final void resolveClass(Class<?> c):执行类生命周期的连接阶段。

1. 入口方法:

2. 再进入看下:

如果查找都失败,进入加载阶段,首先会由启动类加载器加载,这段代码在findBootstrapClassOrNull中。如果失败会抛出异常,父类加载器加载失败就会抛出异常,回到子类加载器的这段代码,这样就实现了加载并向下传递。

3. 最后根据传入的参数判断是否进入连接阶段:

自定义类加载器实现

重新实现下面的核心代码(loadclass)就可以打破双亲委派机制

package classloader.broken;//package com.itheima.jvm.chapter02.classloader.broken;import org.apache.commons.io.IOUtils;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.ProtectionDomain;
import java.util.regex.Matcher;/*** 打破双亲委派机制 - 自定义类加载器*/public class BreakClassLoader1 extends ClassLoader {private String basePath;private final static String FILE_EXT = ".class";//设置加载目录public void setBasePath(String basePath) {this.basePath = basePath;}//使用commons io 从指定目录下加载文件private byte[] loadClassData(String name)  {try {String tempName = name.replaceAll("\\.", Matcher.quoteReplacement(File.separator));FileInputStream fis = new FileInputStream(basePath + tempName + FILE_EXT);try {return IOUtils.toByteArray(fis);} finally {IOUtils.closeQuietly(fis);}} catch (Exception e) {System.out.println("自定义类加载器加载失败,错误原因:" + e.getMessage());return null;}}//重写loadClass方法@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {//如果是java包下,还是走双亲委派机制if(name.startsWith("java.")){return super.loadClass(name);}//从磁盘中指定目录下加载byte[] data = loadClassData(name);//调用虚拟机底层方法,方法区和堆区创建对象return defineClass(name, data, 0, data.length);}public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, IOException {//第一个自定义类加载器对象BreakClassLoader1 classLoader1 = new BreakClassLoader1();classLoader1.setBasePath("D:\\lib\\");Class<?> clazz1 = classLoader1.loadClass("com.itheima.my.A");//第二个自定义类加载器对象BreakClassLoader1 classLoader2 = new BreakClassLoader1();classLoader2.setBasePath("D:\\lib\\");Class<?> clazz2 = classLoader2.loadClass("com.itheima.my.A");System.out.println(clazz1 == clazz2);Thread.currentThread().setContextClassLoader(classLoader1);System.out.println(Thread.currentThread().getContextClassLoader());System.in.read();}
}

​ ​ ​ 问题一:为什么这段代码打破了双亲委派机制?

双亲委派机制的核心是:类加载器在加载类时,会先委托给父类加载器加载,只有父类加载器无法加载时,才自己尝试加载

而这段代码通过重写 loadClass() 方法打破了这一机制:

  • 对于非 java. 开头的类(如自定义类 com.itheima.my.A),代码直接跳过父类加载器,自己从指定目录加载类(loadClassData() 方法读取字节码)
  • 只有 java. 开头的核心类才遵循双亲委派(调用 super.loadClass(name) 让父类加载器处理)

正常情况下,loadClass() 方法的默认实现会先委托父类加载器,而这里重写后改变了这一流程,因此打破了双亲委派机制。

问题二:两个自定义类加载器加载相同限定名的类,不会冲突吗?

不会冲突,原因是:

在 JVM 中,一个类的唯一性由「类的全限定名 + 加载它的类加载器」共同决定。即:

  • 即使两个类的全限定名完全相同,只要由不同的类加载器加载,JVM 会认为它们是两个不同的类
  • 代码中 classLoader1 和 classLoader2 是两个不同的实例(不同的类加载器对象),因此它们加载的 com.itheima.my.A 会被视为两个不同的类
  • 这也是为什么代码中 clazz1 == clazz2 的输出结果为 false

这种特性保证了即使类名相同,只要加载器不同,就不会产生冲突,这也是 Java 类加载机制的重要设计。

关键说明

自定义类加载器的父类:默认情况下,自定义类加载器的父类加载器是应用程序类加载器(AppClassLoader),因ClassLoader构造方法中parentgetSystemClassLoader()(返回AppClassLoader)设置。

线程上下文类加载器

背景

双亲委派机制核心:类加载器在加载类时,优先委托父类加载器去加载。只有当父类加载器无法加载(比如父类加载器的搜索路径里没有该类),当前类加载器才会尝试自己加载。

原理

SPI 是 “约定好的配置方式”,让核心库能找到第三方实现的类名。

线程上下文类加载器 是 “工具”,让核心库(由父加载器加载)能突破双亲委派,用子加载器(应用程序类加载器)去加载第三方库的类。

SPI 机制

SPI 机制通过在 jar 包META-INF/services目录下放置接口名文件(如java.sql.Driver),文件中写入实现类全限定名(如com.mysql.cj.jdbc.Driver),从而找到接口实现类。

JDBC 加载驱动流程

  1. 启动类加载器加载DriverManager

  2. DriverManager初始化时,调用LoadInitialDrivers方法,通过 SPI 机制加载META-INF/services/java.sql.Driver中的实现类。

  3. SPI 机制使用线程上下文类加载器(应用程序类加载器)加载 MySQL 驱动类(com.mysql.cj.jdbc.Driver)。

  4. 驱动类初始化时,调用DriverManager.registerDriver(new Driver()),完成注册。

JDBC案例中真的打破了双亲委派机制吗?

最早这个论点提出是在周志明《深入理解Java虚拟机》中,他认为打破了双亲委派机制,这种由启动类加载器加载的类,委派应用程序类加载器去加载类的方式,所以打破了双亲委派机制。

但是如果我们分别从DriverManager以及驱动类的加载流程上分析,JDBC只是在DriverManager加载完之后,通过初始化阶段触发了驱动类的加载,类的加载依然遵循双亲委派机制。

所以我认为这里没有打破双亲委派机制,只是用一种巧妙的方法让启动类加载器加载的类,去引发的其他类的加载。

Osgi 框架的类加载器

Osgi 是模块化框架,实现了同级类加载器委托加载,还支持热部署(服务不停止时动态更新字节码)。但目前使用较少,此处不展开。

热部署案例:Arthas 不停机修复线上问题

注意事项

  1. 程序重启后,字节码恢复,需将新 class 文件放入 jar 包更新。

  2. retransform不能添加方法 / 字段,不能更新正在执行的方法。

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

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多源最短路(Floyd算法

多源最短路简介 多源最短路算法用于解决图中任意两节点间最短路径的问题&#xff0c;广泛应用于交通网络、社交关系分析、路由优化等场景。与单源最短路&#xff08;如Dijkstra&#xff09;不同&#xff0c;它一次性计算所有节点对的最短距离&#xff0c;适合需要全局路径规划的…

【攻防实战】记一次攻防实战全流程

那天我向众神祈祷&#xff0c;最后回答我的却只有挣扎十年依旧不甘的自己&#xff01;成功究竟是馈赠还是偿还。 前言 网络安全技术学习&#xff0c;承认⾃⼰的弱点不是丑事&#xff0c;只有对原理了然于⼼&#xff0c;才能突破更多的限制。 拥有快速学习能力的安全研究员&…

Anaconda配置环境变量和镜像

Anaconda配置环境变量和镜像 下载失败就是开了梯子 Anaconda 作用&#xff1a;包管理&#xff08;集中&#xff0c;有序&#xff09;和环境管理&#xff08;版本切换&#xff09;使用conda命令对虚拟环境创建、删除自带python解释器pip&#xff08;python自带的包管理工具&…

给定单词倒排

实现代码&#xff1a;public static void main(String[] args) {Scanner scanner new Scanner(System.in);// 输入的字符串String input scanner.nextLine();// 存储单词List<String> words new ArrayList<>();// 存储当前单词StringBuilder currentWord new S…

IO进程——进程引入、进程函数接口

一、引入1、进程&程序1.1 程序编译好的可执行的文件存放在磁盘上的指令和数据的有序集合&#xff08;文件&#xff09;程序是静态的&#xff0c;没有任何执行的概念1.2 进程一个独立的可调度的任务执行一个程序所分配的资源的总称进程是程序执行的一次过程进程是动态的&…

周末游戏推荐:安卓端俄罗斯方块,经典与创新的结合

前段时间&#xff0c;每到周末我都会给大家推荐一些离线的经典游戏&#xff0c;原本打算将这个传统一直延续下去。然而&#xff0c;我实在找不到足够好用且无广告的游戏了。有些游戏刚开始用的时候还不错&#xff0c;但用着用着就开始频繁弹出广告&#xff0c;这让我实在不敢向…

《用 Scikit-learn 构建 SVM 分类模型:从原理到实战的全流程解析》

《用 Scikit-learn 构建 SVM 分类模型:从原理到实战的全流程解析》 一、引言:为什么选择 SVM? 在机器学习的众多算法中,支持向量机(SVM)以其强大的分类能力和良好的泛化性能,在文本分类、人脸识别、医学诊断等领域广泛应用。尤其在中小规模数据集上,SVM 往往能提供比…

一文学会CMakeLists.txt: CMake现代C++跨平台工程化实战

你能学到什么&#xff1f;朋友们好久不见&#xff0c;我是alibli&#xff0c;好久没有更新博客了。今天本人将通过构造一个实际的虚拟小项目&#xff0c;来让你彻底掌握CMake跨平台工程构建&#xff0c;学会CMakeLists.txt语法。该项目实现了一个简单的平方、立方的计算程序&am…

高并发场景下限流算法实践与性能优化指南

高并发场景下限流算法实践与性能优化指南 在大规模并发访问环境中&#xff0c;合理的限流策略能保护后端服务稳定运行&#xff0c;避免系统因瞬时高并发导致资源耗尽或崩溃。本文将从原理出发&#xff0c;深入解析几种主流限流算法&#xff0c;并结合Java和Redis给出完整可运行…

Vue3应用执行流程详解

精确化的完整执行流程 (以 Vite Vue3 SPA 为例)整个过程可以分为两部分&#xff1a;首次访问的“冷启动”和后续的Vue应用接管。第一部分&#xff1a;首次访问与页面加载客户端&#xff1a;发送请求用户打开浏览器&#xff0c;输入 URL&#xff08;如 http://localhost:5173&a…

Redis 持久化与高可用实践(RDB / AOF / Sentinel / Cluster 全解析)

这篇是我把几套生产环境踩坑与复盘整理成的一份“从 0 到 1 长期可维护”的实践文。目标是&#xff1a;明确策略、给出默认可用的配置模板、把常见坑一次讲透。 适用场景&#xff1a;新项目选型、老项目稳定性加固、从单机迁移到 HA/Cluster、应对数据安全与故障切换要求。目录…

Linux内核的PER_CPU机制

参考书《Linux内核模块开发技术指南》 1.原理 在多核CPU的情况下&#xff0c;为了提高CPU并发执行的效率&#xff0c;对于某些不是必须要在核间进行同步访问的资源&#xff0c;可以为每一个CPU创建一个副本&#xff0c;让每个CPU都访问自身的数据副本&#xff0c;而不是通过加锁…

VSCode 的百度 AI编程插件

VSCode 的百度 AI编程插件主要是 Baidu Comate&#xff08;文心快码&#xff09;&#xff0c;这是一款基于文心大模型的新一代编码辅助工具&#xff0c;旨在提升开发者的编码效率&#xff0c;让写代码变得更简单。以下是关于 Baidu Comate 的详细介绍&#xff1a; 一、功能特点…

阿里云监控使用

阿里云的云监控服务&#xff08;CloudMonitor&#xff09;是一款简单易用、功能强大的监控工具&#xff0c;主要用来帮助用户实时监控阿里云上的各种资源&#xff08;比如服务器、数据库、网络等&#xff09;&#xff0c;并在出现问题时及时发出警报&#xff0c;确保业务稳定运…

嵌入式C语言-关键字typedef

定义和作用 typedef是C/C中的一个关键字&#xff0c;作用是为现有的数据类型&#xff08;int 、char 、flaot等&#xff09;创建新的别名&#xff0c;其目的是为了方便阅读和理解代码。 用法 typedef 原有类型名 新类型名;基本类型创建别名 typedef unsigned char uint8_t; typ…

【混合开发】【大前端++】Vue节点优化Dome之单节点轮播图片播放视频二

动图更精彩 背景 Vue作为大前端开发页面交互&#xff0c;在数字屏&#xff0c;智慧大屏等大屏幕开发过程中&#xff0c;轮播效果作为丰富的展示组件经常作为首选。但也因为这个组件的交互体验很好&#xff0c;于是各种单点组件增加到轮播效果里。经过业务的扩展&#xff0c;人…

前端开发核心技术与工具全解析:从构建工具到实时通信

觉得主包文章可以的,可以点个小爱心哟&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 主页:一位搞嵌入式的 genius-CSDN博客 系列文章专栏: https://blog.csdn.net/m0_73589512/category_13028539.html 前端开发核心技术与工具全解…

GPT 系列论文 gpt3-4 175B参数 + few-shot + 多模态输入 + RLHF + system

GPT&#xff0c;GPT-2&#xff0c;GPT-3 论文精读【论文精读】 GPT-4论文精读 从1750亿参数的文本预言家&#xff0c;到多模态的通用天才&#xff0c;OpenAI用两次震撼世界的发布&#xff0c;重新定义了人工智能的可能性边界。这份笔记将带你深入GPT-3和GPT-4的核心突破&#…

.gitignore文件的作用及用法

目录 ​​.gitignore 文件的作用​​ ​​.gitignore 的基本语法​​ ​​Python 项目的 .gitignore 示例​​ ​​如何使用 .gitignore​​ ​​1. 创建 .gitignore 文件​​ ​​2. 编辑 .gitignore​​ ​​3. 检查 Git 状态​​ ​​常见问题​​ ​​Q1&#xff…

QEMU环境准备

QEMU环境准备 下载 qemu # qemu sudo apt install qemu-system-arm # gdb sudo apt install gdb-multiarchsudo apt-get update sudo apt-get install build-essential zlib1g-dev pkg-config libglib2.0-dev \libpixman-1-dev libfdt-dev ninja-build下载并自行编译 qemu(可…