javascript的内存走漏
关于JavaScript这门言语的运用者来讲,大多数的运用者的内存治理认识都不强。因为JavaScript一向以来都只作为在网页上运用的脚本言语,而网页每每都不会长时间的运转,所以运用者对JavaScript的运转时长和内存掌握都比较无视。但跟着Spa(单页运用)、node.js服务端顺序和种种js东西的降生,我们须要从新注重JavaScript的内存治理。
内存走漏的定义
指因为忽视或毛病形成顺序未能开释已不再运用的内存的状况。内存走漏并不是指内存在物理上的消逝,而是运用顺序分派某段内存后,因为设想毛病,失去了对该段内存的掌握,因此形成了内存的糟蹋。
JavaScript的内存治理
起首JavaScript是一个有Garbage Collection 的言语,也就是我们不须要手动的接纳内存。差别的JavaScript引擎有差别的渣滓接纳机制,这里我们主要以V8这个被普遍运用的JavaScript引擎为主。
JavaScript内存分派和接纳的关键词:GC根、作用域
GC根:平常指全局且不会被渣滓接纳的对象,比方:window、document或许是页面上存在的dom元素。JavaScript的渣滓接纳算法会推断某块对象内存是不是是GC根可达(存在一条由GC根对象到该对象的援用),假如不是那这块内存将会被标记接纳。
作用域:在JavaScript的作用域里,我们能够新建对象来分派内存。比方说挪用函数,函数实行的过程当中就会建立一块作用域,假如是建立的是作用域内的部份对象,当作用域运转完毕后,一切的部份对象(GC根没法触及)都邑被标记接纳,在JavaScript中能引发作用域分派的有函数挪用、with和全局作用域。
作用域的分类:部份作用域、全局作用域、闭包作用域
部份作用域
函数挪用会建立部份作用域,在部份作用域中的新建的对象,假如函数运转完毕后,该对象没有作用域外部的援用,那该对象将会标记接纳
全局作用域
每一个JavaScript历程都邑有一个全局作用域,全局作用域上的援用的对象都是常驻内存的,直到历程退出内存才会自动开释。
手动开释全局作用域上的援用的对象有两种体式格局:
global.foo = undefined
从新赋值转变援用
delete global.foo
删除对象属性
闭包作用域
在JavaScript言语中有闭包的观点,闭包指的是包括自在变量的代码块、自在变量不是在这个代码块内或许任何全局高低文中定义的,而是在定义代码块的环境中定义(部份变量)。
var closure = (function(){
//这里是闭包的作用域
var i = 0 // i就是自在变量
return function(){
console.log(i++)
}
})()
闭包作用域会坚持对自在变量的援用。上面代码的援用链就是:
window -> closure -> i
闭包作用域另有一个主要的观点,闭包对象是当前作用域中的一切内部函数作用域同享的,而且这个当前作用域的闭包对象中除了包括一条指向上一层作用域闭包对象的援用外,其他的存储的变量援用一定是当前作用域中的一切内部函数作用域中运用到的变量
罕见的几种内存走漏的体式格局及运用chrome dev tools的排查要领
用全局变量缓存数据
将全局变量作为缓存数据的一种体式格局,将以后要用到的数据都挂载到全局变量上,用完以后也不手动开释内存(因为全局变量援用的对象,渣滓接纳机制不会自动接纳),全局变量逐步就积累了一些不必的对象,致使内存走漏
var x = [];
function createSomeNodes() {
var div;
var i = 10000;
var frag = document.createDocumentFragment();
for (; i > 0; i--) {
div = document.createElement("div");
div.appendChild(document.createTextNode(i + " - " + new Date().toTimeString()));
frag.appendChild(div);
}
document.getElementById("nodes").appendChild(frag);
}
function grow() {
x.push(new Array(1000000).join('x'));
createSomeNodes();
setTimeout(grow, 1000);
}
grow()
上面的代码贴一张 timeline的截图
主要看memory地区,经由过程剖析代码我们能够晓得页面上的dom节点是不停增添的,所以memory里绿色的线(代表dom nodes)也是不停升高的;而代表js heap的蓝色的线是有升有降,当团体趋向是逐步升高,这是因为js 有内存接纳机制,每当内存接纳的时刻蓝色的线就会下落,然则存在部份内存一向得不到开释,所以蓝色的线逐步升高
js毛病援用DOM元素
var nodes = '';
(function () {
var item = {
name:new Array(1000000).join('x')
}
nodes = document.getElementById("nodes")
nodes.item = item
nodes.parentElement.removeChild(nodes)
})()
这里的dom元素虽然已从页面上移除了,然则js中依旧保留这对该dom元素的援用。
因为这段代码是只实行一次的,所以用timeline视图会很难剖析出来是不是存在内存走漏,所以我们能够用 chrome dev tool 的 profile tab里的heap snapshot 东西来剖析。
上面的代码贴一张 heap snapshot 的summary形式的截图
经由过程constructor的filter功用,我们把上面代码中建立的长字符串找出来,能够看到代码运转完毕后,内存中的长字符串依旧没有被渣滓接纳掉。
顺带提一下的是右侧红框里的shadow size和 retainer size的寄义
shadow size 指的是对象当地的大小
retainer size 指的是对象所援用内存的大小,接纳该对象是会将他援用的内存也一并接纳,所以retainer size 指代的是接纳内存后会开释出来的内存大小
上面我们能够看到 长字符串自身的shadow size和retainer size是一样大的,这是援用长字符串没有援用其他的对象,假若有援用其他对象,那shadow size 和retainer size将不一致。
闭包轮回援用
(function(){
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 someMethod() {
console.log('someMessage')
}
};
};
setInterval(replaceThing,100)
})()
起首我们明白一下,unused是一个闭包,因为它援用了自在变量 originalThing,虽然它被没有运用,但v8引擎并不会把它优化掉,因为 JavaScript里存在eval函数,所以v8引擎并不会随意优化掉临时没有运用的函数。
theThing 援用了someMethod,someMethod这个函数作用域隐式的和unused这个闭包同享一个闭包高低文。所以someMethod也援用了originalThing这个自在变量。
这里面的援用链是:
GCHandler -> replaceThing -> theThing -> someMethod -> originalThing -> someMethod(old) -> originalThing(older)-> someMethod(older)
跟着setInterval的不停实行,这条援用链是不会断的,所以内存会不停走漏,直致顺序崩溃。
因为是闭包作用域引发的内存走漏,这时刻最好的挑选是运用 chrome的heap snapshot的container视图,我们经由过程container视图能清晰的看到这条不停走漏内存的援用链
因为作者程度有限,文中若有毛病还望指出,感谢!
参考文档:
百科内存走漏引见
chrome devtolls
深入浅出nodejs
node-interview