在 JavaScript 中,因为垃圾回收是自动进行的,因此人们在编码时可能不太会注意这方面。但事实是,一些 webapp 在使用一段时间后,会出现卡顿的现象,特别是那些单页应用,包括 WebView 方式的手机 app 。这个现象在传统的“单击 - 刷新”类型的页面中并不明显,由于页面刷新以后,全部没有被回收的垃圾对象也会被清除,可是在单页应用中,若是没有手动去点浏览器的刷新按钮,那么就算是很小的内存泄露,随着页面停留时间的增加,累积的泄露会愈来愈多,在手机上的感受就更明显了。javascript
因此这里想讨论一下内存泄露是如何发生的,以及如何去避免。java
开门见山,通常有两种方式的垃圾回收机制,一个是“引用计数”,当一个对象被引用的次数为 0 时,该对象就能够被回收;另外一个是“标记清除”,当一个对象不能再被访问到时,对该对象进行标记,等下一轮 GC 事件发生时,这些对象就会被清除。从 2012 年起,全部的现代浏览器都是基于“标记清除”的回收算法,因此,若是须要兼容更早的浏览器,可能须要作更多的事。GC 的时机由 JS 引擎决定,须要知道的事,当 GC 进行时,主线程会被阻塞,这个时间能够经过 Chrome 的 Timeline 工具看到,最少也会超过 10 ms 吧。web
在 Chrome 中能够很直观方便地看到垃圾回收事件的执行。打开 Chrome 的 Timeline,只须要勾选“Memory”就能够了,而且在左边的 View 中选中第二个。算法
而后单击放大镜下面的圆点,这时候 Chrome 会开始记录内存分配、绘制等事件,等你打开一张页面,好比百度吧,再单击这个圆点(如今应该是红色的了),就会看到一条蓝色的折线。不一样页面不同,但几乎都会有一个忽然降低的地方,好比下图中 1200 ms 左边的地方,单击它,就能在下方显示 GC 事件所用的时间,以及它回收了多少内存。浏览器
若是你看到本身网站的这条蓝色折线是呈上升趋势,在不断的 GC 后,内存仍是在上升,就极有多是发生了内存泄露,须要排查一下代码。闭包
这里的问题在于“循环引用”,若是对象 a 的属性引用了 b,而 b 的属性引用了 a,因为引擎只有在变量的引用次数为 0 的状况下才会回收,这里 a 和 b 的引用次数至少有 1,因此就算它们所在的函数执行完了,这两个变量仍是没法被回收掉。app
function foo() { var a = {}, b = {}; a.attr = b; b.attr = a; } foo();
当 foo 函数执行不少次以后,就会有不少个没法被回收的 a 和 b 存在。webapp
实际状况多是这样的:函数
function foo() { var text = document.getElementById('input-text'); text.onfocus = function() { text.value = ''; } } foo();
意思是,当光标移到输入框时,清空原有的内容。考虑 text 变量和 foo 里面的匿名函数,text 的 onfocus 属性引用了匿名函数,而该匿名函数引用了 text 变量(循环了),因此当 foo 执行结束后,这两个对象因为引用次数大于 0 而没法被回收。工具
对于这种状况,只须要在 foo 的末尾对 text 变量置空就能够了。
text = null;
若是你用 Chrome 运行这个例子的话,会看到蓝线仍是降到初始的高度了,由于 Chrome 是基于“标记清除”的算法来回收内存的,因此不会有“循环引用”的问题。
对于标记清除,心中要想象一个树,每一个页面都存在一个根,每当一个函数执行,就会生成一个节点。天然,嵌套的函数调用就会有子节点。通常状况下(没有闭包),当函数执行完时,内部的变量都是没法被其余代码访问的,因此它就被标记为“没法被访问”。GC 时,JS 引擎统一对全部这些状态的对象进行回收。
介绍两个概念。Shallow Size,表示该对象自己占用的内存。Retained Size,表示释放该对象后能获得的内存大小。什么意思?好比上图绿色的 #3,这个绿色的面积就是 Shallow Size。释放 #3 后,#4 和 #5 也会被释放,因此 Retained Size 就是 #三、#四、#5 的总大小。
在“标记清除”算法中,难点是如何判断一个对象已是“没法被访问”了。
若是用树去分析垃圾回收,会发现其实咱们须要作的事情不多,由于当一个函数执行完以后,它连带的对象都会被清除。就算有闭包,当引用该闭包的函数执行完时,这些闭包也一样会被标记。
那么在哪里会发生内存泄露呢?看这个例子。
var btn = document.getElementById('btn'); btn.onclick = function() { var fragment = document.createElement('div'); }
它表示每单击一次按钮,就建立一个 <div>
,它没有引用任何对象,可是回调结束以后,这个空的 <div>
是不会被回收的。
var content = document.getElementById('content'); content.innerHTML = '<button id="button">click</button>'; var button = document.getElementById('button'); button.addEventListener('click', function() {}); content.innerHTML = '';
这段代码事后,虽然 <button>
从 DOM 中移除了,因为它的监听器还在,因此没法被 GC 回收。
要避免这种状况就是经过 removeEventListener 将回调函数去掉。
若是使用 setInterval,那么它引用到的变量的上下文会保留下来。
function foo() { var name = 'tom', title = 'Hero'; window.setInterval(function() { alert(name); }, 1000); } foo();
这里的状况时,每隔 1 秒弹框一次。第一,虽然只用到了 name,但 name 所在的上下文都没法被释放,包括 title 。第二,因为定时器一直在执行,因此这个上下文是不会被释放的。固然,有时候这是业务要求,也谈不上内存泄露了,只不过要注意的是,若是真的有不必的定时器,请调用 clearInterval 把它去掉吧。
另外一方面,你不用为了仅仅避免内存泄露对 setTimeout 调用 clearTimeout 。它是不会形成内存泄露的,除非是别的什么缘由,好比说,在 setTimeout 中递归调用了当前定时器,这至关于模拟 setInterval,能够与 setInterval 作相似处理。
在平时的一些开发过程当中,我发现虽然在 Chrome 中发生了 GC 事件,而且内存也降得很低,若是用 Profile 工具 Take Heap Snapshot 的话,也不会以为有内存泄露发生。但在手机上(WebView)的确会存在越用越卡的现象,这点可能要根据不一样的环境来分析,但文中提到的关键两个地方就是:解除引用,以及解除监听的事件。
若是本身的代码中能作到这两点的话,可能卡顿是由别的问题引发的,而不是内存泄露。