编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
        • 1. 案例引入
          • 1.1 一次冷启动竞赛
          • 1.2 反直觉的曲线
          • 1.3 我们要回答什么
        • 2. AOT 与 JIT 之争
          • 2.1 两种翻译路径
          • 2.2 各自的代价
          • 2.3 为何此时复活
        • 3. GraalVM 全家桶
          • 3.1 三个核心组件
          • 3.2 Graal 编译器
          • 3.3 Truffle 多语言
          • 3.4 Native Image
        • 4. 闭世界假设
          • 4.1 假设的含义
          • 4.2 可达性分析
          • 4.3 假设代价清单
        • 5. 构建过程拆解
          • 5.1 五大构建阶段
          • 5.2 静态分析迭代
          • 5.3 堆快照与初始化
          • 5.4 镜像产物结构
        • 6. SubstrateVM 揭秘
          • 6.1 极简版 JVM
          • 6.2 GC 选型差异
          • 6.3 没有了什么
        • 7. 反射与动态特性
          • 7.1 三类动态行为
          • 7.2 配置文件机制
          • 7.3 Agent 自动采集
        • 8. 性能取舍真相
          • 8.1 启动与内存收益
          • 8.2 峰值吞吐损失
          • 8.3 PGO 与机器学习
        • 9. 选型决策矩阵
          • 9.1 适合的场景
          • 9.2 不适合的场景
          • 9.3 迁移落地清单
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一次构建的一生
          • 10.3 设计哲学回扣
          • 10.4 对决速查表
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

GraalVM与AOT编译原理

# 10.GraalVM与AOT编译原理

# 目录介绍

  • 1. 案例引入
    • 1.1 一次冷启动竞赛
    • 1.2 反直觉的曲线
    • 1.3 我们要回答什么
  • 2. AOT 与 JIT 之争
    • 2.1 两种翻译路径
    • 2.2 各自的代价
    • 2.3 为何此时复活
  • 3. GraalVM 全家桶
    • 3.1 三个核心组件
    • 3.2 Graal 编译器
    • 3.3 Truffle 多语言
    • 3.4 Native Image
  • 4. 闭世界假设
    • 4.1 假设的含义
    • 4.2 可达性分析
    • 4.3 假设代价清单
  • 5. 构建过程拆解
    • 5.1 五大构建阶段
    • 5.2 静态分析迭代
    • 5.3 堆快照与初始化
    • 5.4 镜像产物结构
  • 6. SubstrateVM 揭秘
    • 6.1 极简版 JVM
    • 6.2 GC 选型差异
    • 6.3 没有了什么
  • 7. 反射与动态特性
    • 7.1 三类动态行为
    • 7.2 配置文件机制
    • 7.3 Agent 自动采集
  • 8. 性能取舍真相
    • 8.1 启动与内存收益
    • 8.2 峰值吞吐损失
    • 8.3 PGO 与机器学习
  • 9. 选型决策矩阵
    • 9.1 适合的场景
    • 9.2 不适合的场景
    • 9.3 迁移落地清单
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一次构建的一生
    • 10.3 设计哲学回扣
    • 10.4 对决速查表

# 1. 案例引入

# 1.1 一次冷启动竞赛

某金融团队做 Serverless 改造,把一个 Spring Boot 微服务搬上 FaaS 平台——按调用次数付费、闲置即冷却。第一周账单出来时所有人惊掉下巴:冷启动费用占总账单的 73%。

复盘数据:

请求峰值:    1200 QPS  → 同时拉起约 80 个实例
冷启动时间:  4.2 秒    → 用户首请求超时
JVM 内存基线:256 MB   → 单实例固定内存开销
预热到峰值:  约 2 分钟  → JIT 编译热代码
1
2
3
4

问题不在业务代码——业务逻辑只占启动 4.2 秒中的 0.4 秒。剩下 3.8 秒全花在:

0.5s  JVM 启动 + 加载 BootClassLoader 核心类
1.4s  Spring 扫描 ClassPath、构建 BeanDefinition
0.8s  反射调用初始化全部单例 Bean
0.6s  Tomcat 启动 + 监听端口
0.5s  JIT 解释执行的"冷"阶段(性能仅峰值 30%)
1
2
3
4
5

团队决定用 GraalVM Native Image 重新打包同一份代码,结果让人意外:

冷启动时间:  4200 ms → 38 ms       (110x 提升)
基线内存:    256 MB → 18 MB         (14x 缩减)
镜像大小:    52 MB jar → 78 MB binary (变大了)
峰值 TPS:    8500 → 6800            (下降 20%)
构建时间:    18 秒 → 4 分 30 秒     (15x 变长)
1
2
3
4
5

冷启动 110 倍提升、内存缩 14 倍——但峰值吞吐反而下降 20%、构建时间膨胀 15 倍。

# 1.2 反直觉的曲线

如果把"启动 → 运行"这条时间轴画成 TPS 曲线,两个产物的曲线走向截然相反:

TPS
 │
 │            ┌──────────  传统 JVM (峰值 8500)
 │           ╱
 │          ╱
 │         ╱   ←—— JIT 预热 1~2 分钟
 │        ╱
 │   ┌───┴────────────  Native Image (稳态 6800)
 │   │
 │   │← 38ms 即达稳态
 ├───┴──────────────────────────────────────→  时间
 0  100ms              60s              120s
1
2
3
4
5
6
7
8
9
10
11
12

这条曲线浓缩了整个 GraalVM 设计哲学的核心矛盾:

  • 传统 JVM:起步慢但天花板高——靠 JIT + 运行时反馈把热代码越优化越极致
  • Native Image:起步飞快但天花板低——AOT 在编译期就把代码焊死,没有运行时学习能力

