JVM——如何打造一个类加载器?
引入
在Java应用程序的生命周期中,类加载器扮演着至关重要的角色。它是Java运行时环境的核心组件之一,负责在需要时动态加载类文件到JVM中。理解类加载器的工作原理以及如何自定义类加载器,不仅可以帮助我们更好地管理应用程序的类加载过程,还能提升系统的安全性和灵活性,满足特定场景下的业务需求。
类加载器家族详解
启动类加载器(Bootstrap ClassLoader)
- 启动类加载器是JVM自带的类加载器,由C++实现,没有对应的Java类。它的主要职责是加载Java的核心类库,如
java.lang
、java.util
等。这些类库位于jre/lib
目录下,例如rt.jar
文件中就包含了Java运行时的核心类。启动类加载器是最顶层的类加载器,它为JVM的启动和基本运行提供了必要的类支持。 - 由于启动类加载器的特殊性,它无法直接在Java代码中被引用。它是类加载器层次结构的起始点,为整个类加载体系奠定了基础,确保了Java核心API的稳定性和可靠性。
扩展类加载器(Extension ClassLoader)
- 扩展类加载器负责加载位于
jre/lib/ext
目录下的JAR文件。这些JAR文件包含了一些扩展的Java类库,例如加密、字符集转换等工具类库。在不同的Java版本和操作系统平台上,ext
目录下的内容可能会有所不同。 - 在Java 9及更高版本中,随着模块系统的引入,扩展类加载器逐渐被平台类加载器(Platform ClassLoader)所取代。这一变化使得Java的模块化管理更加精细和高效,但扩展类加载器在Java 8及更早版本中仍然是一个重要的类加载组件。
应用类加载器(Application ClassLoader)
- 应用类加载器是系统默认的类加载器,负责加载应用程序classpath路径下的类文件。它通常是从
CLASSPATH
环境变量或-cp
命令行选项指定的路径中加载类。当我们使用IDE(如IntelliJ IDEA或Eclipse)开发Java程序时,IDE会自动配置CLASSPATH
,将项目的编译输出目录(如bin
或target
)以及项目依赖的库文件路径包含在内。 - 开发者可以通过
ClassLoader.getSystemClassLoader()
方法获取应用类加载器的实例。应用类加载器使得Java应用程序能够灵活地加载和运行自定义类和第三方库,是日常开发中最常用的类加载器。
自定义类加载器
- 在一些复杂的业务场景中,预设的类加载器可能无法满足需求。这时,开发者可以创建自定义类加载器来实现特定的功能。例如,可能需要从网络远程加载类、对类文件进行加密解密处理,或者实现类的热部署等功能。
- 自定义类加载器通过继承
ClassLoader
类并重写findClass
方法来实现。在实现过程中,需要遵循双亲委派模型的原则,即先将类加载请求委派给父类加载器,只有当父类加载器无法加载时,才尝试自己加载。同时,还需要妥善处理异常情况,如类文件未找到、文件读取错误等,以确保类加载过程的健壮性。
三、双亲委派模型
(一)核心流程
当一个类加载器需要加载一个类时,它会遵循以下步骤:
- 委派给父类加载器:类加载器首先将加载请求委派给父类加载器。例如,当应用类加载器尝试加载一个类时,它会先请求扩展类加载器进行加载;扩展类加载器又会将请求进一步委派给启动类加载器。
- 父类加载器处理:父类加载器会按照相同的委派机制,继续将请求向上委派,直到顶级的启动类加载器。启动类加载器会尝试加载该类,如果类的全限定名对应的是Java核心类库中的类,则加载成功并返回对应的
Class
对象。 - 加载类:如果父类加载器无法加载该类(例如,类不在核心类库或扩展目录中),则当前类加载器会尝试自己加载。它会在指定的路径下查找类文件,将其字节码加载到JVM中,并返回对应的
Class
实例。如果加载成功,则类可以被应用程序使用;如果加载失败,则会抛出ClassNotFoundException
。 - 类加载隔离:不同的类加载器可以加载同一个类的不同版本,这些类在JVM中是相互隔离的。这意味着即使两个类具有相同的全限定名,如果它们是由不同的类加载器加载的,JVM会将它们视为不同的类。这种隔离机制可以避免类之间的冲突,特别是在需要使用不同版本的第三方库时非常有用。
应用场景
安全沙箱隔离:通过自定义类加载器实现安全沙箱的机制,对不可信的类进行隔离加载和限制权限,提高系统的安全性。例如,在Java Applet(现已废弃)时代,为了防止恶意代码访问本地资源,Applet类加载器会将从网络加载的类与本地类隔离,确保其运行在沙箱环境中。
动态扩展和插件化:自定义类加载器可以实现动态加载和卸载功能,使得系统能够动态扩展和插件化。例如,一些IDE工具允许用户安装插件,这些插件的类可以通过自定义类加载器动态加载,而不会影响主程序的运行。这样可以提升程序的灵活性和可扩展性,使应用程序能够适应不断变化的需求。
多版本隔离:在同一个程序中使用不同版本的库文件时,自定义类加载器可以加载不同版本的类,从而避免版本冲突。例如,一个大型项目可能依赖多个第三方库,而这些库可能又依赖不同版本的同一个底层库。通过使用不同的类加载器加载这些不同版本的底层库,可以确保各个上层库能够正常工作,不会因为类的版本不兼容而出现错误。
限制
双亲委派模型存在一些限制:
- 类的静态加载状态:一旦一个类被加载到JVM中,它在整个应用程序的生命周期内都会保持加载状态,即使类的定义已经发生了变化也是如此。这是因为Java内存管理机制的设计使得
ClassLoader
会持有已加载类的Class
对象,并且只有当ClassLoader
、Class
对象以及其所有实例都不可达时,垃圾回收器才会回收这个类。这限制了我们动态加载新类的能力,例如在开发过程中无法直接重新加载修改后的类,必须重新启动应用程序才能看到更改后的效果。 - 类加载顺序的固定性:双亲委派模型规定了类加载的严格顺序,这在某些特殊场景下可能不够灵活。例如,当需要优先加载自定义的类而不是系统提供的类时,双亲委派模型默认的加载顺序可能会导致问题。
Tomcat为什么能够突破限制?
Tomcat通过自定义类加载器的方式突破了双亲委派模型的限制。它采用了层次化的类加载器结构,包括公共类加载器(common ClassLoader)、共享类加载器(shared ClassLoader)和Web应用类加载器(Webapp ClassLoader)。这些类加载器共同协作,实现了以下功能:
- 隔离不同的Web模块:每个Web应用都有自己的Webapp ClassLoader,这样不同Web模块的类可以相互隔离。这避免了不同应用之间的类相互干扰,提高了系统的稳定性和安全性。
- 共享公共类:公共类加载器负责加载所有Web应用都可以访问的公共类库,这些类库位于
Tomcat/lib
目录下。这样可以减少重复加载相同的类,提高资源利用率。 - 灵活的类加载顺序:Tomcat的类加载器在加载类时,会先尝试加载Web应用自己的类,然后再委托给父类加载器。这种加载顺序与双亲委派模型相反,使得Web应用可以使用自己的类来覆盖父类加载器中的同名类。这为动态部署和更新Web应用提供了便利,例如在开发过程中可以更方便地更新类文件而无需重启整个Tomcat服务器。
类加载器的演进
随着JVM的不断迭代更新,类加载器也经历了一系列的演进,以适应新的需求和提高性能。
JDK 9及更高版本中的变化
- 引入模块系统(JPMS):从JDK 9开始,Java引入了模块系统(Java Platform Module System,JPMS),将Java核心库分割成了一系列相互关联的模块。每个模块明确指定了其公开的包和依赖的其他模块,这样可以实现更精细的访问控制和依赖管理。模块系统使得Java的代码结构更加清晰,也为类加载器带来了新的变化。
- 平台类加载器和系统类加载器的引入:原本的启动类加载器被拆分成了平台类加载器和系统类加载器。平台类加载器负责加载JDK模块,而系统类加载器则用于加载应用程序模块。这种拆分使得类加载器的职责更加明确,也为模块化的类加载提供了支持。
JDK 11及更高版本中的变化
- 类数据共享(CDS)技术:JDK 11中引入了类数据共享技术,允许多个Java进程共享同一个JVM类元数据区域。这可以减少每个Java进程的内存占用,提高启动性能,特别是在运行多个Java应用的环境中效果显著。通过共享类元数据,不同进程可以减少重复加载相同类的开销。
JDK 17及更高版本中的变化
- 移除系统类加载器:在JDK 17中,系统类加载器被移除,所有的类加载操作由应用类加载器接管。这一改动简化了JVM的架构,减少了潜在的安全风险。例如,避免了系统类加载器在加载模块时可能出现的映射关系混乱问题,同时也提高了模块化系统的安全性和运行时性能。
打造自定义类加载器的实践案例
下面是一个自定义类加载器的完整实例,展示了如何通过继承ClassLoader
类并重写findClass
方法来实现自定义的类加载逻辑:
import java.io.*;
import java.util.HashMap;// 自定义ClassLoader类,继承自ClassLoader
public class CustomClassLoader extends ClassLoader {// 定义了类文件的根路径private String rootDir;// 缓存已经加载的类private HashMap<String, Class<?>> loadedClasses;/*** Constructor** @param rootDir 类文件的根目录路径*/public CustomClassLoader(String rootDir) {this.rootDir = rootDir;// 初始化缓存HashmaploadedClasses = new HashMap<>();}/*** 加载类文件并返回Class实例** @param className 类的全限定名* @return 加载的类的Class实例* @throws ClassNotFoundException 如果类未被找到或加载*/@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {// 从已加载的类缓存中查找类Class<?> loadedClass = loadedClasses.get(className);// 如果类已经被加载,从缓存中返回if (loadedClass != null) {return loadedClass;}// 否则读取类文件的字节码byte[] classBytes = getClassBytes(className);if (classBytes == null) {throw new ClassNotFoundException();}// 在锁定的环境中,定义类并将类放入已加载的类缓存中synchronized (loadedClasses) {loadedClass = defineClass(className, classBytes, 0, classBytes.length);loadedClasses.put(className, loadedClass);}return loadedClass;}/*** 根据类名读取类文件的字节码** @param className 类的全名(包括包名)* @return 类文件的字节码*/private byte[] getClassBytes(String className) {// 将全名转换为文件名String classPath = rootDir + '/' + className.replace('.', '/') + ".class";FileInputStream fis = null;ByteArrayOutputStream baos = null;try {fis = new FileInputStream(classPath);baos = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int bytesRead;// 循环读取文件直到文件结束while ((bytesRead = fis.read(buffer)) != -1) {baos.write(buffer, 0, bytesRead);}// 返回字节流的字节数组return baos.toByteArray();} catch (IOException e) {e.printStackTrace();} finally {// 关闭资源try {if (fis != null) {fis.close();}if (b aos != null) {baos.close();}} catch (IOException e2) {e2.printStackTrace();}}return null;}public static void main(String[] args) {// 创建新的CustomClassLoader实例CustomClassLoader customClassLoader = new CustomClassLoader("/path/to/classes");try {// 通过自定义的类加载器加载一个类,输出其类名Class<?> sampleClass = customClassLoader.loadClass("com.example.SampleClass");System.out.println("Class loaded successfully: " + sampleClass.getName());} catch (ClassNotFoundException e) {e.printStackTrace();}}
}
总结
通过深入理解类加载器家族的成员及其职责、双亲委派模型的原理和应用场景,以及类加载器在不同JDK版本中的演进,我们可以更加灵活地管理和优化Java应用程序的类加载过程。自定义类加载器为我们提供了强大的工具,以应对复杂的业务需求和特定的技术挑战。在实际开发中,合理利用类加载器的特性,可以帮助我们构建更加安全、高效和可扩展的Java应用。
相关文章:
JVM——如何打造一个类加载器?
引入 在Java应用程序的生命周期中,类加载器扮演着至关重要的角色。它是Java运行时环境的核心组件之一,负责在需要时动态加载类文件到JVM中。理解类加载器的工作原理以及如何自定义类加载器,不仅可以帮助我们更好地管理应用程序的类加载过程&…...

