文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

Java 类加载过程分步源码说明【结合Oracle Docs 官方文档 + HotSpot 实现视角】

七个阶段一览

  1. 加载(Loading) — 找到字节流并把它交给 JVM / ClassLoader 构建类型的运行时表示(创建 Class 镜像、运行时常量池等)。(Oracle Docs)
  2. 验证(Verification) — 校验 classfile 格式与字节码类型安全(防止不安全/非法字节码)。(Oracle Docs)
  3. 准备(Preparation) — 为静态字段分配存储并写入默认零值(field slot 分配、偏移计算)。(Oracle Docs)
  4. 解析(Resolution) — 把运行时常量池中的符号引用(类/字段/方法)替换或映射为直接引用(可延迟到首次使用)。(Oracle Docs)
  5. 初始化(Initialization) — 执行 <clinit>:先把 ConstantValue 常量属性赋值给 static 字段(若有),然后按编译顺序执行静态初始化语句块与显式的静态字段赋值。初始化由若干“主动使用”事件触发。(Oracle Docs)
  6. 使用(Using / Runtime use) — 类可用于创建实例、调用方法、访问静态字段;在运行过程中可能继续延迟解析或被 JIT 优化(内联、vtable、invokedynamic bootstrap 等)。(Oracle Docs, OpenJDK)
  7. 卸载(Unloading) — 实现可选择的优化:类可以在其定义类加载器可回收时被卸载;Bootstrap 加载的类通常不可卸载。卸载由 GC 与 VM 的类表管理协作完成。(O’Reilly Media, OpenJDK)

逐阶段详细说明(规范 + HotSpot 实现视角)

1) 加载(Loading)

规范层面(JVMS)

  • “加载”是把二进制表示(通常是 .class 文件字节流)找出来,并根据它创建类或接口的运行时表示(method area / metaspace 中的内部结构),同时建立该类的运行时常量池(从 classfile 的 constant_pool 构造)。加载可以由 bootstrap loader 或用户定义的 ClassLoader 发起。(Oracle Docs)

ClassLoader 行为(Java API 层)

  • ClassLoader.loadClass(name) 的常见流程:findLoadedClass → 父类委派(parent)→ 自己的 findClass(从文件/JAR/网络/内存读取字节)→ defineClass(bytes) 把字节交给 VM。你可以覆盖 findClass 实现自定义来源,但 defineClass 是进入 VM 本地实现的门。

HotSpot / 源码细节

  • Java 层的 defineClass 最终走本地桥接(如 JVM_DefineClass),进入 HotSpot 的类创建路径:SystemDictionary / PlaceholderTable / ClassFileParser 会把字节解析成 VM 内部的 instanceKlass(或 arrayKlass)结构,并在 Metaspace(或旧时的 PermGen)分配元数据。HotSpot 维护多个表(SystemDictionary、PlaceholderTable 等)跟踪加载过程与已加载类。(OpenJDK, GitHub)

常见陷阱

  • 双亲委派(parent-first)是默认策略;容器/插件通常采用 child-first(或使用 TCCL)以实现隔离。加载失败抛出 ClassNotFoundException

2) 验证(Verification)

目标:保证二进制表示与字节码在结构与类型上“安全可执行”,防止不良或恶意字节码破坏 VM 安全性或内存安全。(Oracle Docs)

验收内容(分层)

  1. 文件格式检查:魔数、版本、常量池结构、字段/方法表结构等(ClassFile 结构层面)。
  2. 字节码结构与语义检查:操作码是否合法、分支目标不是指向指令中间、局部变量/操作数栈的描述一致等。
  3. 类型/数据流检查(最重要):bytecode verifier 做数据流分析以保证类型安全(栈上元素类型与局部变量类型在所有路径上保持一致),现代 JVM 使用 StackMapTable 辅助加速校验。

错误/异常

  • 如果校验失败,会在触发验证的时刻抛出 VerifyErrorClassFormatError(取决失败类型)。注意:验证的时机在规范上可以延迟(实现可调整),但失败时点对于程序语义是有保证的(即当需要链接/执行时会报错)。(Oracle Docs)