更深的问题是:这两条曲线哪条"更好"?——答案完全取决于业务场景。Serverless 选 Native Image 一年省百万;长跑微服务硬切 Native Image 一年损失千万。

# 1.3 我们要回答什么

第 18 篇要把 "Java 还能不像 JVM 那样跑" 这件事讲透——读完之后再面对一个 Java 应用,5 分钟内能判断它该不该上 Native Image,并能识别迁移过程的所有坑点。

带着这个目标,要回答 7 个核心问题:

① AOT 不是新东西(C/C++ 早就这样),为什么到 Java 才大火?        → 第2.3节
② GraalVM 到底是什么?跟 OpenJDK / OracleJDK 什么关系?         → 第3章
③ "闭世界假设"是什么?为什么它是 Native Image 的灵魂?          → 第4章
④ 反射 / 动态代理 / SPI / 资源加载在 Native Image 还能用吗?    → 第7章
⑤ 为什么 Native Image 启动 100x 但峰值吞吐下降 20%?             → 第8章
⑥ 构建时间从 18 秒到 4 分钟,多出来的时间在做什么?             → 第5章
⑦ 哪些场景一定要上 Native Image?哪些上了会翻车?               → 第9章
1
2
3
4
5
6
7

本篇路线:

AOT vs JIT 历史 (第2章)
    ↓
GraalVM 全家桶解构 (第3章)
    ↓
闭世界假设 (第4章)  ←—— 灵魂概念
    ↓
构建过程五阶段 (第5章)
SubstrateVM 揭秘 (第6章)
反射与动态特性 (第7章)
    ↓
性能取舍真相 (第8章)
选型决策矩阵 (第9章)
    ↓
综合案例串讲 (第10章)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2. AOT 与 JIT 之争

# 2.1 两种翻译路径

把"源代码 → CPU 执行"这条路画出来,AOT 与 JIT 是两条截然不同的路径:

                源代码 (.java)
                     │
                     │ javac
                     ↓
                字节码 (.class)
                     │
        ┌────────────┼────────────┐
        │                         │
   传统 JVM 路线                AOT 路线
        │                         │
        ↓                         ↓
   类加载 + 解释执行          静态分析 + 整体编译
        │                         │
        ↓                         ↓
   热代码触发 JIT 编译        生成原生可执行文件
        │                         │
        ↓                         ↓
   Tier1→Tier4 渐进优化       一次性焊死所有决策
        │                         │
        ↓                         ↓
   运行时机器码              直接运行机器码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

关键差异:

维度 JIT (传统 JVM) AOT (Native Image)
编译时机 运行时 构建期
输入信息 字节码 + 运行剖面 字节码 + 配置
输出形式 内存中的机器码 磁盘上的可执行文件
优化空间 基于实际数据的剖面引导 基于静态分析的保守估计
启动开销 解释 → 编译多次切换 零(直接执行)

# 2.2 各自的代价

JIT 的代价——14 篇详细讲过:

+ 长跑业务峰值性能极致(运行时数据指导优化)
+ 跨平台一次编译到处运行
- 启动慢(解释执行 + 类加载)
- 内存占用高(JVM 自身 + Metaspace + CodeCache)
- 部署体积大(JRE 至少 50MB+)
1
2
3
4
5

AOT 的代价——本篇主角:

+ 启动飞快(毫秒级)
+ 内存占用极低(无 JVM 自身开销)
+ 部署体积可控(单一二进制,无需 JRE)
- 构建时间长(静态分析占大头)
- 峰值性能受限(无运行时剖面)
- 动态特性受限(反射 / 代理 / SPI 需配置)
- 平台绑定(Linux 镜像不能跑在 macOS)
1
2
3
4
5
6
7

疑惑:既然 JIT 和 AOT 各有优劣,为什么不"两个都要"?

论证:实际上 OpenJDK 早在 JDK 9 就尝试过 —— jaotc 工具,把热代码 AOT 编译成 .so 文件,运行时仍然用 JVM 加载。但这条路在 JDK 17 被废弃,JDK 21 彻底移除。原因是:

  1. 维护成本高:jaotc 与 JIT 共享一套 IR,演进时两边要同步改
  2. 收益不明显:仍然需要完整 JVM 运行时,启动只省几百毫秒
  3. 生态分叉:开发者难以判断"什么时候 AOT、什么时候 JIT"

结论:纯 AOT (Native Image) 才是真正的范式革命——它不是"在 JVM 上加 AOT",而是"绕过 JVM"。这是与 jaotc 的根本区别。

# 2.3 为何此时复活

疑惑:AOT 是 1950 年代就有的老技术(C/Fortran 早期都是 AOT),为什么直到 2019 年(GraalVM 19.0)才在 Java 圈大火?

论证:三股潮流同时汇聚:

┌─────────────────┐     ┌─────────────────┐     ┌──────────────────┐
│  云原生 / FaaS  │     │   容器化部署    │     │   微服务拆分     │
│  按调用计费      │     │   镜像越小越好  │     │   单服务变小     │
│  冷启动决定钱包  │     │   内存越省越好  │     │   每个 JVM 都贵  │
└────────┬────────┘     └────────┬────────┘     └────────┬─────────┘
         │                       │                       │
         └───────────┬───────────┴───────────────────────┘
                     ↓
              对"启动快 + 内存小"的需求
              第一次成为业务一等公民
                     ↓
              AOT 复活的土壤
1
2
3
4
5
6
7
8
9
10
11
12

