JVM类加载机制

JDK17 lib目录结构

bash 复制代码
jdk-17/
├── bin/                          ← 可执行命令(java, javac, jimage, jlink 等)
│
├── conf/                         ← 配置文件
│
├── include/                      ← JNI 开发头文件
│
├── jmods/                        ← ⭐ 模块完整包(.jmod),用于 jlink 构建自定义运行时
│   ├── java.base.jmod
│   ├── java.compiler.jmod
│   ├── java.desktop.jmod
│   ├── jdk.zipfs.jmod
│   └── ...(共约 70+ 个模块)
│
├── legal/                        ← 各模块的许可证文件
│
├── lib/                          ← ⭐ 运行时核心库
│   ├── classlist                 ← 启动时需要预加载的类列表
│   ├── jrt-fs.jar               ← IDE 用来访问 jrt:/ 文件系统的工具
│   ├── jvm.cfg                  ← JVM 配置文件
│   ├── modules                  ← ⭐⭐⭐ 核心:运行时模块镜像(jimage 二进制文件,约 100~150MB),存储所有标准模块的类文件和资源
│   ├── security/                ← 安全策略、证书库
│   ├── server/                  ← Server VM 的 libjvm.so/dll
│   └── ...                      ← 其他动态库
│
└── release                       ← JDK 版本信息
java 复制代码
                                                                                                               
  ┌────────────────────┬────────────────────────────┐        
  │        文件                     作用                                                                       
  ├────────────────────┼────────────────────────────┤                                                           
  │ libattach.dylib    │ Attach API(动态加载进程)                                                             
  ├────────────────────┼────────────────────────────┤
  │ libawt.dylib       │ AWT 图形界面本地实现                                                                   
  ├────────────────────┼────────────────────────────┤                                                           
  │ libdt_socket.dylib │ JDWP 调试传输(socket)     
  ├────────────────────┼────────────────────────────┤
  │ libextnet.dylib    │ 网络扩展                    
  ├────────────────────┼────────────────────────────┤
  │ libnet.dylib       │ 网络 API(InetAddress 等)  
  ├────────────────────┼────────────────────────────┤
  │ libnio.dylib       │ NIO 本地实现                
  ├────────────────────┼────────────────────────────┤
  │ libzip.dylib       │ ZIP 文件处理                
  └────────────────────┴────────────────────────────┘

  用途:
  - JDK 的 Java 代码需要调用操作系统功能时,通过 JNI 调用这些 .dylib
  - 比如网络连接、文件压缩、字体渲染、图形绘制等

## 关键目录详解

 1. lib/modules --- 运行时核心
   这是一个二进制文件,不是目录
   格式为 JIMAGE,JVM 启动时直接内存映射
   包含所有标准模块的类文件、资源文件
   运行时通过 jrt:/ 协议访问

 2. jmods/ --- 模块完整包
   每个 .jmod 文件是一个模块的完整包
   包含类文件、原生库(.so/.dll)、配置文件、法律文件
   用于 jlink 工具构建自定义运行时镜像
   JVM 运行时不直接加载 jmods,而是加载优化后的 lib/modules

JDK17中JVM类加载器

  • 引导型类加载器(Bootstrap ClassLoader):
java 复制代码
 ┌──────────┬────────────────────────────────────────────────────                                      
 │   项目                               说明                       
 ├──────────┼──────────────────────────────────────────────────── 
 │ 实现         C++ 实现(非 Java 类)                                
 ├──────────┼──────────────────────────────────────────────────── 
 │ 父加载器      无                                                    
 ├──────────┼──────────────────────────────────────────────────── 
 │ 
   加载范围      lib/modules(jimage 二进制文件),
                JDK 核心类库(java.base、 java.lang、 java.util、java.logging, java.xml, java.sql 等**核心**模块) 
 ├──────────┼──────────────────────────────────────────────────── 
 │ 路径         $JAVA_HOME/lib 和 $JAVA_HOME/jmods/                  
 ├──────────┼──────────────────────────────────────────────────── 
 │ 加载方式      JVM 启动时预加载                                       
 └──────────┴──────────────────────────────────────────────────── 
  特点:
  - 是所有类加载器的"祖先"
  - 加载 JVM 运行时必需的类
  - 在 Java 代码中表示为 null(不是 Java 对象)
  • 平台类加载器(Platform ClassLoader)
java 复制代码
 ┌──────────┬──────────────────────────────────────────────────────────────────────┐
 │   项目                                  说明                                 
 ├──────────┼──────────────────────────────────────────────────────────────────────┤
 │ 实现        Java 实现,类名 jdk.internal.loader.ClassLoaders$PlatformClassLoader 
 ├──────────┼──────────────────────────────────────────────────────────────────────┤
 │ 父加载器     引导类加载器                                                         
 ├──────────┼──────────────────────────────────────────────────────────────────────┤
 │ 加载范围     **非核心**平台模块类(java.compiler, java.net.http, jdk.zipfs, jdk.jfr 等      
 │              (JDK 8 中放在 lib/ext 目录)  等)          
 ├──────────┼──────────────────────────────────────────────────────────────────────┤
 │ 路径        $JAVA_HOME/jmods(非 java.base 模块)                                  
 ├──────────┼──────────────────────────────────────────────────────────────────────┤
 │ 替代        JDK 8 的扩展类加载器(ExtClassLoader + lib/ext/ 目录)                                
 └──────────┴──────────────────────────────────────────────────────────────────────┘
  特点:
  - 用于加载 JDK 平台扩展功能
  - 通过 --add-modules 添加的模块由它加载
  • 应用程序类加载器(Application ClassLoader):加载classpath路径下的类,主要就是自己写的类
