news 2026/4/30 22:25:23

JVM 类加载机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JVM 类加载机制

从源码到流程解析 JVM 类加载机制

在 Java 里,.java文件会先被编译成.class字节码文件,但 JVM 不能直接执行磁盘上的字节码。类加载机制要做的事,就是把字节码从外部世界带入 JVM:读取字节流、校验安全性、转换成运行时数据结构,并在需要时完成静态变量和静态代码块的初始化。

如果用一句话概括:类加载机制负责把.class文件变成 JVM 可以使用的Class对象和运行时类型信息。

这里要注意,“类加载”有广义和狭义两种说法:

  • 狭义加载:只指 Loading 阶段,也就是读取字节码并生成类的运行时结构。
  • 广义类加载:通常指加载、链接、初始化这一整套流程。

本文从宏观流程讲到ClassLoader源码调用链,再讲双亲委派以及如何打破双亲委派。

一、类加载的整体流程

JVM 中一个类从被读取到可以使用,通常会经历下面几个阶段:

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

其中,验证、准备、解析统称为链接

加载 -> 链接(验证、准备、解析) -> 初始化

1. 加载:把字节码带进 JVM

加载阶段主要做三件事:

  1. 通过类的全限定名获取这个类的二进制字节流。
  2. 将字节流代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在堆中生成一个java.lang.Class对象,作为访问这个类元数据的入口。

也就是说,我们平时反射时拿到的Class<?> clazz,本质上就是 JVM 暴露给 Java 层的类型入口。

Class<?>clazz=User.class;

这里的clazz并不是类的全部数据本身,而是一个“镜像对象”。真正的类型元数据主要存放在方法区,JDK 8 之后具体实现通常是元空间。

2. 验证:确保字节码合法、安全

验证阶段是 JVM 的安全防线之一。它会检查字节码是否符合 JVM 规范,避免恶意或错误的字节码破坏虚拟机。

常见检查包括:

  • 文件格式校验:例如魔数0xCAFEBABE、版本号、常量池格式。
  • 元数据校验:类是否有父类、是否继承了final类、方法签名是否合法。
  • 字节码校验:操作数栈和局部变量表使用是否正确,跳转指令是否合法。
  • 符号引用校验:引用的类、字段、方法是否存在,访问权限是否满足。

3. 准备:给静态变量分配内存并设置默认值

准备阶段会为类变量,也就是static变量分配内存,并设置 JVM 默认初始值。

例如:

publicclassUser{publicstaticintcount=10;}

在准备阶段,count的值不是10,而是0。真正赋值为10要等到初始化阶段执行<clinit>方法。

但如果变量被static final修饰,并且是编译期常量,情况会不同:

publicstaticfinalintMAX=100;

这类常量在准备阶段就可能直接被赋值为100,因为它的值已经写入了ConstantValue属性。

4. 解析:把符号引用转换为直接引用

.class文件里保存的大多不是直接内存地址,而是符号引用。

例如一个类调用另一个类的方法,字节码里保存的可能是类似下面的信息:

com/example/UserService.login:()V

解析阶段会把这些符号引用转换为 JVM 可以直接使用的引用,比如方法表中的入口、字段偏移量、类元数据指针等。

解析不一定一次性全部完成,JVM 可以选择懒解析。也就是说,有些符号引用可能在真正使用时才解析。

5. 初始化:执行类构造器<clinit>

初始化阶段才真正执行 Java 代码层面的静态赋值和静态代码块。

publicclassUser{staticintcount=10;static{count=20;}}

编译器会把静态变量赋值语句和静态代码块按照源码顺序合并成类构造器<clinit>

初始化有几个关键特点:

  • 父类会先于子类初始化。
  • 一个类的<clinit>在多线程环境下会被 JVM 加锁保证只执行一次。
  • 接口初始化规则和类略有不同,接口不会因为子接口或实现类初始化而必然初始化。

常见触发初始化的场景包括:

  • new一个对象。
  • 读取或设置类的静态字段,编译期常量除外。
  • 调用类的静态方法。
  • 使用反射访问类。
  • 初始化子类时,父类需要先初始化。
  • JVM 启动时初始化包含main方法的主类。

二、从源码看类是怎么被加载的