结论:不是 AOT 突然变好了,是业务场景变了。同一项技术,在 2008 年是负担(JIT 长跑业务的天敌),在 2024 年是救命稻草(Serverless 冷启动杀手)。技术评价永远绑定于场景,没有绝对的好坏。这条规律在第 17 篇 GC 演进、第 11 篇 IO 模型演进里都见过。

# 3. GraalVM 全家桶

# 3.1 三个核心组件

GraalVM 不是一个单独的工具,而是一个技术栈:

┌─────────────────────────────────────────────────────┐
│                    GraalVM                          │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │  Graal       │  │  Truffle     │  │  Native   │  │
│  │  Compiler    │  │  Framework   │  │  Image    │  │
│  │              │  │              │  │           │  │
│  │  替代 C2     │  │  多语言运行  │  │  AOT 打包 │  │
│  │  JIT 编译器  │  │  (JS/Py/R..) │  │           │  │
│  └──────────────┘  └──────────────┘  └───────────┘  │
│           ↓                                  ↓      │
│      JIT 模式                          AOT 模式     │
└─────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

三者的关系:

  • Graal Compiler 是基础——一个用 Java 写的高级 JIT 编译器,可以替代 HotSpot 的 C2
  • Truffle 是上层框架——基于 Graal,实现"用 Java 写解释器、自动获得高性能",让 GraalVM 同时跑 JavaScript / Python / Ruby / R
  • Native Image 是另一种产物形态——用 Graal 在构建期把整个应用编译成可执行文件

关键澄清:本文说的"GraalVM",多数语境特指 Native Image 这条路径——这才是与传统 JVM 范式对立的部分。

# 3.2 Graal 编译器

疑惑:Graal 既然只是替代 C2,跟"AOT"有什么关系?

论证:Graal 同一份编译器代码可以工作在两种模式:

模式 1:JIT (作为 HotSpot 的 C2 替代)
   字节码 + 运行剖面 → Graal → 机器码 (运行时)
   开关:-XX:+UseJVMCICompiler -XX:+UseGraalJIT
   
模式 2:AOT (作为 Native Image 的核心引擎)
   字节码 + 静态分析结果 → Graal → 机器码 (构建期)
   工具:native-image
1
2
3
4
5
6
7

Graal 自身的关键特性:

  • 完全用 Java 写——HotSpot 的 C2 是 C++ 实现,Graal 用 Java 实现
  • 基于 IR (Sea of Nodes)——比 C2 的 IR 更现代、更易演进
  • 支持 PGO(Profile-Guided Optimization)——AOT 模式下也可用历史剖面优化

结论:Graal 是"编译器即库"的范式——可以嵌入 JVM 当 JIT,也可以独立运行做 AOT。这种解耦是 Native Image 能存在的技术前提。

# 3.3 Truffle 多语言

Truffle 与本篇主线关系不大,但理解它有助于理解 GraalVM 的整体定位:

// Truffle 让你"用 Java 写一个 Python 解释器"
//   只要按 Truffle API 写解释器
//   GraalVM 自动用部分求值 (Partial Evaluation) 把解释器
//   优化到接近原生 Python 的性能
1
2
3
4

实际产物:

  • GraalJS——比 V8 引擎慢约 2x,但能直接调 Java 类
  • GraalPy——CPython 兼容
  • TruffleRuby——比 MRI 快 5~30x

本篇之后不再展开 Truffle——重心回到 Native Image。

# 3.4 Native Image

Native Image 的核心承诺:

输入:一份普通的 Java 应用 (jar 或 module)
输出:一个原生可执行文件
      Linux: ELF
      macOS: Mach-O
      Windows: PE
      
特性:
  ✓ 无需 JRE 即可运行
  ✓ 启动时间毫秒级
  ✓ 内存占用极低
  ✓ 与 C/C++ 程序具备相同部署形态
1
2
3
4
5
6
7
8
9
10
11

与 Go / Rust 二进制的区别:

  • Native Image 仍然是有 GC 的语言(不像 Rust 的 RAII)
  • Native Image 仍然有运行时(SubstrateVM,见 §6)
  • Native Image 仍然支持多线程 + 内存模型

简单说——它是"长得像 Go 二进制、内核仍是 Java"的混血产物。

# 4. 闭世界假设

# 4.1 假设的含义

闭世界假设(Closed World Assumption,CWA)是 Native Image 最重要的设计前提,没有之一。

闭世界假设:构建期能枚举到的类、方法、字段,就是运行期所有可能用到的全部。运行期不会出现新的类。

对比传统 JVM 的开世界:

传统 JVM (开世界)              Native Image (闭世界)
─────────────────             ──────────────────
运行时可加载新类               构建期固定所有类
反射可访问任意成员             反射目标必须预声明
ClassLoader 可热部署           没有 ClassLoader 概念
SPI 可发现新服务               SPI 必须构建期注册
代理可动态生成                 代理必须 buildtime 生成
1
2
3
4
5
6
7

# 4.2 可达性分析

疑惑:闭世界假设怎么落地?编译器怎么知道哪些类要不要进二进制?

论证:通过静态可达性分析——从 main 方法出发做巨大的图遍历:

   入口点 (main)
        │
        │ 直接调用
        ↓
    ┌───────┐  ┌──────┐  ┌─────────┐
    │ Foo() │→│ Bar()│→│ Baz()    │
    └───┬───┘  └──┬───┘  └────┬────┘
        │         │           │
        ↓         ↓           ↓
    引用类型    引用类型    Class.forName(...)
    (静态)     (静态)       ↑
                            │
                     必须配置文件告诉
                     "这里要加载 X 类"
1
2
3
4
5
6
7
8
9
10
11
12
13
14