java 复制代码
 ┌──────────┬─────────────────────────────────────────────────────────────────┐
 │ 项目                                  说明                                
 ├──────────┼─────────────────────────────────────────────────────────────────┤
 │ 实现        Java 实现,类名 jdk.internal.loader.ClassLoaders$AppClassLoader  
 ├──────────┼─────────────────────────────────────────────────────────────────┤
 │ 父加载器     平台类加载器                                                     
 ├──────────┼─────────────────────────────────────────────────────────────────┤
 │ 加载范围     -classpath / -cp / --module-path 指定的路径                                
 ├──────────┼─────────────────────────────────────────────────────────────────┤
 │ 对应输出目录   用户项目编译输出目录(target/classes/)                    
 │              第三方依赖 jar(target/dependency/*.jar)               
 │              用户自定义模块路径                                  
 └──────────┴─────────────────────────────────────────────────────────────────┘
特点:
  - 默认的类加载器(ClassLoader.getSystemClassLoader())
  - 加载项目代码和第三方依赖(如 jakarta.persistence-api)
  • 自定义类加载器:用于加载 动态生成的类、网络加载的类、热部署的类等
java 复制代码
用户通过继承 ClassLoader 实现,常用于:
  - Tomcat 的 WebAppClassLoader(实现应用隔离):
        不同 Web 应用依赖不同版本的 Spring/Log4j,如果共用同一个 ClassLoader 会冲突;每个 Web 应用有自己的 WebAppClassLoader,优先加载自己的 WEB-INF/lib 下的 jar,找不到再委派给父加载器。
  - Spring Boot 的 DevRestartClassLoader(热重启):不停机更新代码

JVM类加载机制

作用 :规定了类加载的委派顺序和规则

核心规则:当一个类加载器收到加载请求时,它首先不会自己去加载,而是把请求委派给父类去加载,只有当父类加载器无法加载时,子类加载器才会去尝试自己加载。

流程图

bash 复制代码
自定义 ClassLoader
    │
    ▼ 委派
Application ClassLoader
    │
    ▼ 委派
Platform ClassLoader
    │
    ▼ 委派
Bootstrap ClassLoader
    │
    ▼ 找不到?返回
Application ClassLoader ← 自己尝试加载
    │
    ▼ 找不到?返回
自定义 ClassLoader ← 自己尝试加载

这里是从【自定义 ClassLoader】一直逐层向上委派到最顶层的父类加载器BootStapt ClassLoader,关键点: 每一层都会重复"先查缓存 → 委派给父 → 父找不到自己找"的完整逻辑,所以最终一定会问到 Bootstrap。

为什么要设计双亲委派机制

  1. 避免类的重复加载
bash 复制代码
场景:用户自定义了一个 java.lang.String

如果没有双亲委派:
    Application ClassLoader 加载了自定义的 String
    Bootstrap ClassLoader 也加载了 JDK 的 String
    内存中存在两个 String 类 → 类型转换异常、混乱

有双亲委派:
    加载请求先委派到 Bootstrap
    Bootstrap 在 rt.jar 中找到 String,直接加载
    子加载器不再加载
    内存中只有一个 String 类
  1. 保证核心类库的安全性
bash 复制代码
场景:黑客自定义一个恶意的 java.lang.System

如果没有双亲委派:
    Application ClassLoader 加载恶意 System
    替换掉标准输入输出、获取系统权限

有双亲委派:
    请求先到 Bootstrap
    Bootstrap 从 lib/modules 加载官方的 System
    恶意 System 永远无法被加载(除非用反射/Unsafe 绕过)

双亲委派机制的破坏:有些场景需要破坏双亲委派机制

场景 为什么打破 怎么打破
Tomcat WebApp 不同 Web 应用需要不同版本的 Spring 自定义 WebAppClassLoader,先自己加载,找不到再委派
SPI 机制(JDBC) java.sql.Driver 在 Bootstrap,com.mysql.Driver 在 App 使用 Thread.currentThread().getContextClassLoader()
OSGi 模块间需要版本隔离 每个 Bundle 有自己的 ClassLoader,复杂的网状委派
热部署 需要重新加载修改后的类 每次 new 一个新的 ClassLoader,丢弃旧的

tomcat打破双亲委派的示例(Tomcat底层使用的不是双亲委派机制)

正常双亲委派

bash 复制代码
加载请求:org.springframework.context.ApplicationContext

Bootstrap:lib/modules 中没有 Spring → 找不到
    ↓ 返回
Platform:lib/modules 中没有 Spring → 找不到
    ↓ 返回
Application:classpath 中有 spring-context-5.3.jar → 找到,加载

结果: Spring 由 Application ClassLoader 加载。

Tomcat 打破双亲委派(先自己加载)

bash 复制代码
WebApp A 的 WebAppClassLoader 加载:org.springframework.context.ApplicationContext

WebApp A:WEB-INF/lib/spring-context-4.3.jar 中有 → 直接加载!
    ↑
    注意:不会向上问!父加载器(Common)中的 Spring 5.x 被忽略

结果: WebApp A 用 Spring 4.3,WebApp B 用 Spring 5.2,互不干扰。