内存泄漏: 对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。对于不再用到的内存,没有及时释放,就叫做内存泄漏。
内存泄漏识别方法:
- 浏览器方法:
- 打开开发者工具,选择Memory
- 在右侧的Select profiling type字段里面勾选timeline
- 点击左上角的录制按钮
- 在页面上进行各种操作,模拟用户的使用情况
- 一段时间后,点击左上角的stop按钮,面板上就会显示这段时间的内存占用情况。
- 命令行方法 使用Node提供的process.memoryUsage方法。
console.log(process.memoryUsage());
//输出
{
rss:27709440, //resident set size,所有内存占用,包括指令区和堆栈
heapTotal:5685248, //"堆"占用的内存,包括用到的和没用到的
heapUsed:3449392, //用到的堆的部分
external:8772 //V8引擎内部的C++对象占用的内存
}
判断内存泄漏,以heapUsed字段为准。
四种常见的JS内存泄漏(划重点)
- 意外的全局变量 未定义的变量会在全局对象创建一个新变量
function foo(arg){
a = "this is a hidden global variable";
}
函数foo内部忘记使用var,实际上JS会把a挂载到全局对象上,意外创建一个全局变量。
function foo(arg){
window.a = "this is an explicit global variable";
}
另一个意外的全局变量可能有this创建。
function foo(){
this.variable = "potential accidental global";
}
//foo 调用自己,this指向了全局对象window
//而不是undefined foo()
解决方法:
在JS文件头部加上'use strict',使用严格模式避免意外的全局变量,此时上例中的this指向undefined。如果必须使用全局变量存储大量数据时,确保用完以后把它设置为null或者重新定义。
-
被遗忘的计时器或回调函数
计时器
setInterval代码很常见
var someResource = getData();
setInterval(function(){
var node = document.getElementById('Node');
if(node){
//处理node和someResource
node.innerHtml = JSON.stringify(someResource);
}
},1000);
上面的例子表明,在节点node或者数据不再需要时,定时器依旧指向这些数据。所以哪怕当node节点被移除后,setInterval仍旧存活并且垃圾回收器没办法回收,它的依赖也没办法回收,除非终止定时器。
var element = document.getElementById('button');
function onClick(event){
element.innerHTML = 'text';
}
element.addEventListener('click',onClick);
对于上面观察者的例子,一旦它们不再需要(或者关联的对象变成不可达),明确地移除它们非常重要。老的IE6是无法处理循环引用的。因为老版本的IE是无法检测DOM节点与JS代码之间的循环引用,会导致内存泄漏。
但是,现代的浏览器(包括IE和Microsoft Edge)使用了更先进的垃圾回收算法(标记清除),已经可以正确检测和处理循环引用了。即回收节点内存时,不必非要调用removeEventListener了。
- 脱离DOM的引用 如果把DOM存成字典(JSON键值对)或者数组,此时,同样的DOM元素存在两个引用:一个在DOM树中,另一个在字典中。那么将来需要把两个引用都清除。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
// 更多逻辑
}
function removeButton() {
// 按钮是 body 的后代元素
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
如果代码中保存了表格某一个的引用。将来决定删除整个表格的时候,直觉认为GC会回收除了已保存的以外的其它节点。实际情况并非如此:此是表格的子节点,子元素和父元素是引用关系。由于代码保留了的引用,导致了整个表格仍待在内存中。所以保存DOM元素引用的时候,要小心谨慎。
- 闭包 闭包的关键时匿名函数可以访问父级作用域的变量。
var theThing = null;
var replaceThing = function(){
var originalThing = theThing;
var unused = function(){
if(originalThing)
console.log('hi');
};
theThing = {
longStr:new Array(1000000).join('*'),
someMethod:function(){
console.log(someMessage);
}
};
};
setInterval(replaceThing,1000);
每次调用replaceThing,theThing得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量unused是一个引用
originalThing的闭包(先前的replaceThing又调用了theThing)。someMethod可以通过theThing使用,someMethod与unused分享闭包作用域,尽管unused从未使用,它引用的originalThing迫使它保留在内存中(防止被回收)。
解决方法: 在 replaceThing 的最后添加 originalThing = null 。