核心算法:

  1. 从所有入口点(main / 静态初始化 / 测试入口)出发
  2. 沿着静态可见的调用边遍历
  3. 遇到反射 / Class.forName 时查配置文件——配置里登记的类视为可达
  4. 不可达的类、方法、字段全部不进二进制——这是镜像能瘦身的根本原因

结论:Native Image 的瘦身不是"压缩",是"裁剪"。如果 jar 是 50MB 但应用只用了 20% 的代码,那么 Native Image 镜像里只剩这 20% 的机器码——这是 AOT 相比 JVM 的另一项免费收益。

# 4.3 假设代价清单

闭世界不是免费的——它强制要求你:

动态行为 闭世界下的对策 成本
Class.forName("X") reflect-config.json 注册 X 配置维护
Method.invoke(...) reflect-config.json 注册 method 配置维护
Proxy.newProxyInstance(...) proxy-config.json 注册接口列表 配置维护
getResource("/conf.yaml") resource-config.json 包含资源 配置维护
ServiceLoader META-INF/services 自动扫描,但 impl 类需 reflect 配置 半自动
字节码生成(CGLIB / ASM) 基本无解,必须改用 buildtime 代理 重写代码
类热加载 / OSGi / 热部署 完全无法支持 放弃使用

关键认知:闭世界假设把"动态性"换成了"启动速度 + 内存效率"——这是一笔明确的、不可逆的取舍。如果业务严重依赖动态性(如 OSGi 插件框架、字节码热替换),Native Image 就不是选项。

# 5. 构建过程拆解

# 5.1 五大构建阶段

执行 native-image -jar app.jar 时,幕后跑了一个长链路:

flowchart LR
    A[① Initializing<br/>类路径扫描] --> B[② Performing analysis<br/>静态可达性分析]
    B --> C[③ Building universe<br/>类型与方法集合]
    C --> D[④ Parsing<br/>字节码 → IR]
    D --> E[⑤ Compiling<br/>IR → 机器码]
    E --> F[⑥ Layouting<br/>布局到 ELF/Mach-O]
    F --> G[原生可执行文件]
1
2
3
4
5
6
7

真实构建日志(节选):

[1/8] Initializing...                                      (3.2s @ 0.20GB)
[2/8] Performing analysis...                              (78.4s @ 4.50GB)
   12,856 reachable types  (88.9% of   14,452 total)
   23,447 reachable fields (60.1% of   39,038 total)
   71,288 reachable methods (62.2% of  114,612 total)
[3/8] Building universe...                                 (4.5s @ 4.10GB)
[4/8] Parsing methods...                                   (8.9s @ 5.20GB)
[5/8] Inlining methods...                                  (5.6s @ 5.30GB)
[6/8] Compiling methods...                                (95.7s @ 6.10GB)
[7/8] Layouting methods...                                 (4.8s @ 6.20GB)
[8/8] Creating image...                                    (3.6s @ 5.80GB)
Top 10 origins of code area:
   24.7MB java.base
   12.3MB svm.jar (Native Image)
   8.5MB  spring-core
   ...
Finished generating 'app' in 3m 45s.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

时间分布:分析 + 编译占 80%,其他阶段加起来 20%。构建膨胀的元凶就是这两步——这就回答了第 1 章疑问 ⑥。

# 5.2 静态分析迭代

疑惑:分析阶段为什么这么慢?不就是图遍历吗?

论证:实际上 native-image 的分析是多轮迭代——每轮发现新的可达点后回头重新算:

轮 1:从 main 出发,找到 100 个可达类
       发现某个类的静态构造器引用了新类型
       
轮 2:把新类型加入工作集,再次遍历
       又发现某些类有 @TargetClass 注解(替换原始类)
       
轮 3:处理替换关系,再算一遍
       ...
       
直到一轮不再发现新可达点为止 (固定点收敛)
1
2
3
4
5
6
7
8
9
10

每轮都要重新做:

  • 类型流分析(每个变量在每个程序点的可能类型集)
  • 转义分析(对象逃逸路径)
  • 折叠常量(编译期可确定的值)

固定点收敛通常 5~10 轮,单轮分析数十秒——加起来就是分钟级别。

结论:慢不是工具问题,是问题本身的复杂度——闭世界假设的强度越高(要求保守覆盖所有可达性),分析迭代次数越多。这是 AOT 的"原罪"。

# 5.3 堆快照与初始化

Native Image 一个独特特性——构建期初始化(build-time initialization):

传统 JVM                       Native Image
──────────                     ──────────────
启动时执行所有 <clinit>        构建期就执行选定类的 <clinit>
                              结果序列化到镜像的"image heap"
                              
启动时机器码 + 全部初始化       启动时机器码 + 反序列化堆快照
   ↓                              ↓
慢                             快(直接 mmap 映射)
1
2
3
4
5
6
7
8

举例:一个 Logger.getLogger("foo") 的初始化在普通 JVM 里要花几毫秒解析 logback 配置;在 Native Image 里这个 logger 对象在构建期就创建好了,启动时只是把堆快照映射进内存。

这是 Native Image 启动 38ms 的另一个秘密——很多"启动工作"被前移到了构建期。

陷阱:构建期初始化时,当前主机的环境变量、系统时间、文件路径会被永久固化到镜像里。如果你在 <clinit> 里写了 System.getenv("HOME"),得到的会是构建机的 HOME,而不是运行机的——这是新手常踩的坑。

# 5.4 镜像产物结构

最终输出的二进制文件长这样:

ELF / Mach-O / PE 可执行文件
├── .text         ← 编译后的机器码 (60~70%)
├── .rodata       ← 字符串常量、类元数据
├── .data         ← 可写全局变量
├── .image_heap   ← 构建期堆快照(mmap 加载)
├── .symbols      ← 调试符号(可选)
└── 启动入口      ← 调用 SubstrateVM 入口点
1
2
3
4
5
6
7