3) 准备(Preparation)

做什么:在类“链接”阶段的一部分,为类的静态变量(static fields)分配内存槽并写入默认值(零值),以及完成必要的内部索引/偏移计算(field slot、方法表索引等)。这是把类的元数据从“字节描述”转为“运行时可操作结构”的必要步骤。(Oracle Docs)

细节

  • 字段布局:VM 为每个静态字段分配 slot(int/long/引用不同槽大小),并记录偏移;实体内存(method area / metaspace 管理的结构)准备好后,字段的初始内容是 JVM 规定的零值(0、0.0、null)。
  • ConstantValue 的行为要点static finalConstantValue 属性的字段 不是在准备阶段赋常量,而是在初始化开始时按规范将 ConstantValue 的值写入静态字段,然后再执行 <clinit> 中的其余代码(JVMS 明确说明这一点)。也就是说,准备写入的是零值,常量赋值发生在初始化阶段。(Oracle Docs)

4) 解析(Resolution)

作用:把运行时常量池(符号引用)中的符号引用(例如:类名 A、字段 B.f、方法 C.m)解析为直接引用(指向具体的类对象、字段偏移、方法表条目等)。解析可以在链接时全部做,也可以按需延迟(lazy resolution),JVM 允许实现延迟解析以提高性能。(Oracle Docs)

解析的种类

  • 类解析:将符号类名解析为实际的 Class 型表示(可能触发加载该类)。
  • 字段解析:定位字段的定义(包括可见性校验),并把字段符号替换为字段指针/偏移。
  • 方法解析:定位方法的直接引用(静态/特殊/虚方法的解析语义略有不同),并做好调用点的链接准备。

HotSpot 实现

  • HotSpot 使用 SystemDictionary::resolve / resolve_or_fail 等机制来解析类名与方法/字段引用。解析可能触发更多的类加载或链接(比如解析字段时要加载字段声明类)。解析失败会抛出 NoClassDefFoundErrorNoSuchMethodErrorIllegalAccessError 等。(OpenJDK CR, OpenJDK)

5) 初始化(Initialization)

规范说明(关键):初始化就是执行类或接口的 <clinit> 方法(由编译器把所有静态字段显式初始化和静态块组合成一个 <clinit>)。初始化的时机由 JLS/JVMS 明确限定 —— 只有在“主动使用”场景下才会触发(例如 newinvokestaticgetstatic/putstatic、反射强制初始化等)。(Oracle Docs)

详细步骤(JVMS 的规范化过程,含初始化锁)

  • 初始化锁(LC):规范要求“每个类/接口 C 有一个唯一的初始化锁 LC”,初始化过程中会对该锁进行同步以避免并发初始化导致竞态或重复执行。该映射由 JVM 实现决定(如 HotSpot 可能用 Class 对象的 monitor 或隐藏对象进行同步)。(Oracle Docs)

  • 规范化的初始化流程概要(简化版):

    1. 获取初始化锁 LC(等待直到可获得);
    2. 检查是否已初始化或正在初始化(如果其它线程正在初始化,当前线程可能等待或返回);
    3. 如果需要,先对超类进行初始化(规范要求父类先初始化);
    4. 记录“当前线程正在初始化”状态;
    5. 赋值 ConstantValue 属性到对应 static 字段(按 classfile 中字段表顺序)(这一步发生在 <clinit> 执行前);(Oracle Docs)
    6. 执行 <clinit> 字节码(包含静态初始化块和显式赋值);
    7. 如果 <clinit> 正常返回,标记类已成功初始化;若抛出异常,则把类标记为有初始化错误(随后对该类的主动使用会得到 ExceptionInInitializerError / 初始化错误状态)。(Oracle Docs)

HotSpot 角度

  • HotSpot 在 instanceKlass 中维护 _init_state(例如:allocated / loaded / linked / initialized / initialization_error 等状态),并在 klass->initialize() 路径上完成上述同步、父类初始化依赖与 <clinit> 调用。Class.forName(name, true, loader) 在 native 层会在返回前触发 initialize。(OpenJDK Builds, Google Android源码)

