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

MongoDB查询引发Node.js堆内存未释放问题咨询

问题

执行await Order.find({})(返回22k条文档)时,Node.js的used_heap_size从约50MB飙升至430MB左右,且该数值始终未回落。
疑问:used_heap_size难道不该很快回落吗?不确定这是否属于内存泄漏,请问该现象成因是什么?是否正常?

相关代码

路由代码

app.get('/orders', async (req, res, next) => {
  v8Print('before find orders');
  const j = await Order.find({});
  v8Print('afters find orders');
  res.sendStatus(200);
});
app.get('/logMem', async (req, res, next) => {
  v8Print('memory info');

  res.sendStatus(200);
});

日志函数

const v8 = require('v8');

// 用于内存分配堆调试
module.exports.v8Print = (message) => {
  const heapStats = v8.getHeapStatistics();
  const heapStatsMB = heapStats;
  for (const key in heapStatsMB) {
    heapStatsMB[key] = `${(((heapStatsMB[key] / 1024 / 1024) * 100) / 100).toFixed(2)} MB`;
  }
  console.log('');
  console.log(message);
  console.table(heapStatsMB);
  console.log('');
};

内存日志

before find orders
┌─────────────────────────────┬──────────────┐
│ (index) │ Values │
├─────────────────────────────┼──────────────┤
│ total_heap_size │ '54.05 MB' │
│ total_heap_size_executable │ '0.80 MB' │
│ total_physical_size │ '53.71 MB' │
│ total_available_size │ '1999.10 MB' │
│ used_heap_size │ '49.56 MB' │
│ heap_size_limit │ '2048.00 MB' │
│ malloced_memory │ '0.01 MB' │
│ peak_malloced_memory │ '2.48 MB' │
│ does_zap_garbage │ '0.00 MB' │
│ number_of_native_contexts │ '0.00 MB' │
│ number_of_detached_contexts │ '0.00 MB' │
└─────────────────────────────┴──────────────┘

afters find orders
┌─────────────────────────────┬──────────────┐
│ (index) │ Values │
├─────────────────────────────┼──────────────┤
│ total_heap_size │ '521.66 MB' │
│ total_heap_size_executable │ '1.05 MB' │
│ total_physical_size │ '520.90 MB' │
│ total_available_size │ '1611.12 MB' │
│ used_heap_size │ '435.79 MB' │ // <---查询后升至435MB
│ heap_size_limit │ '2048.00 MB' │
│ malloced_memory │ '0.01 MB' │
│ peak_malloced_memory │ '2.93 MB' │
│ does_zap_garbage │ '0.00 MB' │
│ number_of_native_contexts │ '0.00 MB' │
│ number_of_detached_contexts │ '0.00 MB' │
└─────────────────────────────┴──────────────┘

memory info
┌─────────────────────────────┬──────────────┐
│ (index) │ Values │
├─────────────────────────────┼──────────────┤
│ total_heap_size │ '521.66 MB' │
│ total_heap_size_executable │ '1.05 MB' │
│ total_physical_size │ '520.90 MB' │
│ total_available_size │ '1610.56 MB' │
│ used_heap_size │ '436.35 MB' │ // <---维持在435MB左右
│ heap_size_limit │ '2048.00 MB' │
│ malloced_memory │ '0.01 MB' │
│ peak_malloced_memory │ '2.93 MB' │
│ does_zap_garbage │ '0.00 MB' │
│ number_of_native_contexts │ '0.00 MB' │
│ number_of_detached_contexts │ '0.00 MB' │
└─────────────────────────────┴──────────────┘

分析与解答

成因解析

  1. V8垃圾回收触发逻辑:V8的GC不会在对象失去引用后立即执行,它会等待内存达到阈值或CPU空闲时才启动。你在请求结束后立刻查询内存,此时GC还没来得及清理j变量引用的22k条文档对象。
  2. 内存保留策略:Node.js默认会保留已分配的堆内存,不会立即还给操作系统。即使GC清理了无用对象,total_heap_size也不会立刻下降——V8认为后续可能还需要同等规模的内存,避免频繁分配/释放带来的性能损耗。从total_available_size还有1.6GB左右可以看出,这些内存是可复用的空闲内存,并非被占用的无效内存。
  3. Mongoose文档的额外开销:Mongoose查询返回的不是纯JSON对象,而是带有包装的文档实例,每个实例包含额外方法、元数据,比原生JSON占用更多内存。22k条这类对象占用430MB内存是合理的。

是否属于内存泄漏?

不属于内存泄漏。内存泄漏的核心是无法回收的持续内存增长,而你的情况中:

  • total_available_size始终充足,说明空闲内存可被复用
  • 多次调用/orders接口后,内存不会持续飙升至堆限制以上,而是会在GC触发后维持稳定区间

验证与优化建议

  • 手动触发GC验证:测试环境中可以用--expose-gc启动Node.js,在res.sendStatus(200)后添加global.gc()再打印内存,会看到used_heap_size明显下降。注意生产环境禁止手动触发GC,会影响性能。
  • 用lean()减少内存开销:如果不需要Mongoose文档的方法,查询时添加.lean(),返回原生JSON对象,能大幅降低内存占用:
    const j = await Order.find({}).lean();
    
  • 分页查询优化:一次性返回22k条数据既耗内存又影响响应速度,建议改成分页查询,每次返回部分数据。

内容的提问来源于stack exchange,提问作者Dashiell Rose Bark-Huss

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 00:20:56