Dart 虚拟机(Dart VM)
Dart 虚拟机(Dart VM)的组成与运行逻辑在很多方面都与传统虚拟机(如 JVM)有所不同。它不仅是一个简单的字节码解释器,更是一个灵活的执行环境,能够支持 JIT ( 即时编译 ) 和 AOT (提前编译) 两种截然不同的流水线。
以下是 Dart 虚拟机的核心组成部分及其与传统虚拟机的不同之处:
核心组成部分
- 隔离体 (Isolates) :这是 Dart VM 最独特的组成部分。与传统虚拟机中多个线程共享内存不同,Isolates 之间不共享任何 内存。每个 Isolate 都有自己的堆和执行线程。
- 运行时系统 (Runtime System) :提供执行 Dart 代码所需的基础设施,包括垃圾回收 器 (Garbage Collector) 和 Dart 核心库。
- 快照机制 (Snapshots) :Dart VM 可以将堆的状态(包括代码指令和数据)序列化为快照文件(如
vm_snapshot_data和isolate_snapshot_data)。这允许虚拟机通过直接加载这些数据来快速启动,而无需重新解析源代码。 - 事件循环 (Event Loop) :Dart 是单线程执行模型(在单个 Isolate 内),通过事件循环来调度异步任务和处理 Future 回调。
- 中间表示 层 (Kernel AST ) :无论是在 JIT 还是 AOT 模式下,Dart 代码首先都会被转换为一种称为 Kernel AST 的中间表示形式,然后再进一步转换为机器码。
与传统虚拟机的不同之处
-
内存 模型:无共享 (Shared-nothing)
- 传统 虚拟机:通常使用共享内存的多线程模型,通过“锁(Locks)”来同步线程,这往往会导致性能瓶颈和复杂的同步问题。
- Dart VM:由于使用 Isolate 模型,内存不共享,因此垃圾回收 器在分配内存时无需加锁,从而实现了极速的内存分配和高效的任务调度。Isolate 之间只能通过异步消息进行通信。
-
双流水线编译架构
- 传统 虚拟机:大多侧重于一种模式(如 JVM 侧重于 JIT,或纯解释执行)。
- Dart VM:具备 JIT 和 AOT 双流水线。在开发阶段,它使用 JIT 来支持“热重载(Hot Reload)”,即动态地将修改后的代码注入运行中的 VM。而在发布阶段,它允许将代码预编译成二进制机器码(AOT),此时 VM 更多地充当一个轻量级的“运行时系统”,提供垃圾回收和原生方法支持,而不再进行运行时编译。
-
启动优化:从数据块直接执行
- 传统 虚拟机:启动时通常需要加载大量的类文件并进行解析。
- Dart VM:利用 Snapshot(快照) 技术。特别是在 AOT 模式下,生成的
vm_snapshot_instr和isolate_snapshot_instr实际上是二进制 指令 数据。运行时,操作系统直接将这块内存加载并赋予执行权限,实现了“极速启动”。
-
峰值性能的演进
- 在长时运行的服务端应用中,Dart VM 的 JIT 模式由于拥有运行时性能分析数据,有时能比 AOT 做出更激进的优化,其峰值性能甚至可能超过 AOT。而 AOT 模式则因为无法获取运行时信息,只能根据磁盘上的代码进行静态的“最佳猜测”优化。
总结:Dart 虚拟机是一个高度模块化的环境,它通过 Isolate 隔离模型 解决了多线程竞争问题,并利用 Snapshot 和双编译流水线 在开发效率(热重载)与运行性能(AOT 高效执行)之间取得了平衡。
JIT 和 AOT
在 Flutter 中,Dart 虚拟机利用其独特的架构设计,支持 JIT ( 即时编译 ) 和 AOT (提前编译) 两种互补的流水线,分别服务于开发和生产需求。以下是基于提供的源代码对这两者流程及差异的深度总结:
JIT (Just-In-Time) 编译流程
JIT 主要用于 Debug 模式,核心目标是提升开发效率。
-
中间转换:Dart 源代码(含应用和框架代码)首先转换为 Kernel AST 这一中间表示形式。
-
打包与分发:代码被编译为
kernel_blob.bin,连同用于加速启动的isolate_snapshot_data等数据打包在应用的资源目录中。 -
运行时执行:应用启动后,Dart 虚拟机加载这些中间代码,并在运行时动态地将其编译为机器码执行。
-
核心特性:
- 热重载 (Hot Reload) :由于是运行时动态编译,修改后的代码可以快速下发并注入运行中的 VM,无需重新安装应用。
- 缺点:由于需要运行时的编译和“预热”,启动速度相对较慢,且因为虚拟机本身的开销会占用更多内存。
AOT (Ahead-Of-Time) 编译流程
AOT 主要用于 Release 模式,侧重于运行时的极致性能与用户体验。
-
静态编译:在应用发布前,使用
gen_snapshot工具将代码直接编译为针对特定平台(如 ARM)的汇编文件,最终生成二进制机器码。 -
指令 化产物:生成的产物(如
snapshot_instr)实质上是二进制指令数据。 -
直接执行:运行时,操作系统直接将这块二进制指令加载到内存并赋予执行权限,无需 虚拟机 在运行时进行转换。
-
核心特性:
- 性能卓越:具备极速启动能力,且在图形渲染等高负载场景下表现更佳。
- 缺点:失去了动态性,不支持热重载;且需为不同架构(arm64, x86_64)生成独立代码,导致安装包体积增加。
JIT 与 AOT 的核心差异对比
| 维度 | JIT (Debug 模式) | AOT (Release 模式) |
|---|---|---|
| 编译时机 | 应用运行时(即时) | 应用运行前(提前) |
| 执行载体 | 需要 Dart VM 动态转换 | 直接运行二进制机器码 |
| 启动优化 | 需解析 Kernel 代码,启动慢 | 利用 Snapshot 指令直接执行,启动极快 |
| 功能权衡 | 支持 热重载,迭代快 | 无热重载,但提供高性能和高安全性 |
| 优化深度 | 可利用运行时数据进行激进优化,峰值性能甚至可能超过 AOT | 仅能根据磁盘代码进行静态“最佳猜测”优化 |
| 链接方式 | 运行时动态链接 | 编译阶段完成符号表链接和重定向 |
虚拟机底层的关键支撑
无论是 JIT 还是 AOT,其运行效率都受益于 Dart VM 的独特设计:
- Isolate 隔离模型:每个 Isolate 之间不 共享内存,这使得垃圾回收器在分配内存时无需加锁,从而实现了极速的内存分配。
- 双流水线架构:通过 Kernel AST 统一了编译的前端过程,使 Dart 能够灵活地在开发期的效率(JIT)和生产期的性能(AOT)之间取得平衡。