【MATLAB去噪算法】基于ICEEMDAN联合小波阈值去噪算法
ICEEMDAN联合小波阈值去噪算法相关文献 (注:目前相关论文较少,应用该套代码可发直接一些水刊) 一、CEEMDAN的局限性 模式残留噪声问题:原始CEEMDAN在计算每个IMF时直接对噪声扰动的信号进行模态分解并平均。 后果&a…...
c++ Base58编码解码
Base58 字符集 Base58 使用 58 个字符进行编码,字符集为:123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz。注意:0(零)、O(大写字母O)、I(大写字母I)和 l&a…...
证券交易柜台系统解析与LinkCounter解决方案开发实践
第一章 证券交易柜台系统基础解析 1.1 定义与行业定位 证券交易柜台系统(Trading Counter System)是券商经纪业务的核心支撑平台,承担投资者指令传输、风险控制、清算结算等职能。根据中国证监会《证券期货业网络信息安全管理办法》要求&am…...

XXTEA,XTEA与TEA
TEA、XTEA和XXTEA都是分组加密算法,它们在设计、安全性、性能等方面存在显著区别。以下是它们的主要区别: 密钥长度 TEA:使用128位密钥。 XTEA:通常使用128位或256位密钥。 XXTEA:密钥长度更灵活,可以使用任…...

