WebAssembly 线性内存是如何工作的?
WebAssembly 的内存模型可以先记成一句话:它把模块能访问的数据放进一段连续的线性内存里,JavaScript 通过 ArrayBuffer 视图和它交换数据。Wasm 不能随便读宿主环境的内存,也不能直接碰 DOM,这种隔离既是安全边界,也是很多互操作问题的来源。
线性内存是什么
WebAssembly.Memory 代表一段可增长的字节数组,单位是 page,每页固定 64KB。模块里的 load、store 指令使用整数地址访问这段空间,地址从 0 开始。C、C++、Rust 编译到 Wasm 后,堆、栈、字符串、数组最终都会落在这段线性内存中,只是具体布局由编译器和运行时决定。
javascriptconst memory = new WebAssembly.Memory({ initial: 10, maximum: 100 }); const u8 = new Uint8Array(memory.buffer); u8[0] = 42; console.log(u8[0]);
JavaScript 不能直接拿到 C 里的字符串对象,它拿到的是偏移量和长度。比如 Wasm 返回 ptr=1024、len=5,JS 需要用 new Uint8Array(memory.buffer, ptr, len) 读取,再用 TextDecoder 转成字符串。
内存为什么相对安全
Wasm 的每次内存访问都会做边界检查,越界会 trap,而不是像传统原生程序那样覆盖宿主进程的任意地址。模块运行在沙盒中,只能访问自己导入或导出的资源。这个模型让浏览器敢执行来自网络的二进制代码,但它不等于业务层绝对安全:如果你把不可信数据传给有漏洞的解析器,解析器仍可能在 Wasm 内部崩溃或产生错误结果。
grow 的隐藏成本
内存可以用 memory.grow(n) 增长 n 页,但增长后 memory.buffer 可能被替换,旧的 TypedArray 视图会失效。很多线上 bug 就出在这里:初始化时缓存了一个 Uint8Array,后来 Wasm 扩容,JS 还在写旧视图,数据看起来像“莫名其妙丢了”。
javascriptconst view = new Uint8Array(memory.buffer); memory.grow(1); const freshView = new Uint8Array(memory.buffer);
因此大数据场景最好预估初始内存,少频繁 grow;必须 grow 时,JS 侧要重新创建视图。
在工程里还要区分“内存安全”和“内存好用”。Wasm 能阻止越界访问逃出沙盒,但不会帮你自动设计对象生命周期。谁负责分配输入缓冲区、谁负责释放输出缓冲区、错误时是否也释放,都要在 JS 包装层里写清楚。否则功能测试很容易通过,长时间批量处理时才暴露内存持续上涨的问题。
调试内存问题时,不要只盯着 Wasm 文件本身。JS 包装层缓存的视图、未释放的 Blob、Worker 没有终止,也会让整体内存居高不下。浏览器任务管理器看到的是页面总占用,需要结合 Performance、Memory 面板和模块自己的分配日志一起看。线上最好对大文件处理设置上限,避免一次分配把标签页打崩。
追问
WebAssembly 的内存和 JavaScript 堆是同一块吗?
不是。JavaScript 对象生活在 JS 引擎管理的堆里,Wasm 的线性内存是一个独立的 ArrayBuffer。两边可以通过 TypedArray 读写同一段 Wasm 内存,但 JS 对象本身不会自动出现在 Wasm 里。取舍是共享字节数据很高效,共享复杂对象很麻烦。踩坑点是把对象序列化、反序列化做得太频繁,性能收益会被抹掉。
为什么字符串传递这么麻烦?
因为 Wasm 只认识数字和内存地址,不认识 JavaScript 的字符串对象。通常需要把字符串编码成 UTF-8 字节,写入 Wasm 内存,再把指针和长度传给导出函数。边界在于编码、内存分配和释放都要约定清楚。常见坑是 Wasm 分配的字符串由 JS 读取后没有调用释放函数,长时间运行会出现内存上涨。
memory.grow 为什么会让旧视图失效?
增长内存可能需要创建新的底层 buffer,JS 之前创建的 Uint8Array 仍指向旧 buffer。这样不会总是立刻报错,但读写结果已经不是你想要的那块内存。稳妥做法是在可能扩容之后重新取 memory.buffer。如果项目对延迟敏感,应尽量初始化时给足内存,避免运行中扩容造成抖动。
Wasm 有垃圾回收吗?
传统 MVP 形态的 Wasm 没有像 JavaScript 那样的通用 GC,C/C++ 往往靠 malloc/free,Rust 靠所有权和分配器。新一代 Wasm GC 正在改善托管语言的支持,但不是所有运行环境都可依赖。取舍是手动或半自动内存管理能带来可控性能,也带来泄漏风险。特别是 JS 与 Wasm 之间传递指针时,谁分配、谁释放必须写成接口契约。
SharedArrayBuffer 能和 Wasm 一起用吗?
可以,在需要多线程或 Worker 协作时会用到共享内存。浏览器侧通常要求跨源隔离相关响应头,否则 SharedArrayBuffer 不可用。它的优势是减少复制,边界是并发读写要处理同步和原子操作。踩坑点是把共享内存当普通数组用,出现竞态后问题很难复现。