对于JavaScript这门语言的使用者来讲,大多数的使用者的内存管理意识都不强。由于JavaScript一直以来都只做为在网页上使用的脚本语言,而网页每每都不会长时间的运行,因此使用者对JavaScript的运行时长和内存控制都比较漠视。但随着Spa(单页应用)、node.js服务端程序和各类js工具的诞生,咱们须要从新重视JavaScript的内存管理。javascript
指因为疏忽或错误形成程序未能释放已经再也不使用的内存的状况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,于是形成了内存的浪费。java
首先JavaScript是一个有Garbage Collection 的语言,也就是咱们不须要手动的回收内存。不一样的JavaScript引擎有不一样的垃圾回收机制,这里咱们主要以V8这个被普遍使用的JavaScript引擎为主。node
GC根:通常指全局且不会被垃圾回收的对象,好比:window、document或者是页面上存在的dom元素。JavaScript的垃圾回收算法会判断某块对象内存是不是GC根可达(存在一条由GC根对象到该对象的引用),若是不是那这块内存将会被标记回收。git
做用域:在JavaScript的做用域里,咱们可以新建对象来分配内存。好比说调用函数,函数执行的过程当中就会建立一块做用域,若是是建立的是做用域内的局部对象,看成用域运行结束后,全部的局部对象(GC根没法触及)都会被标记回收,在JavaScript中能引发做用域分配的有函数调用、with和全局做用域。github
函数调用会建立局部做用域,在局部做用域中的新建的对象,若是函数运行结束后,该对象没有做用域外部的引用,那该对象将会标记回收web
每一个JavaScript进程都会有一个全局做用域,全局做用域上的引用的对象都是常驻内存的,直到进程退出内存才会自动释放。
手动释放全局做用域上的引用的对象有两种方式:算法
global.foo = undefinedchrome
从新赋值改变引用缓存
delete global.foo闭包
删除对象属性
在JavaScript语言中有闭包的概念,闭包指的是包含自由变量的代码块、自由变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。
var closure = (function(){ //这里是闭包的做用域 var i = 0 // i就是自由变量 return function(){ console.log(i++) } })()
闭包做用域会保持对自由变量的引用。上面代码的引用链就是:
window -> closure -> i
闭包做用域还有一个重要的概念,闭包对象是当前做用域中的全部内部函数做用域共享的,而且这个当前做用域的闭包对象中除了包含一条指向上一层做用域闭包对象的引用外,其他的存储的变量引用必定是当前做用域中的全部内部函数做用域中使用到的变量
将全局变量做为缓存数据的一种方式,将以后要用到的数据都挂载到全局变量上,用完以后也不手动释放内存(由于全局变量引用的对象,垃圾回收机制不会自动回收),全局变量逐渐就积累了一些不用的对象,致使内存泄漏
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 有内存回收机制,每当内存回收的时候蓝色的线就会降低,可是存在部份内存一直得不到释放,因此蓝色的线逐渐升高
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