6.3 垃圾回收集成 本节摘要:垃圾回收提案把托管类型(结构体、数组、引用)写进了 Wasm 规范,带 GC 的语言从此不必在线性内存里自建堆;模块对象与宿主对象之间也从序列化隧道升级为引用直通。本节讲清类型系统的新成员、双堆协同的架构,以及"谁受益、谁无感"的采用判断。 别以为垃圾回收提案只是"给 Wasm 加个 GC"这么简单——它改写的是字节码的表达力边界:MVP 的世界里万物皆数字(线性内存里的字节与栈上的标量),GC 之后的 Wasm 第一次拥有了"对象"这个概念。对托管语言,这是从"勉强能编译"到"高效编译"的分水岭。
本节摘要:垃圾回收提案把托管类型(结构体、数组、引用)写进了 Wasm 规范,带 GC 的语言从此不必在线性内存里自建堆;模块对象与宿主对象之间也从序列化隧道升级为引用直通。本节讲清类型系统的新成员、双堆协同的架构,以及"谁受益、谁无感"的采用判断。
别以为垃圾回收提案只是"给 Wasm 加个 GC"这么简单——它改写的是字节码的表达力边界:MVP 的世界里万物皆数字(线性内存里的字节与栈上的标量),GC 之后的 Wasm 第一次拥有了"对象"这个概念。对托管语言,这是从"勉强能编译"到"高效编译"的分水岭。
GC 提案之前,Java、Kotlin、Dart 这类语言编译到 Wasm 的方式相当拧巴:语言运行时(虚拟机加垃圾回收器)整体被编译进模块,对象的内存管理在线性内存里自建一套堆——通常叫 outbound heap。这套"字节码里再跑一个虚拟机"的结构带来三重税:模块体积动辄数 MB(运行时自身占大头),启动要先初始化自建堆,回收器还要与宿主环境的 GC 各管各堆、互不知情。将就着能用,但离"一等公民"差得远。
GC 提案的解法是把回收职责上移:Wasm 虚拟机(引擎)自己管理托管对象的生命周期,语言编译器只管产出托管类型的指令——对象分配、字段读写、引用比较都有专属指令族,回收由引擎统一调度。语言运行时从模块里整个消失,体积、启动、双堆摩擦三重税一起免掉。
GC 提案给类型系统添的成员围绕引用展开:结构体引用(structref)指向带字段布局的复合对象,数组引用(arrayref)指向定长元素序列,函数与外部对象引用(funcref、externref)此前已有,另有一个精巧的 i31ref——把小整数压缩进引用位里当即时数用,服务那些"用对象表示小值"的语言习惯。
这套类型落进运行时,Wasm 的内存格局从单堆变成双堆:线性内存依旧是字节平原,手写内存的语言(C、C++、Rust)照旧在那里生活;托管堆由引擎管理,分配与回收走指令族,模块自身看不见它的物理布局。两堆各有居民、互不侵犯:
// Kotlin/Wasm:对象直接分配在托管堆,编译产物不再携带运行时 data class Order(val id: Int, val amount: Double) @JsExport fun topAmounts(orders: Array<Order>): Double { // Array 与 Order 都是 Wasm 托管对象,回收由引擎负责 return orders.maxOf { it.amount } }
Kotlin 的这个例子浓缩了提案的价值主张:编译器不再把整个 Kotlin 运行时打进产物,数组与对象直接使用 Wasm 托管类型,与 JavaScript 交换参数时也是引用直通——没有序列化、没有拷贝,跨边界传对象像同语言内传对象一样自然。

受益排序相当清晰。第一梯队是托管语言的编译产物:Kotlin/Wasm、Dart 与 Flutter 的 Web 目标、Java 的 GraalVM Wasm 后端——它们是提案的直接服务对象,体积与启动的收益立竿见影。第二梯队是宿主互操作密集的应用:引用直通让"对象频繁过境"的架构不再昂贵,第五章的过境成本账本在托管类型这里改写。第三梯队才是 C 与 Rust 这类手写内存的语言——它们的常规代码与线性内存绑定,短期无感;但组件模型落地后,GC 类型预计成为接口描述的一等公民,跨语言边界上的新机会值得留意。
采用的现实约束也要摆在台面上:浏览器支持已就绪,独立运行时的落地进度参差,服务端引擎对 GC 的完整支持要逐家核对版本;工具链侧,各语言编译器的成熟度同样在快速追赶期。与第五章讲过的分层采用策略呼应:浏览器目标可以先行,服务端目标等支持矩阵追平再动。
引擎接手回收之后,性能画像里多了一个需要理解的变量:回收停顿的时机与时长由引擎调度,应用层不再可控。这不是退步——语言自带回收器时代,停顿同样存在且更不可预测——但观测与调优的抓手换了位置:过去调语言的回收参数,如今看引擎的回收统计与调度策略。
工程上的应对有三板斧。其一,观测先行:把引擎暴露的回收计数与停顿时长接入监控,与延迟分位一起看——停顿尖刺与请求尾延迟的相关性一查便知。其二,分配节流:托管对象的创建频率是停顿频率的根,热点路径上的临时对象能复用则复用、能展开则展开,这条纪律与任何 GC 语言的优化常识相通。其三,场景对齐:交互敏感的场景选停顿友好的引擎配置,吞吐敏感的批处理场景则不必为几毫秒停顿纠结。把回收当部署参数而非宿命,是托管语言团队上船后的第一课。
托管对象在指令层的样子可以略窥一斑——结构体的分配与字段访问都有专属指令族:
(type $point (struct (field $x f64) (field $y f64))) (func $make (param $x f64) (param $y f64) (result (ref $point)) (struct.new $point (local.get $x) (local.get $y))) (func $get_x (param $p (ref $point)) (result f64) (struct.get $point $x (local.get $p)))
分配不再走线性内存的手工布局,字段访问不再是指针算术——这些指令由引擎兑现到托管堆上。对托管语言的编译器,这是从"翻译成数字把戏"到"直译"的解放。
表达力补完,下一节跨出浏览器:WASI 如何给字节发"系统能力"的签证。