《深入理解Java虚拟机》| 类加载与双亲委派机制

**摘要:**本文围绕类加载机制展开,详解类的生命周期,介绍类加载器分类、双亲委派机制及打破该机制的方式。

书摘内容

虚拟机类加载机制


"Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最
终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。"

类加载的时机

Java 虚拟机中,一个类从被加载到内存,到最终被卸载,会经历 7 个阶段 ,其中验证、准备、解析统称为连接(Linking)

类加载的过程

加载(Loading)

这是类加载的第一个阶段,核心任务是将类的字节码数据加载到虚拟机内存中。

  • 主要工作

    • 通过一个类的全限定名,获取其定义的二进制字节流(来源可以是 .class 文件、JAR、网络、动态生成等)。

    • 将这个字节流所代表的静态存储结构,转化为方法区的运行时数据结构。

    • 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

  • 关键点:加载阶段既可以使用虚拟机内置的引导类加载器,也可以使用用户自定义的类加载器完成。

验证**(Verification)**

这是连接阶段的第一步,目的是确保加载的类符合《Java 虚拟机规范》,不会危害虚拟机自身安全。

  • 主要工作

    • 文件格式验证 :验证字节流是否符合 Class 文件格式的规范,例如是否以魔数 0xCAFEBABE 开头。

    • 元数据验证 :对字节码描述的信息进行语义分析,例如这个类是否有父类、是否继承了不允许被继承的类(如 final 类)。

    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的,例如保证跳转指令不会跳转到方法体以外的字节码上。

    • 符号引用验证:确保符号引用能够被正确解析,例如通过符号引用中描述的全限定名能否找到对应的类。

  • 关键点 :验证阶段是非常重要的,但不是必须的,如果运行的类都是可信的(如内部项目),可以通过 -Xverify:none 参数关闭大部分验证,以缩短类加载时间。

准备(Preparation)

这个阶段正式为类中定义的静态变量分配内存并设置初始值。

  • 主要工作

    • 内存分配:仅为类变量(static 修饰的变量)分配内存,不包括实例变量,实例变量会在对象实例化时随对象一起分配在 Java 堆中。

    • 初始值设置:这里的初始值通常是数据类型的零值,例如 int0booleanfalsereferencenull

    • 特殊情况 :如果类变量被 final 修饰(如 public static final int value = 123),那么在准备阶段就会直接将其初始化为指定的值 123,而不是零值。

解析(Resolution)

这个阶段将常量池内的符号引用 替换为直接引用

  • 主要工作

    • 符号引用:以一组符号来描述所引用的目标,与虚拟机内存布局无关,引用的目标不一定已经加载到内存。

    • 直接引用:可以直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄,与虚拟机内存布局相关,引用的目标一定已经存在于内存中。

    • 解析的对象包括:类或接口、字段、类方法、接口方法、方法类型等。

  • 关键点:解析阶段通常在初始化之后再执行,以支持动态绑定,但也可以在初始化之前进行,这取决于虚拟机的实现。

初始化(Initialization)

这是类加载过程的最后一步,也是真正执行类中定义的 Java 代码的阶段。

  • 主要工作

    • 执行类构造器 <clinit>() 方法,该方法由编译器自动收集类中所有类变量的赋值动作和静态代码块 static{} 中的语句合并产生。

    • 虚拟机会保证在多线程环境下,<clinit>() 方法被正确地加锁和同步,确保只有一个线程执行,其他线程阻塞等待。

  • 触发时机:虚拟机严格规定了 5 种必须对类进行初始化的场景:

    • 遇到 newgetstaticputstaticinvokestatic 这 4 条字节码指令时。

    • 使用 java.lang.reflect 包的方法对类进行反射调用时。

    • 初始化一个类时,如果发现其父类还没有初始化,则先触发父类的初始化。

    • 当虚拟机启动时,用户指定的主类(包含 main() 方法的类)会被优先初始化。

    • 当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为特定类型的方法句柄,且其对应的类未初始化时。

类加载器

