【JVM原理详解】06-类加载器与双亲委派模型
类加载器与双亲委派模型上一篇我们梳理了类加载的完整生命周期其中的加载阶段有一个核心动作——通过类的全限定名获取定义此类的二进制字节流。这个动作由谁来完成答案就是类加载器ClassLoader。类加载器是JVM类加载机制的核心组件它不仅负责查找和加载字节流还决定了类的可见性和隔离性。本篇将深入讲解JVM中各类加载器的层次结构、双亲委派模型的设计原理以及如何自定义类加载器。类加载器的种类JVM默认提供了几种类加载器它们各自负责不同范围的类加载工作形成了一个层次结构。从JDK 8到JDK 11/17加载器的命名和职责有一些变化但核心架构一致。Bootstrap ClassLoader引导类加载器Bootstrap ClassLoader由C实现在HotSpot JVM中是JVM的一部分不是Java类。它负责加载JVM运行所需的核心类库即JAVA_HOME/lib目录下或者被-Xbootclasspath参数指定的路径中的类库。// 获取String类的类加载器System.out.println(String.class.getClassLoader());// 输出: null// null表示Bootstrap ClassLoader因为它不是Java对象注意由于Bootstrap ClassLoader由C实现Java层面无法直接获取到它的引用getClassLoader()返回null就代表该类由Bootstrap ClassLoader加载。Extension ClassLoader / Platform ClassLoader扩展类加载器在JDK 8中称为Extension ClassLoader扩展类加载器由sun.misc.Launcher$ExtClassLoader实现负责加载JAVA_HOME/lib/ext目录下或由java.ext.dirs系统变量指定路径中的类库。从JDK 9开始由于模块化系统Jigsaw的引入扩展类加载器被改名为Platform ClassLoader平台类加载器实现类变为jdk.internal.loader.ClassLoaders$PlatformClassLoader。它负责加载平台模块中的类。// JDK 8// sun.misc.Launcher$ExtClassLoaderxxxSystem.out.println(sun.misc.Launcher.getLauncher().getClassLoader().getParent());// JDK 11// jdk.internal.loader.ClassLoaders$PlatformClassLoaderxxxClassLoaderplatformLoaderClassLoader.getPlatformClassLoader();System.out.println(platformLoader);Application ClassLoader应用程序类加载器Application ClassLoader也称为System ClassLoader系统类加载器由sun.misc.Launcher$AppClassLoaderJDK 8或jdk.internal.loader.ClassLoaders$AppClassLoaderJDK 9实现。它负责加载用户类路径-classpath或-cp上的类库也就是我们自己编写的应用类和第三方依赖。// 获取系统类加载器即Application ClassLoaderClassLoaderappLoaderClassLoader.getSystemClassLoader();System.out.println(appLoader);// 输出类似: jdk.internal.loader.ClassLoaders$AppClassLoaderxxx自定义类加载器除了以上三种类加载器开发者还可以通过继承java.lang.ClassLoader类来实现自定义类加载器。自定义类加载器常见的应用场景包括从网络、加密文件、数据库等非标准来源加载类实现类的隔离如Web容器中不同应用加载同名类的不同版本实现热部署和热替换类加载器层次结构┌─────────────────────────┐ │ Bootstrap ClassLoader │ (C实现, 加载核心类库) │ java.lang.String 等 │ └────────────┬────────────┘ │ 父加载器 ┌────────────▼────────────┐ │ Extension/Platform CL │ (Java实现, 加载扩展模块) │ javax.*, javafx.* 等 │ └────────────┬────────────┘ │ 父加载器 ┌────────────▼────────────┐ │ Application ClassLoader │ (加载classpath) │ 用户类 第三方依赖 │ └────────────┬────────────┘ │ 父加载器 ┌────────────▼────────────┐ │ 自定义 ClassLoader │ (按需加载) │ 网络/加密/动态生成等 │ └─────────────────────────┘需要特别指出这里的父子关系不是继承关系而是组合关系通过parent字段引用。子加载器持有一个指向父加载器的引用形成委托链。双亲委派模型工作流程双亲委派模型Parent Delegation Model的工作流程非常简洁当一个类加载器收到了类加载请求时它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此因此所有的加载请求最终都应该传送到顶层的Bootstrap ClassLoader中。只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有所需的类时子加载器才会尝试自己去加载。用伪代码描述这个逻辑protectedClass?loadClass(Stringname,booleanresolve){// 1. 检查类是否已被加载Class?cfindLoadedClass(name);if(cnull){try{// 2. 委托给父加载器加载if(parent!null){cparent.loadClass(name,false);}else{// 3. 父加载器为null说明到顶了使用Bootstrap加载器cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父加载器无法加载}if(cnull){// 4. 父加载器无法加载自己尝试加载cfindClass(name);}}returnc;}这段逻辑实际就是java.lang.ClassLoader的loadClass方法的核心实现。一个完整的加载流程示例假设我们编写了一个com.example.HelloWorld类放在classpath下。当JVM第一次需要使用这个类时加载流程如下1. Application ClassLoader 收到加载 com.example.HelloWorld 的请求 2. Application ClassLoader 委托给 Extension ClassLoader 3. Extension ClassLoader 委托给 Bootstrap ClassLoader 4. Bootstrap ClassLoader 在 JAVA_HOME/lib 中查找 → 未找到 → 返回null 5. Extension ClassLoader 在 JAVA_HOME/lib/ext 中查找 → 未找到 → 返回null 6. Application ClassLoader 在 classpath 中查找 → 找到HelloWorld.class → 加载成功设计意图双亲委派模型的设计有两个核心目的1. 安全性假设没有双亲委派模型用户可以自定义一个名为java.lang.String的类由Application ClassLoader先加载那么核心API就被篡改了。有了双亲委派模型java.lang.String的加载请求会一路委派到Bootstrap ClassLoader由它从核心类库中加载真正的String类用户的假String永远不会被加载。2. 避免重复加载同一个类被不同类加载器加载会产生不同的Class对象。双亲委派模型保证了Java核心类库只会被Bootstrap ClassLoader加载一次避免了重复加载和类型不一致的问题。Class对象的唯一性这里有一个非常重要的概念对于任何一个类都需要由加载它的类加载器和这个类本身一同确立其在JVM中的唯一性。换句话说比较两个类是否相等只有在这两个类是由同一个类加载器加载的前提下才有意义。publicclassClassIdentityDemo{publicstaticvoidmain(String[]args)throwsException{// 自定义两个不同的类加载器加载同一个class文件ClassLoaderloader1newMyClassLoader(/path/to/classes/);ClassLoaderloader2newMyClassLoader(/path/to/classes/);Class?class1loader1.loadClass(com.example.HelloWorld);Class?class2loader2.loadClass(com.example.HelloWorld);System.out.println(class1class2);// 输出: false// 虽然是同一个class文件但由不同类加载器加载产生不同的Class对象Objectobj1class1.newInstance();Objectobj2class2.newInstance();System.out.println(obj1instanceofcom.example.HelloWorld);// 输出: false —— 这就是类型隔离的本质// obj1的真实类型是 loader1加载的HelloWorld// instanceof检查的是 Application ClassLoader加载的HelloWorld// 两者不是同一个类}}这个特性是类加载器隔离的基础。Tomcat利用它实现Web应用之间的类隔离OSGi利用它实现模块化——这些内容我们将在后续文章中展开。自定义类加载器自定义类加载器通常只需要继承ClassLoader并重写findClass方法。如果不想破坏双亲委派模型不要重写loadClass只重写findClass。下面是一个完整的自定义类加载器示例它从指定磁盘路径加载class文件// 适用: JDK 8/11/17publicclassDiskClassLoaderextendsClassLoader{privateStringclassPath;// class文件所在目录publicDiskClassLoader(StringclassPath){// 不指定parent默认parent为当前线程的上下文类加载器// 通常就是Application ClassLoaderthis.classPathclassPath;}publicDiskClassLoader(StringclassPath,ClassLoaderparent){super(parent);this.classPathclassPath;}OverrideprotectedClass?findClass(Stringname)throwsClassNotFoundException{byte[]classDataloadClassData(name);if(classDatanull){thrownewClassNotFoundException(无法找到类: name);}// defineClass将字节数组转换为Class对象// 这是JVM提供的原生方法负责验证、解析等底层工作returndefineClass(name,classData,0,classData.length);}privatebyte[]loadClassData(Stringname){// 将类名转换为文件路径: com.example.HelloWorld → com/example/HelloWorld.classStringpathclassPath/name.replace(.,/).class;try{PathfilePaths.get(path);if(!Files.exists(file)){returnnull;// 返回null表示自己无法加载}returnFiles.readAllBytes(file);}catch(IOExceptione){returnnull;}}}使用自定义类加载器publicclassDiskClassLoaderDemo{publicstaticvoidmain(String[]args)throwsException{DiskClassLoaderloadernewDiskClassLoader(D:/custom-classes);// 加载com.example.HelloWorldClass?clazzloader.loadClass(com.example.HelloWorld);// 通过反射创建实例并调用方法Objectinstanceclazz.getDeclaredConstructor().newInstance();Methodmethodclazz.getMethod(sayHello);method.invoke(instance);// 查看加载器System.out.println(clazz.getClassLoader());// 输出: DiskClassLoaderxxx如果是自定义加载器加载的话// 如果D:/custom-classes/com/example/HelloWorld.class不存在// 由于双亲委派最终会由Application ClassLoader从classpath中加载// 输出将是: jdk.internal.loader.ClassLoaders$AppClassLoaderxxx}}自定义类加载器何时打破双亲委派如果自定义类加载器只重写了findClass它会遵循双亲委派模型——先委托父加载器加载父加载器加载不了才自己加载。但在某些场景下我们需要打破双亲委派比如隔离需求Tomcat的WebApp类加载器优先自己加载而不是先委托父加载器热部署需要重新加载已修改的类而父加载器中缓存的旧版本无法更新SPI机制父加载器需要访问子加载器的类下一篇详细讲解打破双亲委派的方法是重写loadClass方法publicclassCustomClassLoaderextendsClassLoader{privateStringclassPath;publicCustomClassLoader(StringclassPath,ClassLoaderparent){super(parent);this.classPathclassPath;}OverrideprotectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{// 1. 检查是否已加载Class?cfindLoadedClass(name);if(cnull){// 2. 对自己负责的类优先自己加载打破双亲委派if(name.startsWith(com.myapp.)){try{cfindClass(name);}catch(ClassNotFoundExceptione){// 自己加载失败再走父加载器}}// 3. 其他类走正常的双亲委派if(cnull){if(getParent()!null){cgetParent().loadClass(name);}else{thrownewClassNotFoundException(name);}}}if(resolve){resolveClass(c);}returnc;}OverrideprotectedClass?findClass(Stringname)throwsClassNotFoundException{byte[]dataloadClassData(name);if(datanull)thrownewClassNotFoundException(name);returndefineClass(name,data,0,data.length);}privatebyte[]loadClassData(Stringname){StringpathclassPath/name.replace(.,/).class;try{returnFiles.readAllBytes(Paths.get(path));}catch(IOExceptione){returnnull;}}}实践要点优先使用findClass而非loadClass除非你有明确的隔离需求否则始终重写findClass保持双亲委派。随意打破双亲委派可能导致核心API被篡改、类加载混乱等严重问题。defineClass的不可逆性一旦defineClass成功这个类的Class对象就会被锁定在该类加载器中。即使后续字节码文件被修改同一个类加载器再次defineClass同名类会抛出LinkageError。热部署必须创建新的类加载器实例。避免类加载器内存泄漏类加载器引用了大量Class对象如果类加载器无法被GC回收如线程的ContextClassLoader引用会导致Metaspace持续增长。在使用线程池时尤其需要注意。JDK 9模块化对类加载器的影响JDK 9引入模块化后类加载器之间增加了模块层的概念。java.lang.ClassLoader增加了getNamedModule()方法BootClassLoader的加载范围通过模块定义而非路径。但双亲委派模型的基本架构没有改变。-Xbootclasspath/a的使用如果需要将自定义的类加入引导类加载器的加载范围如覆盖或补充核心类可以使用-Xbootclasspath/a:/path/to/your.jar/a表示append追加到末尾。java-Xbootclasspath/a:./supplement.jar-jaryour-app.jar小结JVM类加载器分为Bootstrap ClassLoaderC实现加载核心库、Extension/Platform ClassLoader加载扩展模块、Application ClassLoader加载classpath以及自定义类加载器。双亲委派模型要求类加载请求先委派给父加载器父加载器无法加载时子加载器才自行加载。其设计目的是保证安全性和避免重复加载。类的唯一性由类加载器类名共同确定。同一个class文件被不同类加载器加载后产生不同的Class对象instanceof检查会返回false。自定义类加载器继承ClassLoader并重写findClass可保持双亲委派重写loadClass则可打破双亲委派。下一篇我们将探讨双亲委派模型的局限性深入SPI机制和线程上下文类加载器如何打破这一模型。

