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' │
└─────────────────────────────┴──────────────┘
成因解析
- V8垃圾回收触发逻辑:V8的GC不会在对象失去引用后立即执行,它会等待内存达到阈值或CPU空闲时才启动。你在请求结束后立刻查询内存,此时GC还没来得及清理
j变量引用的22k条文档对象。 - 内存保留策略:Node.js默认会保留已分配的堆内存,不会立即还给操作系统。即使GC清理了无用对象,
total_heap_size也不会立刻下降——V8认为后续可能还需要同等规模的内存,避免频繁分配/释放带来的性能损耗。从total_available_size还有1.6GB左右可以看出,这些内存是可复用的空闲内存,并非被占用的无效内存。 - 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

