Emscripten/WebAssembly内存分配后的正确释放方法咨询
Emscripten Wasm内存泄漏问题:正确的内存释放方式
问题场景
我用Emscripten搭建了C转WebAssembly的项目,正在测试JS和C之间的内存交互逻辑:JS通过_malloc分配字节数组内存,传给C++代码修改,最后JS接收修改后的数据。但每次点击按钮触发操作,浏览器内存都会持续增长(图像数据大小1.4MB)。
现有JS代码
let worker = null; window.onload = function () { worker = new Worker('worker.js'); drawImage(); const btn = document.querySelector('.btn'); btn.addEventListener('click', clickHandler); } function clickHandler() { console.log('clicked'); //for (var i = 0; i < 100; ++i) test(); } function test() { const imageData = getCanvasImage(); const buffer = imageData.data.buffer; const bufSize = imageData.width * imageData.height * 4; const dataPtr = Module._malloc(bufSize); const dataOnHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, bufSize); dataOnHeap.set(new Uint8Array(buffer)) worker.onerror = function (error) { console.error(error); } worker.onmessage = function (event) { console.log('result: \ncall id: ' + event.data.callbackId + '.\n response data: '); const arr = new Uint8Array(event.data.data, length=bufSize); console.log(arr); // output is correct Module._free(dataOnHeap.byteOffset); }; const clientMessage = { callbackId: 1, funcName: 'TestMemory', data: dataOnHeap }; worker.postMessage(clientMessage); //Module._free(dataPtr); }
现有C++代码
extern "C" EMSCRIPTEN_KEEPALIVE void TestMemory(unsigned char* data, size_t size) { std::cout << "TestMemory begin" << std::endl; std::cout << "size: " << size << std::endl; std::cout << "data: "; for (size_t i = 0; i < size; ++i) { std::cout << static_cast<int>(*(data + i)) << " "; } std::cout << std::endl; // modify test for (size_t i = 0; i < size; ++i) { *(data + i) = *(data + i) / 2; } emscripten_worker_respond(reinterpret_cast<char* >(data), size); //free(data); std::cout << "TestMemory end" << std::endl; }
内存泄漏原因
- 重复绑定Worker回调:每次调用
test()都会给worker重新绑定onmessage和onerror,旧的回调函数不会被销毁,间接引用之前分配的Wasm内存和相关对象,导致GC无法回收。 - 固定请求ID导致回调覆盖:一直用
callbackId: 1,多次快速点击时,后续回调会覆盖前一次的,导致前一次分配的内存永远无法释放。 - 闭包引用残留:
dataOnHeap作为闭包变量被onmessage回调引用,若回调未被正确清理,内存会一直被占用。
正确的内存释放方案
1. 一次性绑定Worker回调
把worker.onerror和worker.onmessage的绑定移到window.onload中,只绑定一次,避免回调累积。
2. 用唯一请求ID跟踪内存
每次触发test()时生成唯一callbackId,将对应的内存指针和数据大小存在全局对象中,在onmessage回调中根据ID找到对应内存释放。
3. 正确的释放时机和方式
- 只有主线程能释放通过
Module._malloc分配的内存(Wasm堆属于主线程的Module实例),C++代码绝对不能调用free(data),否则会内存崩溃。 - 在
onmessage回调处理完返回数据后,立即调用Module._free释放内存,同时清理相关引用帮助GC回收。
修改后的JS代码示例
let worker = null; const pendingRequests = {}; // 跟踪待处理请求的内存信息 let requestIdCounter = 0; window.onload = function () { worker = new Worker('worker.js'); drawImage(); const btn = document.querySelector('.btn'); btn.addEventListener('click', clickHandler); // 一次性绑定Worker回调 worker.onerror = function (error) { console.error(error); // 错误时清理对应内存 const req = pendingRequests[error.callbackId]; if (req) { Module._free(req.dataPtr); delete pendingRequests[error.callbackId]; } } worker.onmessage = function (event) { const { callbackId } = event.data; const req = pendingRequests[callbackId]; if (!req) return; console.log(`result: \ncall id: ${callbackId}.\n response data: `); const arr = new Uint8Array(event.data.data, req.bufSize); console.log(arr); // 释放内存 Module._free(req.dataPtr); // 清理引用 delete pendingRequests[callbackId]; req.dataOnHeap = null; }; } function clickHandler() { console.log('clicked'); test(); } function test() { const imageData = getCanvasImage(); const buffer = imageData.data.buffer; const bufSize = imageData.width * imageData.height * 4; const dataPtr = Module._malloc(bufSize); const dataOnHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, bufSize); dataOnHeap.set(new Uint8Array(buffer)); // 生成唯一请求ID const callbackId = ++requestIdCounter; // 记录请求对应的内存信息 pendingRequests[callbackId] = { dataPtr, bufSize, dataOnHeap }; const clientMessage = { callbackId, funcName: 'TestMemory', data: dataOnHeap }; worker.postMessage(clientMessage); }
修改后的C++代码(无需大幅改动)
确保不调用free(data),内存由主线程负责释放:
extern "C" EMSCRIPTEN_KEEPALIVE void TestMemory(unsigned char* data, size_t size) { std::cout << "TestMemory begin" << std::endl; std::cout << "size: " << size << std::endl; // 修改数据逻辑 for (size_t i = 0; i < size; ++i) { *(data + i) = *(data + i) / 2; } emscripten_worker_respond(reinterpret_cast<char* >(data), size); std::cout << "TestMemory end" << std::endl; }
关键注意事项
- 永远不要在Worker的C++代码中释放主线程分配的Wasm内存,只能由主线程通过
Module._free释放。 - 避免在循环或重复调用的函数中绑定Worker回调,防止回调累积导致内存泄漏。
- 使用唯一ID跟踪每个请求的内存,确保每个分配的内存都能被对应释放,避免因回调覆盖导致泄漏。
内容的提问来源于stack exchange,提问作者Александр Гуменный
相关产品推荐
相关产品推荐