Java 层类加载的核心入口是ClassLoader#loadClass。以 OpenJDK 8 的逻辑简化后,大致如下:

protectedClass<?>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){// 1. 先检查这个类是否已经被当前类加载器加载过Class<?>c=findLoadedClass(name);if(c==null){try{// 2. 优先交给父加载器加载if(parent!=null){c=parent.loadClass(name,false);}else{c=findBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父加载器找不到,继续让当前加载器尝试}// 3. 父加载器加载失败,当前加载器才调用 findClassif(c==null){c=findClass(name);}}// 4. 如果需要解析,则执行链接中的解析动作if(resolve){resolveClass(c);}returnc;}}

这段逻辑就是双亲委派机制的源码基础。

核心顺序是:

先查缓存 -> 委派父加载器 -> 父加载失败 -> 当前加载器 findClass -> 可选 resolve

loadClass、findClass、defineClass 的关系

这三个方法经常一起出现,但职责不一样:

方法主要职责
loadClass控制类加载的整体流程,默认实现包含双亲委派
findClass当前类加载器真正查找字节码的位置,通常由自定义加载器重写
defineClassbyte[]字节码交给 JVM,转换成Class<?>对象

如果只想自定义“类从哪里来”,一般重写findClass

例如从磁盘、网络、数据库或某个特殊目录读取字节码:

publicclassMyClassLoaderextendsClassLoader{@OverrideprotectedClass<?>findClass(Stringname)throwsClassNotFoundException{byte[]bytes=readClassBytes(name);returndefineClass(name,bytes,0,bytes.length);}privatebyte[]readClassBytes(Stringname){// 读取 .class 文件,转换为 byte[]thrownewUnsupportedOperationException();}}

真正让字节码进入 JVM 的方法是defineClass

从 Java 层看,它接收一段byte[],返回一个Class<?>

protectedfinalClass<?>defineClass(Stringname,byte[]b,intoff,intlen)

从 JVM 层看,defineClass会触发虚拟机进行类定义:校验字节码、创建内部类元数据,并生成 Java 层可见的Class镜像对象。

所以可以这样理解:

findClass 负责找到字节码 defineClass 负责把字节码定义成 JVM 里的类 loadClass 负责控制加载顺序和委派规则

三、双亲委派机制是什么

双亲委派机制指的是:一个类加载器收到类加载请求时,不会先自己加载,而是先把请求委派给父加载器。只有父加载器无法加载时,子加载器才会尝试自己加载。

在 Java 8 中,常见加载器层级如下:

Bootstrap ClassLoader ^ Extension ClassLoader ^ Application ClassLoader ^ Custom ClassLoader

对应职责如下:

加载器实现加载范围
Bootstrap ClassLoaderC/C++加载 JVM 核心类库,如 Java 8 中的rt.jar
Extension ClassLoaderJava加载JAVA_HOME/lib/ext下的扩展类
Application ClassLoaderJava加载应用classpath下的类
Custom ClassLoaderJava加载用户自定义来源的类

JDK 9 之后模块化机制引入,Extension ClassLoader的位置逐渐被Platform ClassLoader替代,但“父优先”的思想仍然存在。

为什么需要双亲委派

双亲委派主要解决三个问题。

第一,避免核心类被篡改

假设我们自己写一个java.lang.String,如果没有双亲委派,应用类加载器可能会优先加载这个伪造类,JVM 核心类型体系就会出问题。

有了双亲委派,java.lang.String会优先交给 Bootstrap ClassLoader 加载,用户自定义版本不会覆盖核心类。

第二,避免重复加载

同一个类如果被多个加载器随意加载,会导致 JVM 里出现多份类型定义。双亲委派让基础类尽可能由上层加载器统一加载,减少重复。

第三,保证类型体系稳定

在 JVM 中,判断两个类是否相等,不只看全限定类名,还要看定义它们的类加载器。

类的唯一性 = 全限定类名 + 定义类加载器

即使两个类的包名、类名完全相同,只要由不同类加载器加载,JVM 也会认为它们是两个不同的类。

四、如何打破双亲委派机制

双亲委派的关键逻辑在loadClass中:

先 parent.loadClass() 再 this.findClass()

所以,如果要打破双亲委派,核心就是:重写loadClass,改变父优先的加载顺序。

1. 遵循双亲委派:重写 findClass

推荐做法是只重写findClass

这样不会破坏loadClass中的父优先逻辑,只是告诉当前类加载器:当父加载器找不到时,你应该怎么读取字节码。

publicclassNormalClassLoaderextendsClassLoader{@OverrideprotectedClass<?>findClass(Stringname)throwsClassNotFoundException{byte[]bytes=readClassBytes(name);returndefineClass(name,bytes,0,bytes.length);}privatebyte[]readClassBytes(Stringname){thrownewUnsupportedOperationException();}}

调用顺序仍然是:

loadClass -> parent.loadClass -> findClass -> defineClass

2. 打破双亲委派:重写 loadClass

如果希望某些类由当前加载器优先加载,就需要重写loadClass

下面是一个简化的 child-first 类加载器:

publicclassChildFirstClassLoaderextendsClassLoader{@OverrideprotectedClass<?>loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){Class<?>c=findLoadedClass(name);if(c==null){// 核心类仍然交给父加载器,避免破坏 JDK 基础类型if(name.startsWith("java.")){c=super.loadClass(name,false);}else{try{// 先让当前类加载器自己加载c=findClass(name);}catch(ClassNotFoundExceptione){// 当前加载器找不到,再交给父加载器c=super.loadClass(name,false);}}}if(resolve){resolveClass(c);}returnc;}}@OverrideprotectedClass<?>findClass(Stringname)throwsClassNotFoundException{byte[]bytes=readClassBytes(name);returndefineClass(name,bytes,0,bytes.length);}privatebyte[]readClassBytes(Stringname){thrownewUnsupportedOperationException();}}

这时调用顺序变成:

loadClass -> findLoadedClass -> findClass -> defineClass -> 失败后再 super.loadClass

这就是典型的“子加载器优先”模型。

不过在实际工程中,不建议对所有类都 child-first。常见做法是按包名区分:

  • java.*javax.*jdk.*等核心类仍然父优先。
  • 应用自身的业务类可以子优先。
  • 公共 API 或容器 API 通常交给父加载器统一加载。

3. 典型应用场景

打破双亲委派不是为了“绕过安全”,而是为了解决隔离和扩展问题。

典型场景包括:

  • Tomcat:不同 Web 应用之间需要类隔离,同名业务类可以在不同应用中共存。
  • OSGi / 插件系统:每个模块或插件有独立的类加载空间。
  • 热部署:通过丢弃旧的类加载器,让旧类随加载器一起被卸载,再用新加载器加载新版本。
  • SPI 机制:例如 JDBC 驱动加载中会用到线程上下文类加载器,让父加载器可以反向访问子加载器可见的实现类。

五、打破双亲委派就一定不安全吗

不一定。

打破双亲委派只是改变了“类加载请求的查找顺序”,例如从父加载器优先变成当前加载器优先。它会削弱双亲委派带来的统一性和稳定性,但并不等于 JVM 的安全机制全部失效。

原因是:类加载器只负责把字节码交给 JVM,字节码能不能真正成为一个可执行的类,还要经过 JVM 后续的安全检查。

1. 类链接时 JVM 会检查该类是否有害

即使一个类是通过自定义类加载器加载的,它也必须经过链接阶段。

链接阶段中的验证会检查字节码是否合法,例如:

  • .class文件格式是否正确。
  • 常量池、字段、方法结构是否合法。
  • 操作数栈和局部变量表使用是否符合规范。
  • 类型转换、方法调用、字段访问是否安全。

所以,defineClass并不是“无条件把字节数组变成类”。它只是把字节码交给 JVM,JVM 仍然会做校验。如果字节码不符合规范,类加载会失败。

2. 沙箱安全机制仍然可以限制代码能力

双亲委派解决的是“类由谁加载”的问题,而沙箱安全机制解决的是“代码能做什么”的问题。

在传统 Java 安全模型中,可以通过SecurityManagerAccessController、权限策略等机制限制代码行为,例如:

  • 是否允许读写本地文件。
  • 是否允许访问网络。
  • 是否允许反射访问敏感成员。
  • 是否允许创建类加载器。

虽然新版 JDK 中SecurityManager已经逐步废弃,但这个点在面试中仍然可以表达为:类加载顺序被改变,不代表运行权限没有边界。

也就是说,自定义类加载器可以加载类,但加载进来的类仍然可能受到运行时权限控制。

3. 包名限制:不能随便伪造核心包

JVM 对某些包名有保护限制,最典型的是java.*

例如你自己写一个类:

packagejava.lang;publicclassString{}

即使你试图通过自定义类加载器调用defineClass去加载它,JVM 也不会允许普通用户代码随意定义这类受保护包名下的类,通常会抛出安全异常。

这说明:打破双亲委派并不等于可以任意替换 JDK 核心类。核心包名本身还有额外防护。

4. 密封类与签名验证

对于来自 JAR 包的类,JVM 和类库还会处理包密封、证书签名等信息。

如果一个包被声明为 sealed,那么同一个包下的类必须来自同一个代码源。否则即使类名合法,也可能因为来源不一致而加载失败。

签名验证也是类似的思路:如果 JAR 中的类带有签名信息,加载时需要保证签名一致,防止同一个包中混入来源不可信的类。

所以,从工程安全角度看,类加载不只看“谁先加载”,还要看:

  • 字节码是否合法。
  • 包名是否被允许。
  • 代码来源是否一致。
  • 签名和密封约束是否满足。

5. 真正的风险在哪里

打破双亲委派不是绝对不安全,但它确实会带来风险。

最大的问题是:同名类可能被不同类加载器加载,导致类型隔离、类型冲突或强转失败。

在 JVM 中,一个类的唯一性由两部分决定:

类的唯一性 = 全限定类名 + 定义类加载器

例如:

loader1 加载 com.example.User loader2 加载 com.example.User

这两个User在 JVM 看来不是同一个类,强转时可能抛出:

ClassCastException
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/30 22:12:26

用rand7()函数构造函数rand10()

题目要求&#xff1a;用一个已知均匀随机的rand7()&#xff08;生成1~7等概率&#xff09;来构造rand10()&#xff08;生成1~10等概率&#xff09;。思路&#xff1a;题目要求用均匀分布生成另一个均匀分布。1.前提&#xff1a;rand7()可以均匀生成1&#xff0c;2&#xff0c;3…

作者头像 李华
网站建设 2026/4/30 22:10:46

别再复制粘贴了!手把手教你封装一个可复用的Vue2百度地图组件

从零构建高复用Vue2百度地图组件&#xff1a;工程化实践指南 每次新项目需要地图功能时&#xff0c;你是否还在重复复制粘贴那段熟悉的集成代码&#xff1f;当团队中不同成员各自实现的地图功能出现行为差异时&#xff0c;是否让项目维护变得棘手&#xff1f;本文将带你超越基础…

作者头像 李华
网站建设 2026/4/30 22:06:49

流匹配与扩散模型在机器人动作生成中的对比与应用

1. 流匹配与扩散模型的核心差异解析在机器人动作生成领域&#xff0c;流匹配(Flow Matching)和扩散模型(Diffusion Models)代表了两种截然不同的概率路径构建方法。理解它们的本质区别对于选择合适的技术方案至关重要。1.1 概率路径构建方式的对比扩散模型采用了一种"随机…

作者头像 李华
网站建设 2026/4/30 22:04:07

告别手动画图!用PostGIS+PostgreSQL自动生成城市路网(附巴黎实战案例)

基于PostGISPostgreSQL的城市路网自动化生成实战指南 从手工绘制到智能生成&#xff1a;城市路网建模的技术演进 城市规划师和GIS开发者们一定深有体会&#xff1a;传统手工绘制城市路网不仅耗时费力&#xff0c;而且难以保证数据的一致性和准确性。一个中等规模城市的路网可能…

作者头像 李华
网站建设 2026/4/30 21:56:59

Swoole 的onWorkerStart的生命周期的庖丁解牛

Swoole 的 onWorkerStart 是 Swoole 常驻内存架构中最关键、最复杂、也最容易踩坑的生命周期节点。 它的本质是&#xff1a;Worker 进程&#xff08;工作进程&#xff09;诞生后的“初始化入口”。在这个回调中&#xff0c;你完成所有 一次性加载 (One-time Loading) 、 资源预…

作者头像 李华