机器人玩转之---嵌入式开发板基础知识到实战选型指南(包含ORIN、RDK X5、Raspberry pi、RK系列等)
1. 基础知识讲解 1.1 什么是嵌入式开发板? 嵌入式开发板是一种专门设计用于嵌入式系统开发的硬件平台,它集成了微处理器、内存、存储、输入输出接口等核心组件于单块印刷电路板上。与传统的PC不同,嵌入式开发板具有体积小、功耗低、成本适中…...

腾讯云国际版和国内版账户通用吗?一样吗?为什么?
在当今全球化的数字化时代,云计算服务成为众多企业和个人拓展业务、存储数据的重要选择。腾讯云作为国内领先的云服务提供商,其国际版和国内版备受关注。那么,腾讯云国际版和国内版账户是否通用?它们究竟一样吗?背后又…...

OrCAD X Capture CIS设计小诀窍系列第二季--03.如何在Capture中输出带有目录和元器件信息的PDF
背景介绍:我们在进行原理图设计时,经常需要输出PDF来查看或评审,但通过”Print”功能导出的PDF较为简单,只能查看设计视图;而通过使用Ghostscript软件可以输出带有目录和元器件信息的PDF,让设计师可以直接在…...

汽车的安全性能测试:试验台铁地板的重要性
汽车的安全性能测试是非常重要的,其中试验台铁地板的设计和材料选择起着至关重要的作用。试验台铁地板是指在进行汽车碰撞、侧翻等试验时,用于支撑汽车底部和提供稳定支撑的重要部件。 在进行汽车碰撞试验时,试验台铁地板的设计和材料需要具…...
Lua和JS的垃圾回收机制
Lua 和 JavaScript 都采用了 自动垃圾回收机制(GC) 来管理内存,开发者无需手动释放内存,但它们的 实现机制和行为策略不同。下面我们从原理、策略、优缺点等方面来详细对比: 🔶 1. 基本原理对比 特性LuaJa…...