相关新闻

SolidWorks_焊件设计9_子焊件管理

SolidWorks_焊件设计9_子焊件管理

子焊件管理 摘要 在大型机械结构、钢结构框架或复杂焊接件的设计过程中,将整体焊件合理拆分为子焊件是一种至关重要的工程实践。本文深入探讨了子焊件管理的核心理念、技术实现方法及其在工程出图中的实际应用。通过详细的理论分析和完整的代码示例,展示…

2026/7/22 0:58:51 阅读更多 →
SolidWorks_焊件设计8_多草图骨架布局

SolidWorks_焊件设计8_多草图骨架布局

多草图骨架布局:利用多个2D/3D草图构建复杂空间框架结构 摘要 在计算机图形学、CAD辅助设计、游戏开发以及建筑信息模型(BIM)等领域,构建复杂的空间框架结构一直是一个核心挑战。传统的多边形建模或参数化建模往往需要大量的手动调…

2026/7/22 0:58:51 阅读更多 →
【独家】基于217个真实AI项目复盘的场景适配决策树(含GPU成本/延迟/准确率三维度阈值标定)

【独家】基于217个真实AI项目复盘的场景适配决策树(含GPU成本/延迟/准确率三维度阈值标定)

更多请点击: https://codechina.net 第一章:AI模型适用场景分析 AI模型并非万能工具,其价值高度依赖于具体业务需求与数据特性。选择合适模型的关键在于理解任务类型、数据规模、实时性要求及可解释性约束。脱离场景空谈“大模型”或“小模型…

