You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
}

内存泄漏原因

  1. 重复绑定Worker回调:每次调用test()都会给worker重新绑定onmessage和onerror,旧的回调函数不会被销毁,间接引用之前分配的Wasm内存和相关对象,导致GC无法回收。
  2. 固定请求ID导致回调覆盖:一直用callbackId: 1,多次快速点击时,后续回调会覆盖前一次的,导致前一次分配的内存永远无法释放。
  3. 闭包引用残留: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,提问作者Александр Гуменный

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 06:37:34