启动流程:

OS 加载 ELF
   ↓
执行入口点 _start
   ↓
SubstrateVM 初始化(极简,约 10ms)
   ↓
mmap 映射 image_heap
   ↓
跳转到用户 main 方法
1
2
3
4
5
6
7
8
9

结论:启动 38ms 的构成:~10ms SubstrateVM、~5ms mmap、~20ms 用户业务初始化、~3ms HTTP 端口监听。根本没有给"类加载"留空间。

# 6. SubstrateVM 揭秘

# 6.1 极简版 JVM

Native Image 不是没有运行时——它有一个极简版 JVM 叫 SubstrateVM (SVM),体积约 10~12MB,被静态链接进每个镜像。

SVM 提供的功能:

✓ GC(Serial / G1 子集)
✓ 异常处理 (栈展开)
✓ 内存模型 (volatile / synchronized)
✓ 线程调度 (基于 OS 线程)
✓ 信号处理
✓ JNI(受限)
1
2
3
4
5
6

SVM 不提供的功能(与 HotSpot 对比):

✗ 类加载器
✗ Metaspace
✗ JIT 编译器
✗ 解释器
✗ JFR(JDK 22+ 部分支持)
✗ 字节码热替换
✗ 大部分 JVMTI 接口
1
2
3
4
5
6
7

# 6.2 GC 选型差异

SVM 的 GC 实现是简化版,与 HotSpot 不能直接对应:

GC 算法 HotSpot SubstrateVM
Serial ✓ ✓(默认)
Parallel ✓ ✗
G1 ✓(17 默认) ✓(GraalVM 22.2+ 商业版可用)
ZGC ✓ ✗
Shenandoah ✓ ✗
Epsilon ✓ ✓

关键区别:SubstrateVM 的 Serial GC 是全 STW 单线程——简单可靠但暂停时间较长。这对短生命周期 Serverless 函数完全没问题(一次调用可能 GC 都不触发就结束了),但对长跑大堆服务不友好。

配置参数:

# 选择 GC(构建期决定)
native-image --gc=serial      # 默认
native-image --gc=G1          # 仅 GraalVM EE 商业版

# 运行期参数(与 HotSpot 类似但子集)
./app -Xmx2g -Xms2g -XX:+PrintGC
1
2
3
4
5
6

# 6.3 没有了什么

疑惑:去掉这么多东西,还能叫"Java"吗?

论证:SubstrateVM 的取舍是精确的——

保留:Java 语言核心规范的全部
      + 内存模型、并发原语、异常体系
      + 大部分 JDK 标准库
      
裁剪:JVM 实现细节中"运行时灵活性"相关的部分
      - 类加载、JIT、字节码增强
1
2
3
4
5
6

也就是说——Java 语言层面写的代码不变,变的是底下的执行机制。99% 的业务代码在 Native Image 上能跑得跟 JVM 一样,剩下 1% 是用了反射、代理、字节码生成的"框架代码"——而这正是为什么Spring / Hibernate 等框架要专门做 GraalVM 适配。

结论:Native Image 是"Java 语言的子集运行时"——不是"另一种语言",而是"裁掉动态性的同一种语言"。理解这一点,才能正确预期它的能力边界。

# 7. 反射与动态特性

# 7.1 三类动态行为

闭世界假设最大的痛点是动态行为。归纳成三类:

① 反射(Reflection)
   Class.forName / Method.invoke / Field.set
   
② 动态代理(Dynamic Proxy)
   Proxy.newProxyInstance
   CGLIB.create
   
③ 资源加载(Resource)
   ClassLoader.getResource / getResourceAsStream
   ServiceLoader.load
1
2
3
4
5
6
7
8
9
10

每一类都需要告诉 native-image 你要用什么——通过配置文件。

# 7.2 配置文件机制

GraalVM 通过 META-INF/native-image/ 下的 JSON 配置文件描述动态需求:

META-INF/native-image/group/artifact/
├── reflect-config.json       ← 反射目标
├── proxy-config.json         ← 代理接口列表
├── resource-config.json      ← 资源文件
├── jni-config.json           ← JNI 调用
└── serialization-config.json ← 序列化目标
1
2
3
4
5
6

reflect-config.json 示例:

[
  {
    "name": "com.example.User",
    "allDeclaredFields": true,
    "allDeclaredMethods": true,
    "allDeclaredConstructors": true,
    "queryAllPublicMethods": true
  },
  {
    "name": "com.example.OrderService",
    "methods": [
      { "name": "process", "parameterTypes": ["com.example.Order"] }
    ]
  }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

proxy-config.json 示例:

[
  { "interfaces": ["java.sql.Connection", "java.lang.AutoCloseable"] },
  { "interfaces": ["com.example.UserRepo"] }
]
1
2
3
4

疑惑:这些 JSON 谁来写?手写岂不是要写到天荒地老?

# 7.3 Agent 自动采集

论证:GraalVM 提供 Tracing Agent 自动采集——以普通 JVM 运行你的应用、走一遍业务流,Agent 记录所有反射 / 代理 / 资源加载,自动生成配置文件:

# 第一步:以追踪模式跑一遍应用
java -agentlib:native-image-agent=config-output-dir=META-INF/native-image \
     -jar app.jar
     
# 业务正常跑,覆盖完测试用例后退出
# Agent 自动生成 reflect-config.json 等文件

# 第二步:用生成的配置构建镜像
native-image -jar app.jar
1
2
3
4
5
6
7
8
9

配置覆盖率原则:

单测覆盖   = 必要下限(接口契约层)
集成测试覆盖 = 推荐目标(覆盖框架反射)
生产流量回放 = 最高保障(覆盖罕见路径)
1
2
3

陷阱:Agent 只能记录"已经跑过的代码路径"——如果某个分支测试没覆盖到,运行时就会爆 ClassNotFoundException / NoSuchMethodException——而且是毫无征兆地爆,因为闭世界已经把那些类裁剪了。

框架现状:Spring Boot 3.0+ / Quarkus / Micronaut 都内置了 META-INF/native-image/ 配置——这就是"GraalVM-friendly 框架"的真实含义。老框架(如 Hibernate 5)需要大量手工补配。

# 8. 性能取舍真相

# 8.1 启动与内存收益

启动时间收益的来源(38ms vs 4.2s):

4200ms 拆解(传统 JVM)              38ms 拆解(Native Image)
─────────────────────             ──────────────────────
500ms  JVM 自身启动                10ms  SubstrateVM
1400ms ClassPath 扫描              0ms   (构建期完成)
800ms  Spring Bean 初始化          5ms   (堆快照映射)
600ms  Tomcat 启动                 20ms  业务初始化
500ms  JIT 解释期"冷"              0ms   (机器码已就绪)
500ms  其他                        3ms   端口监听
1
2
3
4
5
6
7
8

根因:Native Image 把"类加载、初始化、JIT 编译"这三件事全前移到了构建期。运行期只剩"内存映射 + 业务逻辑"。

内存收益(256MB → 18MB)来源:

传统 JVM 运行时基线:
  ├── JVM 自身代码段        ~70 MB
  ├── Metaspace             ~80 MB (Spring 上千个类)
  ├── CodeCache             ~50 MB
  ├── GC 元数据 + JIT 缓冲  ~30 MB
  ├── 线程栈 (200 × 1MB)    ~200 MB
  └── 业务堆                ~256 MB(设定上限)
                合计 RSS:     约 500~600 MB
                
Native Image:
  ├── SubstrateVM           ~10 MB
  ├── 业务机器码            ~30 MB(裁剪后)
  ├── image_heap            ~5 MB
  ├── 线程栈 (10 × 256KB)   ~3 MB
  └── 业务堆                ~10 MB(小工作集)
                合计 RSS:     约 50~80 MB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

结论:Native Image 不是"用更聪明的方法",而是"用更少的东西"——本来就没有 Metaspace、没有 JIT,自然不占内存。

# 8.2 峰值吞吐损失

疑惑:为什么 Native Image 峰值吞吐反而下降 20%?AOT 编译过的代码不是更快吗?

论证:JIT 有 AOT 不可能拥有的三件优化:

  1. 基于剖面的内联:JIT 看到 obj.method() 99% 时间 obj 是 Foo 类型,会激进内联 Foo.method——AOT 不知道实际调用分布,只能保守不内联(或者内联所有可能类型,代码膨胀)
  2. 分支频率优化:JIT 知道某个 if 分支 99.9% 走 false,会把热路径放在前面,提升分支预测命中——AOT 只能按代码顺序排
  3. 去虚化 (devirtualization):JIT 看到某接口运行时只有一个实现,直接调具体方法——AOT 必须保留虚调用
JIT 在 60 秒预热后能做的优化:
  + 基于运行时数据的激进内联
  + 类层级分析 (CHA) 去虚化
  + 推测优化 + 失败回退
  
AOT 在构建期能做的优化:
  - 静态内联(保守)
  - 静态去虚化(仅 final 类生效)
  - 没有推测,全是稳态
1
2
3
4
5
6
7
8
9

真实数据对比(Spring Boot REST 服务,相同业务):

                     启动     稳态TPS   P99    内存
HotSpot + C2         4.2s     8500    18ms   256MB
Native Image         38ms     6800    24ms   18MB
Native Image + PGO   45ms     7900    20ms   22MB
1
2
3
4

PGO(下一节)能把吞吐损失从 20% 缩小到 7%——但仍达不到 JIT。

# 8.3 PGO 与机器学习

PGO(Profile-Guided Optimization)—— GraalVM EE / Oracle GraalVM 提供:

# 第一步:构建带 instrumentation 的镜像
native-image --pgo-instrument -jar app.jar

# 第二步:用真实流量跑 instrumented 镜像
./app                # 业务跑一段时间,生成 default.iprof

# 第三步:用剖面构建优化版镜像
native-image --pgo=default.iprof -jar app.jar
1
2
3
4
5
6
7
8

机制:第一步构建的镜像内嵌了计数器,运行时记录每条分支频率、每个调用点的实际类型——这些数据喂回第三步,让 AOT 编译期也能做"基于剖面的优化"。

收益:吞吐回升 10%~20%,但仍无法达到 JIT 长跑峰值。根本原因——剖面数据是离线收集的,无法适应流量模式变化。

前沿方向:Oracle 在做 ML-PGO——用机器学习预测剖面,避免手工 instrument。仍在实验阶段。

# 9. 选型决策矩阵

# 9.1 适合的场景

✅ Serverless / FaaS 函数

  • AWS Lambda / 阿里云函数计算 / Knative
  • 收益:冷启动从秒级 → 毫秒级,省钱
  • 案例:本篇开篇

✅ CLI 工具 / 短生命周期任务

  • kubectl 风格的命令行工具
  • 数据处理脚本(一次性运行)
  • 收益:启动快、内存省、单二进制易分发

✅ 边缘计算 / 嵌入式

  • IoT 网关、车载、边缘节点
  • 收益:内存极低、不需要 JRE 部署

✅ Spring Boot 3 / Quarkus / Micronaut 现代框架

  • 框架已做 GraalVM 适配
  • 收益:享受云原生红利、容器密度提升 5~10 倍

# 9.2 不适合的场景

❌ 长跑高吞吐微服务

  • 每天处理亿级请求的核心交易系统
  • 损失:峰值吞吐 -10%~20%,单机成本反升
  • 替代方案:保留 JVM,调优 G1 / ZGC(见第 17 篇)

❌ 重度依赖动态特性的应用

  • OSGi 插件框架(Eclipse / 工业控制)
  • 字节码热替换(jrebel / 热部署)
  • 替代方案:放弃 Native Image,无解

❌ 生态尚未适配的老框架

  • Hibernate 5、Struts、老版本 Mybatis
  • 损失:手工写大量 reflect-config.json
  • 替代方案:等框架升级或迁移

❌ 大量 JNI / Native 互操作

  • JNI 在 SVM 下能用但限制多
  • 替代方案:用 Project Panama (FFM API)

# 9.3 迁移落地清单

如果决定上 Native Image,按这个清单走:

[ ] 1. JDK 升级到 17+(GraalVM 21.0+ 强依赖)
[ ] 2. 框架升级(Spring Boot 3.x / Quarkus / Micronaut 最新版)
[ ] 3. 单测 + 集成测试覆盖率 ≥ 70%(Agent 采集质量保障)
[ ] 4. 启用 Tracing Agent 跑全部测试用例:
       java -agentlib:native-image-agent=config-output-dir=...
[ ] 5. 检查项目里所有 ClassLoader / Class.forName / Proxy 调用
[ ] 6. 评估第三方依赖的 GraalVM 兼容性(spring.io/native)
[ ] 7. 在 CI 加 native-image 构建步骤(注意:构建机至少 8GB 内存)
[ ] 8. 灰度发布:单实例 → 10% → 50% → 全量
[ ] 9. 加监控:启动时间、内存 RSS、TPS、ClassNotFoundException 计数
[ ] 10. 回退方案:保留传统 jar 部署能力 6 个月
1
2
3
4
5
6
7
8
9
10
11

反向 checklist(满足任一项请放弃):

[ ] 业务严重依赖运行时字节码增强(JRebel / Mock / AOP 框架)
[ ] 团队没有 1 人有 GraalVM 经验
[ ] 框架版本无法升级到 GraalVM 适配版
[ ] 业务 SLA 对峰值吞吐敏感(<5% 偏差不可接受)
1
2
3
4

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章那场冷启动竞赛,逐条揭晓:

① AOT 为什么到 Java 才大火:不是 AOT 突然变好,是云原生 + 容器化 + 微服务三股潮流让"启动快 + 内存小"第一次成为业务一等公民。技术评价永远绑定场景。真相:参考 §2.3——同样的技术,2008 年是负担、2024 年是救星。

② GraalVM 与 OpenJDK / OracleJDK 的关系:GraalVM 不是 JDK 替代品,它是个技术栈——包含 Graal 编译器(可替代 C2)、Truffle 多语言、Native Image 三大组件。开源版(CE)功能受限,商业版(EE / Oracle GraalVM)有 G1 GC、PGO、ML-PGO 等高级特性。

③ 闭世界假设的灵魂地位:CWA 是 Native Image 一切能力(启动快 / 内存省 / 体积小)和一切限制(动态特性受限 / 构建慢)的唯一根源。理解 CWA 就理解了 80% 的 Native Image。

④ 反射 / 代理 / SPI / 资源加载的处境:全都需要配置(reflect-config.json / proxy-config.json / resource-config.json)——但有 Tracing Agent 自动采集兜底,只要测试覆盖率 ≥ 70% 基本能自动配齐。字节码生成(CGLIB / 运行时 ASM)则基本无解——必须改用 buildtime 代理。

⑤ 启动 100x 但峰值吞吐 -20% 的根因:启动收益来自"工作前移到构建期"——类加载、初始化、JIT 全部省掉;峰值吞吐损失来自"AOT 没有运行时剖面"——无法做激进内联、去虚化、推测优化。这是结构性的取舍,PGO 能弥补一半但弥补不全。

⑥ 构建时间膨胀 15 倍的去向:80% 花在静态分析迭代(5~10 轮固定点收敛)+ 整体编译(每个方法都要走完整 IR 优化管线)。对比 javac 只是"字节码生成"——Native Image 是"把 javac + JIT + linker 一次性全做完"。

⑦ 何时该用 / 不该用:参考 §9 决策矩阵——Serverless / CLI / 边缘计算选 Native Image,长跑高吞吐微服务选 JVM。这是清晰的二分。

# 10.2 一次构建的一生

把"一份 jar 变成原生二进制并跑起来"的完整时间线串成一棵树——回扣本册 17 篇:

T-30min   开发提交代码
          [13篇] javac → 字节码 .class
          
T-10min   CI 触发构建
          [17篇] 容器规格 8C16G(构建机内存要求高)
          
T 0       native-image 启动
          ↓
T+3s      [§5.1] Initializing:扫描 class path
          [02篇] 模拟 BootClassLoader 行为,但是构建期
          
T+5s      [§5.2] Performing analysis 第一轮
          从 main 出发遍历调用图
          [04篇] 容器引用解析(HashMap / ArrayList 都进可达集)
          
T+25s     第二轮分析
          发现 reflect-config.json 中的类
          [07篇] 反射目标加入可达图
          
T+78s     固定点收敛 (第 6 轮)
          12856 个类可达,其余裁剪
          [04/06篇] 大量泛型类被擦除合并
          
T+90s     [§5.3] 构建期初始化
          执行选定类的 <clinit>
          [01篇] 创建对象到 image_heap(构建期堆)
          [05篇] String 常量池预填充
          
T+105s    [§5.4] Compiling 阶段
          [14篇] Graal IR → 机器码(与 JIT 同一引擎)
          每个方法走完整 inline / EA / 标量替换
          [14篇] 所有可能的优化在构建期一次完成
          
T+200s    Layouting + Creating image
          .text / .rodata / .image_heap 段写入 ELF
          [15篇] 调试符号写入(运行期可用 perf 看)
          
T+230s    构建完成,输出 80MB 的 app 二进制
          
═══════════ 镜像产物部署 ═══════════
          
T 0       OS exec ./app
          [01篇] 内核 mmap 镜像各段到虚拟内存
          
T+3ms     [§6] SubstrateVM 入口启动
          初始化 GC、信号处理、线程池
          
T+8ms     [§5.3] image_heap mmap 到位
          构建期创建的对象"瞬间"可见
          
T+15ms    用户 main 方法执行
          [09/10篇] Spring 容器"快速恢复"——
          因为 BeanDefinition 已经构建期固化
          
T+30ms    [11篇] HTTP 端口监听就绪
          
T+38ms    第一个请求到达
          直接走机器码,无解释执行阶段
          [16篇] 不会发生 GC overhead,因为堆很小
          
事故时    某条罕见路径触发 Class.forName("X")
          但 X 没在 reflect-config.json 里
          → ClassNotFoundException
          → SubstrateVM 抛错并打印
          [16篇] 没有 OOM 但有"配置 OOM"
            ↓
          补 reflect-config,重新构建,重新部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

这条时间线串起本册 80% 的关键概念——Native Image 的每一项收益都对应着 JVM 的某一项开销被前移或裁剪。理解了这一点,AOT 就不再是黑魔法。

# 10.3 设计哲学回扣

跳出技术细节,提炼三条贯穿本篇的设计哲学:

  1. 取舍即设计:Native Image 不是"更好的 JVM",它是"用动态性换启动速度"的另一种取舍。没有更好,只有更适合——同样的代码,Serverless 场景选 Native Image 一年省百万、长跑微服务选 Native Image 一年损失千万。这条规律在第 03 篇 GC 算法选型、第 11 篇 IO 模型演进、第 17 篇 G1 vs ZGC 都见过。结论:所有架构决策本质都是取舍,承认取舍才能做对决策。

  2. 工作前移是最大优化:Native Image 100x 启动提升的根因不是"运行得更快"——是"很多事情根本不在运行期做"。类加载、初始化、JIT 编译全部前移到构建期。这与第 05 篇 String 常量池(编译期固化)、第 13 篇 invokedynamic(首次解析后缓存)、第 14 篇 JIT 编译(首次后机器码缓存)是同一根思想——"做一次缓存到底" 永远比"每次都做"快。结论:启动优化的终极方向是把工作搬到非启动期。

  3. 静态可达性是闭世界的灵魂:Native Image 一切能力(裁剪 / 启动快 / 内存省)和一切限制(反射要配置 / 字节码生成不可用)都源自"编译期能枚举所有可能"这一假设。这与第 02 篇双亲委派模型的反面正好——双亲委派是"开放可加载",Native Image 是"封闭已固化"。两种范式各自映射不同的业务诉求。结论:理解一项技术的边界,比记住它的特性更重要。

# 10.4 对决速查表

最后一张表,建议截图保存——JVM vs Native Image 全维度对决:

维度 传统 JVM Native Image 谁赢
启动时间 秒级 毫秒级 🏆 Native
基线内存 数百 MB 数十 MB 🏆 Native
镜像体积 jar+JRE >100MB 单二进制 50~100MB 🏆 Native
峰值吞吐 100% (基准) 80%~93%(PGO) 🏆 JVM
启动后峰值时间 1~2 分钟预热 立刻达稳态 🏆 Native
构建时间 秒级 分钟级 🏆 JVM
构建机内存 <2GB 4~8GB 🏆 JVM
反射支持 完整 需配置 🏆 JVM
字节码生成 完整 不支持 🏆 JVM
跨平台 一次编译跨平台 平台绑定 🏆 JVM
调试体验 成熟 (JDWP) 弱(gdb) 🏆 JVM
部署形态 需要 JRE 单二进制 🏆 Native
容器密度 中 高(5~10x) 🏆 Native

选型心法三条:

1. 启动 / 内存敏感场景 → Native Image
   (Serverless / CLI / 边缘 / 现代云原生框架)
   
2. 吞吐 / 灵活性敏感场景 → 传统 JVM
   (长跑微服务 / 大数据 / 老框架 / 重动态性)
   
3. 都不敏感 → 默认传统 JVM
   (生态成熟、运维门槛低、问题可查)
1
2
3
4
5
6
7
8

至此卷一 JVM 与运行时核心画上句号——从 01 篇内存模型一路走到 18 篇 AOT 革命,我们把"虚拟机如何把字节码跑起来"这件事拆透了。但 JVM 之上跑的是Java 集合框架——HashMap / ArrayList / ConcurrentHashMap 这些容器才是业务代码每天打交道的对象。下一篇我们顺着"GraalVM 静态分析时为什么 ArrayList 会被疯狂裁剪?"这条线,进入 卷二第一篇——第 19 篇:ArrayList 与 LinkedList 源码深析——把动态扩容、fail-fast 迭代器、modCount、为什么 LinkedList 几乎被弃用一次讲透。

上次更新: 2026/06/10, 11:13:41
JVM参数调优全景图
HashMap底层哈希设计

← JVM参数调优全景图 HashMap底层哈希设计→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式