JavaScript和node.js内存泄露的原因和避免方法及Chrome调试工具使用教程


发布者 ourjs  发布时间 1591319841520
关键字 JavaScript  Node.JS  编程技巧  前端 

当我们使用“老式”方法编写网页时,通常不太需要关注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,如下面的情况:

<script type="text/javascript">
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"));
};
</script>
<div id="DivElement">Div Element</div>

下面的例子同样有这个问题

function myFunction(element)
{
    this.elementReference = element;
    //循环引用 DOM-->JS-->DOM
    element.expandoProperty = this;
}

function Leak() {
    //这段代码造成泄露
    new myFunction(document.getElementById("myDiv"));
}

Chrome 内存对象查看

可在Chrome中创建内存快照

snapshot_1.png

然后查看内存中的对象

snapshot_2.png

参考资料:

  1. https://www.ditdot.hr/en/causes-of-memory-leaks-in-javascript-and-how-to-avoid-them
  2. https://www.cnblogs.com/duhuo/p/4760024.html
  3. https://www.lambdatest.com/blog/eradicating-memory-leaks-in-javascript/








 热门文章 - 分享最多
  1. Html5网页中用JavaScript调用本地手机摄像头扫描识别微信二维码、条形码:postMessage跨域https传递扫码结果消息
  2. JavaScript设置对象属性只读不可修改、不可枚举、不可删除:Object.defineProperty
  3. CSS教程:图片使用混合模式和颜色叠加filter滤镜,改变PNG图标颜色
  4. CSS教程:如何设置自动显示隐藏scrollbar滚动条,自定义外观样式/宽度,附demo示例大全
  5. node.js通过Error.prepareStackTrace获取上层调用函数的文件名地址和行数位置
  6. JavaScript判断字符串是否为数字类型:Number.isInteger、isNaN、正则表达式比较
  7. webpack前端项目调试环境安装入门:webpack.config.js禁用UglifyJs只合并JavaScript不压缩混淆代码
  8. node.js创建aria2代理服务器:使用net.socket转发rpc或http request请求,替换websocket
  9. request停止维护:用node.js实现http网页爬虫抓取,模拟ajax\post请求,大文件上传下载
  10. 用node.js在Windows或Linux平台上高性能解压/压缩zip、tar大文件,输出到文件或Stream流

 相关阅读
  1. Node.JS如何按顺序调用async函数,如何判断是否为async函数,在mocha中自动化测试async/await代码
  2. node.js将回调函数嵌套,用promise改造成async/await顺序执行:异常处理和返回多个参数
  3. 使用node.js和oAuth2协议集成Github/LinkedIn第三方登录以OnceOA模块源码为例
  4. JavaScript在Array数组中按指定位置删除或添加元素对象
  5. node.js含有%百分号时,发送get请求时浏览器地址自动编码的问题
  6. 让pre和textarea等HTML元素去掉滚动条自动换行自适应文本内容高度
  7. Facebook发布全新JavaScript引擎Hermes:越来越像Java字节码,JS要统一全端?
  8. 在嵌入式设备树莓派上编译QuickJS教程:一个C语言编写的极简JavaScript引擎
  9. 使用JavaScript的Proxy监听对象属性变化并进行类public/private的访问控制
  10. JavaScript求一个字符串的字节长度

  开源的 OurJS
OurJS开源博客已经迁移到 OnceOA 平台。

  关注我们
扫一扫即可关注我们:
OnceJS

OnceOA