2026/7/22 0:57:50 阅读更多 →

最新新闻

海外红队面试经验分享

海外红队面试经验分享

互联网公司A 老牌头部互联网公司,Top 10 职位:高级红队操作员 (Senior Red Team Operator) 流程 简历筛选 招聘人员电话面试: 背景、沟通能力、项目经验概述、对他们公司技术栈的初解 在线评估 : 基础编码/脚本能力测试 核心安全概念问答 (网络、操作系统、加密、认证) 重…

2026/7/22 3:42:09 阅读更多 →
总结 7.21

总结 7.21

今天学了线代的行列式和矩阵。行列式学了插型,剪头型还有ab型,ab型的计算公式。还有使用升阶法求行列式,把它化成剪型。还有范德蒙德,注意范德蒙德的阶数和为最高次数减一,然后递乘就行了,然后是算行列式的…

2026/7/22 3:42:09 阅读更多 →
PDF 批量提取指定内容到 Excel:按字段整理多个 PDF 的方法

PDF 批量提取指定内容到 Excel:按字段整理多个 PDF 的方法

手里有几十份甚至更多 PDF,要从每份里取出姓名、编号、日期、金额这类固定信息,再汇总成 Excel,最容易卡在两件事上:每页内容很多,最后要交的却只是几列数据;而且复制出来的文本还要反复贴进表格。 这类任…

