引言
WASM 最初为 C/C++/Rust 设计——它们用线性内存自己管理一切,天然适合 WASM 的底层模型。但 Java、C#、Kotlin 这类托管语言依赖垃圾回收(GC)管理对象生命周期,直接编译到 WASM 会撞上「没有 GC 指令」的墙:要么把整个运行时(含 GC)编进去(体积爆炸、性能差),要么用线性内存手动模拟(别扭且易错)。WasmGC 提案从根上解决:让 WASM 原生支持 GC 对象与回收,托管语言可以干净地编译成 WASM。本文把 WasmGC 的提案动机、引用类型、指令集、运行时集成与各语言编译路径一次讲透。
前置:/wasm-introduction-architecture/(内存模型)、/wasm-rust-compilation-guide/(编译工具链)、/wasm-component-model-wit/(类型系统演进)。
目录
- 1. 为什么托管语言需要 GC 提案
- 2. 引用类型与结构类型
- 3. i31ref 与整型装箱
- 4. GC 指令集与分配
- 5. 与线性内存的对比
- 6. 宿主运行时的 GC 集成
- 7. 各语言的编译路径
- 8. 提案状态与支持矩阵
- 9. 性能开销与优化方向
- 10. 速查表与一句话记忆
- 延伸阅读
1. 为什么托管语言需要 GC 提案
托管语言的对象模型依赖「自动回收无用对象」——这是编译器假设的基础设施。但 WASM 最初的指令集里没有任何「分配对象」「读取字段」「回收」的语义:
WASM 初始模型:
□ 线性内存(memory):可读写字节,但「对象」概念不存在
□ 只能手动管理:malloc/free,或用值栈传简单类型
□ 托管语言直接编译 → 需要自带 GC(体积巨大、难以优化)
WasmGC 提案:
□ 新增引用类型(struct/array/func/extern)
□ 新增 GC 指令(struct.new、array.get、ref.cast…)
□ 分配/回收交给宿主(浏览器/运行时)的 GC
为什么重要:GC 提案让 Java/C#/Kotlin/Swift/Dart 等语言能以「干净」的方式编译到 WASM——对象直接映射为 WASM 引用,GC 由宿主负责,语言运行时大幅缩小。这是 WASM 从「系统语言专属」走向「通用语言目标」的关键一步。
工程定位:GC 提案不是让现有 WASM 变慢,而是让原本「编不进来」的语言变得可行——它扩展的是 WASM 的「语言覆盖」,而非替代线性内存。
2. 引用类型与结构类型
WasmGC 引入的核心类型:引用类型(reference types),与标量(i32/i64/f32/f64)平级:
| 类型 | 含义 | 典型用途 |
|---|---|---|
funcref | 函数引用 | 闭包、回调、多态(对应 call_indirect) |
externref | 外部宿主对象引用 | 传 JS/宿主对象,不参与 WASM 内存 |
(ref null $t) / (ref $t) | 到堆对象的可空/不可空引用 | GC 对象的指针 |
(struct $T) | 结构体(字段集合) | 对象的实例 |
(array $T) | 数组(元素类型固定) | 数组/集合 |
;; 声明一个结构类型(类似 C 的 struct / 类的字段布局)
(type $Point (struct (field $x (mut i32)) (field $y (mut i32))))
;; 声明一个数组类型
(type $Ints (array (mut i32)))
关键认知:
- struct/array 是「类型化的堆对象」:分配后宿主在 GC 堆管理,WASM 只持有引用;
- 引用与线性内存指针不同:引用不可「算术运算」(不能 +1、不能解引用成整数),由指令专门操作;
- mut 与不可变:字段可标
(mut i32)(可写)或i32(不可写,利于优化)。
3. i31ref 与整型装箱
GC 对象里的小整数(如 int、枚举、布尔)如果都装箱成堆对象,内存与分配开销巨大。i31ref(31-bit 整数引用)是经典优化:
i31ref:把 31 位整数直接编码进「引用」本身(tag 指针/立即数)
→ 小整数不用真的分配堆对象
→ 存储与读取零分配、零 GC 压力
编码技巧(常见实现):
□ 指针标记:用最低 1 位标记「这是 i31」而非真指针
□ 读写:ref.i31 → 直接取整数;i31.ref → 检查范围后装箱
;; 装箱:把 i32 变成 i31 引用
i32.const 42
ref.i31 ;; 生成一个 i31ref(可能直接是立即数)
;; 拆箱:取回整数
ref.i31
i31.get_s ;; 得到带符号的 31 位整数
工程意义:编译器把「小整数字段」映射成 i31ref,避免为每个小整数分配对象——GC 语言编译到 WASM 的性能关键就在这类优化。
4. GC 指令集与分配
WasmGC 的指令分几类:
分配:struct.new $T / struct.new_default $T / array.new $T
读取:struct.get $T $f / array.get $T / array.len
写入:struct.set $T $f / array.set $T(需 mut 字段)
判断:ref.test (ref null $T) / ref.cast (ref null $T)
分支:br_on_cast / br_on_non_null / br_on_null
比较:ref.eq(同引用相等)
;; 创建并写入一个 Point 对象
(func $make_point (result (ref $Point))
i32.const 10
i32.const 20
struct.new $Point ;; 分配 + 初始化两个字段
;; 栈顶现在是 (ref $Point)
return)
;; 读取字段 + 类型收缩
(func $get_x (param $p (ref $Point)) (result i32)
local.get $p
struct.get $Point $x ;; 读取 x 字段
return)
关键认知:
- 分配即结构化:
struct.new直接按类型布局分配,无「先 malloc 再手动布置」; - 类型收缩
ref.cast:把(ref null $Base)安全缩成(ref $Derived),配合br_on_cast做多态分发; - 编译器视角:GC 指令让「对象模型」直接落到 WASM,语言运行时不再需要自己造对象堆。
5. 与线性内存的对比
GC 对象在 GC 堆(宿主管理),与线性内存(WASM 手动管理)是两条轨道:
| 维度 | 线性内存 | GC 堆(WasmGC) |
|---|---|---|
| 分配 | memory.grow + 手动分配器 | struct.new / array.new |
| 生命周期 | 手动 free / 作用域管理 | 宿主 GC 自动回收 |
| 表示 | 整数指针(可算术) | 引用(不可算术) |
| 遍历 | 任意读写字节 | 按类型读取(struct/array) |
| 适用 | C/C++/Rust、缓冲区、二进制 | Java/C#/Kotlin 对象图 |
典型布局:
[线性内存]──────► 大缓冲区、字节流、FFI 数据
[GC 堆]────────► 语言级对象、集合、闭包(引用相连)
桥梁:externref / 显式拷贝(把 GC 对象拷进线性内存做零拷贝 FFI)
工程要点:两者可共存——同一模块既能用线性内存做高性能字节处理,也能用 GC 堆承载托管语言对象。选择取决于「谁拥有数据、谁管理生命周期」。
6. 宿主运行时的 GC 集成
分配与回收由宿主(浏览器引擎或独立运行时)的 GC 负责:
WASM 模块发出 struct.new
→ 宿主 GC 在托管堆分配
→ 返回引用给 WASM
→ 对象不再被引用时,宿主 GC 回收
浏览器:V8(Chrome/Node)、Spidermonkey(Firefox)、JavaScriptCore
独立:Wasmtime(wasm-gc 实验)、Wasmer、WAMR(轻量)
集成的关键点:
- 根(roots):WASM 栈/全局里的引用是 GC 根,宿主必须正确标记;
- 跨边界引用:
externref传出的对象要「钉住」防止被回收; - 帧栈:多线程/栈切换时引用根随协程移动(关联提案,见 /wasm-exceptions-stack-switching/)。
工程要点:宿主 GC 的停顿/分配速度决定体验——浏览器引擎为网页优化,独立运行时可配参数;语言运行时不应重复实现 GC,把「谁回收」交给宿主是 WasmGC 的架构原则。
7. 各语言的编译路径
WasmGC 让多种托管语言具备「干净」编译路径:
| 语言 | 编译路径 | 状态 |
|---|---|---|
| Java | JVM → WASM(GraalVM/Soy 等实验性后端) | 实验/进展中 |
| Kotlin | Kotlin/Native → WASM(Kotlin/Wasm 官方支持) | 官方推进 |
| C# / .NET | Mono/Runtime → WASM(Blazor 服务器端 + GC 提案) | 官方路线 |
| Dart | Dart → WASM(Flutter Web 的 wasm 后端) | 官方支持 |
| Swift | SwiftWasm 探索 GC 路径 | 实验 |
| OCaml / Haskell | 函数式语言实验后端 | 实验 |
以 Kotlin/Wasm 为例:
Kotlin 源码 → Kotlin/Native 编译器 → WASM + GC 指令
→ 对象映射为 GC 引用,字符串/集合走 WASM GC 堆
→ 浏览器运行:无需把 JVM 编进去,产物大幅减小
工程定位:WasmGC 的目标不是「让每种语言无脑编译」,而是让语言运行时可以很薄——对象的分配回收交给宿主,语言只需保留自己的类型系统与语义。这条路走通,WASM 才能成为「通用编译目标」。
8. 提案状态与支持矩阵
WasmGC 已进入稳定化,主流引擎逐步支持:
提案状态(截至 2026):
□ Phase 4(标准化):引用类型、GC 指令核心
□ 浏览器:V8 / Spidermonkey 已默认支持或 flag 开启
□ 独立运行时:Wasmtime 实验支持,Wasmer 部分支持
□ 工具链:wasm-tools / cargo-component / Kotlin 工具链跟进
# 在 V8(Node)里启用 wasm-gc(示例,版本相关)
node --experimental-wasm-gc app.mjs
工程要点:
- 检查运行时支持:部署前在目标浏览器/运行时确认 GC 指令可用(特性检测);
- 回退方案:不支持 GC 的环境退回「自带运行时」版本(体积大但可用);
- 关注工具链:wasm-tools 的 validate/dump 对 GC 类型的支持是最新验证手段。
9. 性能开销与优化方向
GC 语言编到 WASM 的性能「天花板」取决于几个因素:
主要开销:
□ 分配频率:每次 struct.new 走宿主 GC(比手动快但非零)
□ 装箱:小整数用 i31ref 避免堆分配(编译器优化关键)
□ 多态:ref.cast / br_on_cast 的分发成本
□ GC 停顿:宿主 GC 全局停顿(大堆时明显)
优化方向:
□ 编译期消除装箱(i31ref / 标量替换)
□ 减少不必要的分配(escape analysis)
□ 与线性内存结合:热路径缓冲区走线性内存
□ 选择 GC 友好的运行时参数(增量/分代 GC)
经验基线:GC 语言 WASM 产物 ≈ 原生 JIT 的 60~90% 性能
(视语言、工作量与宿主 GC 而定),但远优于「自带 GC 运行时」的方案。
工程要点:优化重点不是「让 WASM 更快」,而是让语言运行时薄——编译器把对象模型正确落到 GC 指令,宿主 GC 做剩下的。工具链(编译优化 + GC 指令生成质量)是当前最大提升空间。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 为什么需要 WasmGC | 托管语言(Java/C#/Kotlin)依赖 GC,WASM 原生不支持 |
| 新增什么类型 | 引用类型:struct/array/funcref/externref/i31ref |
| 怎么分配对象 | struct.new / array.new,宿主 GC 管理 |
| 与线性内存区别 | GC 堆不可算术、自动回收;线性内存手动管理 |
| 谁负责回收 | 宿主(浏览器/运行时)的 GC |
| 各语言怎么用 | Kotlin/Wasm 官方、.NET/Dart 官方推进 |
| 性能怎么看 | 依赖装箱消除与宿主 GC,远优于自带运行时 |
一句话记忆:WasmGC = 让 WASM 原生支持「引用 + 分配 + 回收」——托管语言(Java/C#/Kotlin)不再自带运行时,对象走 GC 堆(struct/array/i31ref),回收交给宿主——WASM 从「系统语言专属」走向「通用语言目标」。
延伸阅读
- /wasm-introduction-architecture/ — WASM 内存模型与模块结构
- /wasm-rust-compilation-guide/ — Rust 编译到 WASM(无需 GC 的路径)
- /wasm-component-model-wit/ — 类型系统与组件互操作
- /wasm-exceptions-stack-switching/ — 异常与栈切换提案(引用根随协程移动)
- /wasm-multithreading-sharedarraybuffer/ — 多线程下的对象与共享内存
- Rust 专题 — 系统语言编译路径对比
- Java 专题 — JVM 与 GraalVM 的 WASM 后端
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。