JavaScript和node.js内存泄露的原因和避免方法及Chrome调试工具使用教程
当我们使用“老式”方法编写网页时,通常不太需要关注JavaScript内存管理。
但 SPA(单页应用程序)的兴起促使我们需要关注与内存相关的编码实践。
在本文中,我们将探讨导致JavaScript内存泄漏的编程模式,并说明如何改善内存管理。
JavaScript代码中常见的内存泄漏
偶然的全局变量
全局变量始终被根结点引用,并且永远不会被垃圾回收。在非严格模式下,一些错误会导致变量从本地范围泄漏到全局:
给未声明的变量赋值, 使用“ this”指向全局对象。
function createGlobalVariables() { leaking1 = 'I leak into the global scope'; // leaking未声名 this.leaking2 = 'I also leak into the global scope'; // 'this' 指向全局变量 }; createGlobalVariables(); window.leaking1; // 'I leak into the global scope' window.leaking2; // 'I also leak into the global scope'
2 Closures 闭包
函数内的变量将在函数退出调用堆栈后清除,如果函数外部没有指向它们的引用,则将对其进行清理。
尽管函数已完成执行,但闭包仍将保留变量的引用。
function outer() { const potentiallyHugeArray = []; return function inner() { potentiallyHugeArray.push('Hello'); // 返回内部函数并封闭 potentiallyHugeArray 变量 console.log('Hello'); }; }; const sayHello = outer(); // 返回内部函数 function repeat(fn, num) { for (let i = 0; i < num; i++){ fn(); } } repeat(sayHello, 10); // 每一次调用都将 'Hello' 加到了 potentiallyHugeArray // now imagine repeat(sayHello, 100000)
内部变量 potentiallyHugeArray 永远无法访问到,并且占用内存一直在增加。
3 Timers 计时器
下面的例子中因为计时器一直在执行, data 永远不会被回收。
··· function setCallback() { const data = { counter: 0, hugeString: new Array(100000).join(‘x’) }; return function cb() { data.counter++; // data 对象在回调范围可访问 console.log(data.counter); } } setInterval(setCallback(), 1000); // 如何停止? ···
记得及时清楚计时器:
function setCallback() { const data = { counter: 0, hugeString: new Array(100000).join('x') }; return function cb() { data.counter++; // data 对象在回调范围可访问 console.log(data.counter); } } const timerId = setInterval(setCallback(), 1000); // 保存计时器ID // doing something ... clearInterval(timerId); //停止计时器
4 Event listeners 事件绑定
事件绑定的变量永远不会被回收,除非:
- 执行 removeEventListener()
- 相关 DOM 元素被删除.
const hugeString = new Array(100000).join('x'); document.addEventListener('keyup', function() { // 匿名函数无法被回收 doSomething(hugeString); // hugeString 被保持在匿名函数的引用中 });
不用的时侯要取消注册
function listener() { doSomething(hugeString); } document.addEventListener('keyup', listener); // 使用命名函数注册事件 document.removeEventListener('keyup', listener); // 取消事件注册
如果事件只需要执行一次,请使用 once 参数
document.addEventListener('keyup', function listener(){ doSomething(hugeString); }, {once: true}); // listener will be removed after running once
5 Cache 缓存
如果使用了缓存,而没有相应删除逻辑,缓存会一直增加。
let user_1 = { name: "Peter", id: 12345 }; let user_2 = { name: "Mark", id: 54321 }; const mapCache = new Map(); function cache(obj){ if (!mapCache.has(obj)){ const value = `${obj.name} has an id of ${obj.id}`; mapCache.set(obj, value); return [value, 'computed']; } return [mapCache.get(obj), 'cached']; } cache(user_1); // ['Peter has an id of 12345', 'computed'] cache(user_1); // ['Peter has an id of 12345', 'cached'] cache(user_2); // ['Mark has an id of 54321', 'computed'] console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") user_1 = null; // 删除了不活跃用户1 //垃圾回收后 console.log(mapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321") // 用户1还在内存中
解决方法有很多, 可使用 WeakMap 弱引用对象
let user_1 = { name: "Peter", id: 12345 }; let user_2 = { name: "Mark", id: 54321 }; const weakMapCache = new WeakMap(); function cache(obj){ // ...same as above, but with weakMapCache return [weakMapCache.get(obj), 'cached']; } cache(user_1); // ['Peter has an id of 12345', 'computed'] cache(user_2); // ['Mark has an id of 54321', 'computed'] console.log(weakMapCache); // ((…) => "Peter has an id of 12345", (…) => "Mark has an id of 54321"} user_1 = null; // 删除了不活跃用户1 //垃圾回收后 console.log(weakMapCache); // ((…) => "Mark has an id of 54321") - user_1 被回收了
6. 被引用的 DOM 对象
如果 dom 被JS引用了,即使从DOM树中被删除,也无法被垃圾回收
function createElement() { const div = document.createElement('div'); div.id = 'detached'; return div; } // 这里产生了对 DOM 元素的引用 const detachedDiv = createElement(); document.body.appendChild(detachedDiv); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement(); // 可在泄露工具中查看 div#detached 对象仍然存在
可使用局部变量创建DOM
function createElement() {...} // same as above // DOM 在函数内部被引用 function appendElement() { const detachedDiv = createElement(); document.body.appendChild(detachedDiv); } appendElement(); function deleteElement() { document.body.removeChild(document.getElementById('detached')); } deleteElement(); // 在泄露工具中看不到 div#detached
7 循环引用
在完美的垃圾回收机制中,循环引用的两个对象只要不被外部变量引用就可以被回收。但在引用计数实现的垃圾回收中,这两个对象永远不会回收,因为引用计数永远无法到达0,如下面的情况:
document.write("Circular references between JavaScript and DOM!"); var obj; window.onload = function(){ obj=document.getElementById("DivElement"); document.getElementById("DivElement").expandoProperty = obj; // DOM 引用了它自身 obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); };Div Element
下面的例子同样有这个问题
function myFunction(element) { this.elementReference = element; //循环引用 DOM-->JS-->DOM element.expandoProperty = this; } function Leak() { //这段代码造成泄露 new myFunction(document.getElementById("myDiv")); }
Chrome 内存对象查看
可在Chrome中创建内存快照
然后查看内存中的对象
参考资料:
- https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them
- https://www.cnblogs.com/duhuo/p/4760024.html
- https://www.lambdatest.com/blog/eradicating-memory-leaks-in-javascript/