实践指南:从零开始搭建RAG驱动的智能问答系统
LLM 赋能的最强大的应用之一是复杂的问答 (Q&A) 聊天机器人。这些是可以回答关于特定来源信息问题的应用程序。这些应用程序使用一种称为检索增强生成的技术,或 RAG。本文将展示如何基于 LangChain 构建一个简单的基于非结构化数据文本数据源的问答应用程序。 温…...

边缘计算服务器
边缘计算服务器的核心要点解析,综合技术架构、应用场景与部署方案: 一、核心定义与技术特性 本质定位 部署在网络边缘侧的专用计算设备(如工厂车间、智慧路灯等),直接处理终端设备(传感器、摄像头等…...
矩阵的偏导数
设 X ( x i j ) m n X (x_{ij})_{m \times n} X(xij)mn,函数 f ( X ) f ( x 11 , x 12 , … , x 1 n , x 21 , … , x m n ) f(X) f(x_{11}, x_{12}, \ldots, x_{1n}, x_{21}, \ldots, x_{mn}) f(X)f(x11,x12,…,x1n,x21,…,xmn) 是一个 m n…...

第R9周:阿尔茨海默病诊断(优化特征选择版)
文章目录 1. 导入数据2. 数据处理2.1 患病占比2.2 相关性分析2.3 年龄与患病探究 3. 特征选择4. 构建数据集4.1 数据集划分与标准化4.2 构建加载 5. 构建模型6. 模型训练6.1 构建训练函数6.2 构建测试函数6.3 设置超参数 7. 模型训练8. 模型评估8.1 结果图 8.2 混淆矩阵9. 总结…...

电动螺丝刀-多实体拆图建模案例
多实体建模要注意下面两点: 多实体建模的合并结果一定要谨慎在实际工作中多实体建模是一个非常好的思路,先做产品的整体设计,再将个体零件导出去做局部细节设计 电动螺丝刀模型动图展示 爆炸视图动图展示 案例素材点击此处获取 建模步骤 1. …...

当丰收季遇上超导磁测量:粮食产业的科技新征程
麦浪藏光阴,心田种丰年!又到了一年中最令人心潮澎湃的粮食丰收季。金色的麦浪随风翻滚,沉甸甸的稻穗谦逊地低垂着,处处洋溢着丰收的喜悦。粮食产业,无疑是国家发展的根基与命脉,是民生稳定的压舱石。在现代…...

电子电气架构 --- 什么是功能架构?
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 做到欲望极简,了解自己的真实欲望,不受外在潮流的影响,不盲从,不跟风。把自己的精力全部用在自己。一是去掉多余,凡事找规律,基础是诚信;二是…...
Android四大组件通讯指南:Kotlin版组件茶话会
某日,Android王国举办Kotlin主题派对。Activity穿着Jetpack Compose定制礼服,Service戴着协程手表,BroadcastReceiver拿着Flow喇叭,ContentProvider抱着Room数据库入场。它们正愁如何交流,Intent举着"邮差"牌…...
C++.OpenGL (11/64)材质(Materials)
材质(Materials) 真实感材质系统 #mermaid-svg-NjBjrmlcpHupHCFQ {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-NjBjrmlcpHupHCFQ .error-icon{fill:#552222;}#mermaid-svg-NjBjrmlcpHupHCFQ .error-text{fill:…...

AudioRelay 0.27.5 手机充当电脑音响
—————【下 载 地 址】——————— 【本章下载一】:https://pan.xunlei.com/s/VOS4MvfPxrnfS2Zu_YS4egykA1?pwdi2we# 【本章下载二】:https://pan.xunlei.com/s/VOS4MvfPxrnfS2Zu_YS4egykA1?pwdi2we# 【百款黑科技】:https://uc…...
会计 - 合并1- 业务、控制、合并日
一、业务 1.1 业务的定义以及构成要素 业务,是指企业内部某些生产经营活动或资产的组合,该组合一般具有投入、加工处理过程和产出能力,能够独立计算其成本费用或所产生的收入。 (1)投入,指原材料、人工、必要的生产技术等无形资产以及构成产出能力的机器设备等其他长期资…...
前端项目eslint配置选项详细解析
文章目录 1. 前言2、错误级别3、常用规则4、目前项目使用的.eslintrc.js 1. 前言 ESLint 是一个可配置的 JavaScript 代码检查工具,旨在帮助开发者发现并修复代码中的潜在问题,包括语法错误、逻辑错误以及风格不一致等问题。以下是其核心功能和特点…...

NVIDIA Dynamo:数据中心规模的分布式推理服务框架深度解析
NVIDIA Dynamo:数据中心规模的分布式推理服务框架深度解析 摘要 NVIDIA Dynamo是一个革命性的高吞吐量、低延迟推理框架,专为在多节点分布式环境中服务生成式AI和推理模型而设计。本文将深入分析Dynamo的架构设计、核心特性、代码实现以及实际应用示例&…...

第十三节:第四部分:集合框架:HashMap、LinkedHashMap、TreeMap
Map集合体系 HashMap集合的底层原理 HashMap集合底层是基于哈希表实现的 LinkedHashMap集合的底层原理 TreeMap集合的底层原理 代码: Student类 package com.itheima.day26_Map_impl;import java.util.Objects;public class Student implements Comparable<Stu…...

Spring AI之RAG入门
目录 1. 什么是RAG 2. RAG典型应用场景 3. RAG核心流程 3.1. 检索阶段 3.2. 生成阶段 4. 使用Spring AI实现RAG 4.1. 创建项目 4.2. 配置application.yml 4.3. 安装ElasticSearch和Kibana 4.3.1. 安装并启动ElasticSearch 4.3.2. 验证ElasticSearch是否启动成功 …...

应用案例 | 设备分布广, 现场维护难? 宏集Cogent DataHub助力分布式锅炉远程运维, 让现场变“透明”
在日本,能源利用与环保问题再次成为社会关注的焦点。越来越多的工业用户开始寻求更高效、可持续的方式来运营设备、管理能源。而作为一家专注于节能与自动化系统集成的企业,日本大阪的TESS工程公司给出了一个值得借鉴的答案。 01 锅炉远程监控难题如何破…...
C#中的密封类与静态类:特性、区别与应用实例
深入解析两类特殊类的设计哲学与实战应用 在面向对象编程领域中,C#提供了多种特殊的类类型以满足不同设计需求。其中密封类(sealed class)和静态类(static class)是最常用的两种特殊类类型。本文将从设计理念、应用场…...

LINUX 66 FTP 2 ;FTP被动模式;FTP客户服务系统
19. 在vim中将所有 abc 替换为 def,在底行模式下执行©?D A、s/abc/def B、s/abc/def/g C、%s/abc/def D、%s/abc/def/g FTP连接 用户名应该填什么 [rootcode ~]# grep -v ^# /etc/vsftpd/vsftpd.conf anonymous_enableNO local_enab…...

网心云 OEC/OECT 笔记(2) 运行RKNN程序
目录 网心云 OEC/OECT 笔记(1) 拆机刷入Armbian固件网心云 OEC/OECT 笔记(2) 运行RKNN程序 RKNN OEC/OEC-Turbo 使用的芯片是 RK3566/RK3568, 这个系列是内建神经网络处理器 NPU 的, 利用 RKNN 可以部署运行 AI 模型利用 NPU 硬件加速模型推理. 要使用 NPU, 首先需要在电脑使…...
vue-21 (使用 Vuex 模块和异步操作构建复杂应用)
实践练习:使用 Vuex 模块和异步操作构建复杂应用 Vuex 模块提供了一种结构化的方式来组织你的应用程序状态,特别是当应用程序变得复杂时。命名空间模块通过防止命名冲突和提高代码可维护性来增强这种组织。异步操作对于处理从 API 获取数据等操作至关重要,这些操作在现代 W…...