php中文网 | cnphp.com

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 690|回复: 0

识别并避免 Js 内存泄漏,跟低级缺陷say goodbye,让老总对...

[复制链接]

3138

主题

3148

帖子

1万

积分

管理员

Rank: 9Rank: 9Rank: 9

UID
1
威望
0
积分
7946
贡献
0
注册时间
2021-4-14
最后登录
2024-11-21
在线时间
763 小时
QQ
发表于 2022-1-20 16:09:28 | 显示全部楼层 |阅读模式
内存泄漏
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。 对于不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

常见的内存泄漏类型
1、意外的全局变量
在一个局部作用域中,未定义的变量会在全局对象创建一个新变量
function fn() {
    msg = "这是一个意外的全局变量";
}

函数 fn 内部忘记使用 var ,实际上 js 会把 msg 挂载到全局对象上,意外创建一个全局变量。

类似
function fn() {
    window.msg = "这是一个显式定义的全局变量";
}

另一种意外的全局变量可能由 this 创建
function fn() {
    this.msg = "该this指向window,创建了一个全局变量";
}

// fn 调用自己时,this 指向了全局对象(window),而不是 undefined
fn();

如何避免

在 JavaScript 文件头部加上 "use strict" ,使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为 null 或者重新定义
<script>
    "use strict"
    //  以下的所有代码都处于严格模式
</script>

2、被遗忘的定时器或回调函数
定时器setInterval、setTimeout代码很常见
let data = getData();
setInterval(() => {
    let el = document.getElementById('El');
    if(el) {
        // 处理 el 和 data
        el.innerHTML = JSON.stringify(data);
    }
}, 1000);
以上例子中,在 el 或者数据不再需要时(如节点移除),定时器仍然指向这些数据。所以就算 el 节点被移除后,setInterval 仍旧存活且垃圾回收器没办法回收,它的依赖自然也没办法被回收,除非终止定时器,如
[mw_shl_code=applescript,true]let data = getData();
let timerId = setInterval(() => {
    let el = document.getElementById('El');
    if(el) {
        // 处理 el 和 data
        el.innerHTML = JSON.stringify(data);
    }
}, 1000);

// 终止定时器,使得它的依赖(el、data)可被回收
clearInterval(timerId)

// setTimeout使用clearTimeout()
[/mw_shl_code]
补充
let btn = document.getElementById('button');
function onClick(event) {
    btn.innerHTML = 'text';
}
btn.addEventListener('click', onClick);
对于监听绑定,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的 IE 6 是无法处理循环引用的。因为老版本的 IE 是无法检测 DOM 节点与 JavaScript 代码之间的循环引用,会导致内存泄漏。
// 明确移除监听器
btn.removeEventListener('click', onClick);
但是,现代的浏览器(包括 IE 和 Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用 removeEventListener 了。

3、脱离DOM的引用
如果把DOM 存成字典(JSON 键值对)或者数组,此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中,那么将来需要把两个引用都清除
[mw_shl_code=applescript,true]// 创建一个elements,依赖#button与#image
let elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
};
function doStuff() {
    let image = document.getElementById('image');
    image.src = 'http://some.url/image';
    let button = document.getElementById('button');
    button.click();
    // ...
}
function removeButton() {
    // 按钮是 body 的后代元素
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的#button的引用elements字典。button元素仍旧在内存中,不能被GC回收。
}

// 显式移除引用,elements = null,使依赖的DOM可被GC回收[/mw_shl_code]
如果代码中保存了表格某一个 <td> 的引用。将来决定删除整个表格的时候,直觉认为 GC 会回收除了已保存的 <td> 以外的其它节点。实际情况并非如此:此 <td> 是表格的子节点,子元素与父元素是引用关系。由于代码保留了 <td> 的引用,导致整个表格仍待在内存中。所以保存 DOM 元素引用的时候,要小心谨慎。

4、闭包
闭包的关键是内部函数可以访问父级作用域的变量
[mw_shl_code=applescript,true]let theThing = null;
let replaceThing = function () {
  let originalThing = theThing;
  let unused = function () {
    if (originalThing)
      console.log("hi");
  };
   
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log(someMessage);
    }
  };
};

setInterval(replaceThing, 1000);[/mw_shl_code]
每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。

如何避免

在 replaceThing 的最后添加 originalThing = null 。

扩展
垃圾回收机制
对垃圾回收机制来说,核心思想就是如何判断内存已经不再使用,常用垃圾回收算法有下面两种:

引用计数
标记清除(常用)
引用计数法
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用,如果没有其他对象指向它了,说明该对象已经不再需要了,如上我们将一些变量赋值为null。

引用计数有一个致命的问题,那就是循环引用。如果两个对象相互引用,尽管他们已不再使用,但是垃圾回收器不会进行回收,最终可能会导致内存泄漏
[mw_shl_code=applescript,true]function cycle() {
    let obj1 = {};
    let obj2 = {};
    // 相互引用
    obj1.a = obj2;
    obj2.a = obj1;
    // 即使未使用到这两个对象,两者也都不会被GC回收
    return "循环引用!"
}

cycle();[/mw_shl_code]
cycle函数执行完成之后,对象obj1和obj2实际上已经不再需要了,但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,所以现代浏览器不再使用这个算法,但是IE依旧使用。

标记清除法(常用)
标记清除算法将“不再使用的对象”定义为“无法到达的对象”。即从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,保留。那些从根部出发无法触及到的对象被标记为不再使用,稍后进行回收。无法触及的对象包含了没有引用的对象这个概念,但反之未必成立。

对于主流浏览器来说,只需要切断需要回收的对象与根部的联系,就可以正确被垃圾回收处理。最常见的内存泄露一般都与DOM元素绑定有关
email.message = document.createElement(“div”);
displayList.appendChild(email.message);

// 稍后从displayList中清除DOM元素
displayList.removeAllChildren();
上面代码中,div元素已经从DOM树中清除,但是该div元素还绑定在email对象的message,所以如果email对象存在,那么该div元素就会一直保存在内存中。

彻底清除 email = null 或 email.message = null。





上一篇:Node Sass version 7.0.1 is incompatible with ^4.0.0
下一篇:【罗盘时钟---使用html,js,css编写。附源代码及效果】
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|php中文网 | cnphp.com ( 赣ICP备2021002321号-2 )

GMT+8, 2024-11-22 04:55 , Processed in 0.280158 second(s), 33 queries , Gzip On.

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2020, Tencent Cloud.

申明:本站所有资源皆搜集自网络,相关版权归版权持有人所有,如有侵权,请电邮(fiorkn@foxmail.com)告之,本站会尽快删除。

快速回复 返回顶部 返回列表