这一小节超级实用,朴灵作者从开发者的角度讲解了如何在Node.js中高效使用内存,重点是闭包的内存特性和常见的内存泄漏场景。读完这一节,你以后写代码时就会下意识避开很多“隐形内存炸弹”,尤其是线上长期运行的Node服务。
5.2.1 作用域与闭包
作者先复习JS作用域链和闭包的内存影响(这是内存泄漏的根源之一)。
- 作用域链:函数嵌套时,内部函数能访问外部变量,形成链。
- 闭包:内部函数引用外部变量,即使外部函数执行完,外部变量也不会被GC回收。
示例(经典闭包内存占用):
function outer() {
let bigData = new Array(10 * 1024 * 1024).fill('data'); // 80MB大数组
function inner() {
console.log(bigData.length); // 引用 bigData
}
return inner; // 返回内部函数
}
let leak = outer(); // outer 执行完,但 bigData 因闭包被 inner 引用,无法回收
leak(); // 调用 inner,bigData 一直占用内存
问题:只要 leak 存在,80MB的bigData就永远不释放!
正确做法:用完后置null
leak = null; // 断开引用,bigData 下次GC时回收
5.2.2 内存泄漏常见场景(书里重点列出的四大坑)
作者总结了Node生产环境中最常见的内存泄漏原因:
-
无限增长的缓存
- 用对象/Map做缓存,不设置上限或过期策略。
const cache = new Map(); function addToCache(key, value) { cache.set(key, value); // 永远不删除 }- 结果:缓存越来越大,最终OOM。
- 解决:用LRU缓存(lru-cache库)、设置最大条数、TTL过期。
-
全局变量滥用
- 不小心把大对象挂到global或module.exports。
- global是根对象,永远不回收。
-
未移除的事件监听器
- EventEmitter 添加监听,但不removeListener。
server.on('request', hugeHandler); // hugeHandler 引用大对象 // 服务器长期运行,监听器越来越多,或大对象无法回收- 解决:用once(),或显式removeListener,尤其在动态添加时。
-
队列消费不匹配(观察者队列积压)
- 生产者快、消费者慢(如网络慢),事件队列无限增长。
- 典型:WebSocket推送大数据,客户端消费慢。
作者强调:Node内存泄漏往往缓慢上升(不像Java那么明显),需要长期监控。