类与类加载器
双亲委派模型
  1. 从 Java 虚拟机视角,类加载器分为两类:

    1. 启动类加载器(Bootstrap ClassLoader):由 C++ 实现,是虚拟机自身的一部分。

    2. 其他所有类加载器:由 Java 语言实现,独立于虚拟机外部,继承自抽象类java.lang.ClassLoader

  2. 从 Java 开发人员视角(JDK 8 及之前),类加载器分为三层,采用双亲委派架构:

    1. 启动 类加载器 :负责加载<JAVA_HOME>/lib目录或-Xbootclasspath指定路径中、能被虚拟机识别的类库(如 rt.jar);无法被 Java 程序直接引用,自定义类加载器委派给它时用null表示。

    2. 扩展 类加载器 :由 Java 代码实现(sun.misc.Launcher$ExtClassLoader),负责加载<JAVA_HOME>/lib/ext目录或java.ext.dirs指定路径的类库,是 Java 系统类库的扩展机制;JDK 9 后被模块化扩展能力取代。

    3. 应用程序 类加载器 :由sun.misc.Launcher$AppClassLoader实现,是ClassLoader.getSystemClassLoader()的返回值,也叫 "系统类加载器";负责加载用户类路径(ClassPath)上的类库,无自定义类加载器时为程序默认类加载器。

  3. 双亲委派模型的层级关系为:启动类加载器是扩展类加载器的父加载器,扩展类加载器是应用程序类加载器的父加载器,应用程序类加载器是自定义类加载器的父加载器。

  4. 双亲委派的核心逻辑是:当一个类加载器收到加载请求时,先不自己尝试加载,而是将请求委派给父类加载器,依次向上,直到启动类加载器;只有当父加载器反馈无法完成加载时,子加载器才会尝试自己加载。

课程内容 - 类加载机制

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) 3 FFD8FF
PNG (png) 4 89504E47(文件尾也有要求)
bmp 2 424D
XML (xml) 5 3C3F786D6C
AVI (avi) 4 41564920
Java 字节码文件 (.class) 4 CAFEBABE

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 代码拓展渠道,常见渠道如下:
  1. 类加载器加载完类后,Java 虚拟机会将字节码中的信息保存到方法区,生成一个InstanceKlass对象,该对象保存类的所有信息(含实现多态的虚方法表等)。
  1. 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.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. 类加载器接收到加载任务后,先检查自身是否已加载该类,若已加载则直接返回。
  1. 若未加载,将任务委派给父类加载器,父类加载器重复步骤 1-2。

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

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

案例分析

案例 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方法:使用当前类的类加载器加载指定类,示例:

java 复制代码
Class<?> clazz = Class.forName("com.itheima.my.A");

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

java 复制代码
// 获取应用程序类加载器
​
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. 入口方法:
  1. 再进入看下:

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

  1. 最后根据传入的参数判断是否进入连接阶段:
自定义类加载器实现

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

java 复制代码
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方法
    @Override
    public 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 会认为它们是两个不同的类
  • 代码中 classLoader1classLoader2 是两个不同的实例(不同的类加载器对象),因此它们加载的 com.itheima.my.A 会被视为两个不同的类
  • 这也是为什么代码中 clazz1 == clazz2 的输出结果为 false

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

关键说明

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

Osgi 框架的类加载器

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

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

注意事项

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

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


恭喜你学习完毕!✿

相关推荐
寻寻觅觅☆5 小时前
东华OJ-基础题-106-大整数相加(C++)
开发语言·c++·算法
m0_607076605 小时前
CSS3 转换,快手前端面试经验,隔壁都馋哭了
前端·面试·css3
l1t5 小时前
在wsl的python 3.14.3容器中使用databend包
开发语言·数据库·python·databend
青云计划6 小时前
知光项目知文发布模块
java·后端·spring·mybatis
赶路人儿6 小时前
Jsoniter(java版本)使用介绍
java·开发语言
NEXT066 小时前
二叉搜索树(BST)
前端·数据结构·面试
NEXT066 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
ceclar1236 小时前
C++使用format
开发语言·c++·算法
探路者继续奋斗7 小时前
IDD意图驱动开发之意图规格说明书
java·规格说明书·开发规范·意图驱动开发·idd
码说AI7 小时前
python快速绘制走势图对比曲线
开发语言·python