2026/7/22 3:42:09 阅读更多 →
长文本AI处理技术:自建方案实现与算力优化指南

长文本AI处理技术:自建方案实现与算力优化指南

最近不少开发者朋友在尝试接入 Kimi 智能助手 API 时发现,官方突然暂停了 C 端会员的销售服务。作为国内领先的长文本处理 AI,Kimi 凭借强大的上下文理解能力迅速成为开发者进行文档分析、代码解读的得力助手。这次服务调整背后反映的正是当前 AI 大模型…

2026/7/22 3:42:09 阅读更多 →
Claude Code与Agent技术:模块化Skill架构与日抛式软件开发

Claude Code与Agent技术:模块化Skill架构与日抛式软件开发

1. Claude Code与Agent创作新范式解析MuleRun创始人陈宇森在访谈中提出的"日抛式软件"概念,正在通过Claude Code的Agent技术变为现实。这种新型开发模式彻底改变了传统软件的构建方式,让每个功能模块都能像乐高积木一样自由组合。1.1 模块化Sk…

2026/7/22 3:42:09 阅读更多 →
开源AI模型许可合规:技术原理、部署方案与风险应对

开源AI模型许可合规:技术原理、部署方案与风险应对

开源模型正面临前所未有的许可合规挑战。近期,美国政策变化可能对全球开源AI生态产生重大影响,特别是涉及商业应用和跨国分发的场景。对于依赖开源模型进行开发和研究的技术团队来说,理解当前的许可困境并提前制定应对策略至关重要。开源模型…

2026/7/22 3:41:08 阅读更多 →

日新闻

TI DSP系统配置模块SYSCFG详解:中断机制与主设备优先级配置实战

TI DSP系统配置模块SYSCFG详解:中断机制与主设备优先级配置实战

1. 项目概述与SYSCFG模块的核心价值在嵌入式系统,尤其是像TI C6000系列这样的高性能DSP开发中,我们常常会与芯片手册里那些密密麻麻的寄存器打交道。很多开发者可能更关注算法实现、内存优化或者外设驱动,但对于一个稳定、高效的系统而言&…

2026/7/22 0:00:26 阅读更多 →
微信Server酱:高到达率的应急通知方案实践

微信Server酱:高到达率的应急通知方案实践

1. 为什么我们需要"最次"的通知方案? 在数字化协作环境中,消息通知系统的重要性不言而喻明。但现实情况是,企业级通知方案往往需要复杂的API对接(如企业微信、钉钉、飞书),个人开发者的小项目又经…

2026/7/22 0:00:26 阅读更多 →
甲方要的“简洁“PPT,到底是简洁还是省事?

甲方要的“简洁“PPT,到底是简洁还是省事?

甲方说"简洁一点",乙方听到的是"少做几页"。甲方说"不要太复杂",乙方理解成"别放图表了"。结果交过去,甲方说"我说的简洁不是这个意思"。"简洁"这个词在PPT语境里,是…

2026/7/22 0:00:26 阅读更多 →

周新闻

Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/21 8:48:31 阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/21 5:34:47 阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/21 8:25:39 阅读更多 →

月新闻