重要注意点

  • 编译期常量内联static final 被编译为常量并内联到使用处(常量折叠),因此读取这些常量通常不会触发类初始化(因为字节码里没有 getstatic)。这是很多人对“什么时候触发初始化”产生困惑的根源。(Oracle Docs)

6) 使用(Using / Runtime use)

含义:类完成初始化后就可以被“使用”——创建实例(new)、调用方法、访问静态成员等。使用阶段是程序语义层面的持续运行:运行时继续可能发生的事情包含运行期解析(lazy resolution)动态链接(例如 invokedynamic bootstrap)、以及 JIT 编译器对方法/字段的优化(内联、逃逸分析等)。(Oracle Docs, OpenJDK)

实现与优化关注点

  • 方法调用:第一次调用某方法时会完成符号解析和调用点链接(对于虚方法,调用点会使用 vtable/itable);JIT 在热点处会把调用内联或做其他优化。
  • 动态语言支持invokedynamic 引导函数(bootstrap method)会在首次执行时完成链接/安装 callsite,之后调用是直接的。

7) 卸载(Unloading)

规范与条件

  • 卸载类/接口是实现可选的(JVM 规范允许但不强制)。JLS 明确:一个类或接口能被卸载的充分且必要条件是其定义它的类加载器可以被垃圾回收(也就是说:类卸载与类加载器可回收性挂钩)。此外,由 Bootstrap(启动类加载器)加载的类通常不可卸载。(O’Reilly Media, Stack Overflow)

HotSpot 实现要点

  • HotSpot 在内部维护 SystemDictionaryPlaceholderTable 等结构来跟踪类;实际卸载通常发生在 GC safepoint 时清理可回收的 class loader 所拥有的类与元数据(Metaspace 回收),并移除 SystemDictionary 的条目。卸载并非即时且容易受“类泄露”问题(静态引用、线程本地、JNI 全局引用、未释放的 ClassLoader 等)阻碍。(OpenJDK)

实践中的陷阱

  • 类加载器泄露(例如:静态集合持有来自 webapp 的类实例或线程未停止)会阻止类卸载,这是容器(Tomcat 等)常见的内存泄露来源。要让类被卸载,必须确保没有任何 GC 可达路径引用定义该类的 ClassLoader、Class 对象或其实例。(Morling)

常见问题与误区(速答)

  • “准备阶段会把 static final 常量赋上值吗?” 不。准备只写入零值;带 ConstantValuestatic final 字段的常量值在初始化开始时赋值。(Oracle Docs)
  • “什么时候触发类初始化?” JLS/JVMS 列出了严格的“主动使用”触发条件(new/getstatic/putstatic/invokestatic、反射/ForName 等);编译内联的常量读取不触发。(Oracle Docs)
  • “类什么时候会卸载?” 只有当其定义的类加载器可回收时才可能卸载;bootstrap 加载的类不会被卸载;实际卸载由 GC 与 VM 实现决定。(O’Reilly Media, OpenJDK)

Mermaid 流程图(一张图串通全流程)

在这里插入图片描述


推荐阅读(规范与源码入口)

  • JVM 规范(Chapter 5)——Loading, Linking and Initialization(官方). (Oracle Docs)

  • Java Language Specification(Chapter 12,Initialization)(触发条件与初始化细则). (Oracle Docs)

  • HotSpot Runtime Overview / SystemDictionary / instanceKlass 源码(查看 instanceKlass.cpp / systemDictionary.cpp / classfile 路径可了解 VM 内部状态与实现)。(OpenJDK, GitHub)

  • 关于类卸载与类加载器回收的讨论(JLS §12.7 / 各类博客与实践文章),便于理解 webapp 中的类泄露问题。(O’Reilly Media, Morling)

posted @ 2025-09-01 10:31  NeoLshu  阅读(4)  评论(0)    收藏  举报  来源