垃圾回收机制
自动垃圾收集机制
javascript 具有自动垃圾收集机制, 也就是说, 执行环境会负责管理代码执行过程中使用的内存. 即开发人员不需要当心内存使用的问题, 所需内存的分配以及无用内存的回收完全实现了自动管理. 这种垃圾回收机制的原理: 找出那些不再继续使用的变量, 然后释放其内存. 为此, 垃圾收集器会按照固定的时间间隔, 周期性地执行这一操作.
局部变量的生命周期
局部变量只在函数执行的过程中存在. 在这个过程中, 会为局部变量在栈内存或堆内存上分配相应的空间, 以便存储他们的值. 然后再函数中使用这些变量, 直至函数执行结束. 此时, 局部变量就没有存在的必要了, 因此可以释放他们的内存以供将来使用.
javascript 中的 2 中垃圾回收机制
标记清除(Mark-and-sweep)
包括标记阶段和清除阶段.
标记阶段: 垃圾回收器创建了一个 roots 列表. Roots 通常是代码中全局变量的引用. javascript 中, window 对象是一个全局变量, 被当作 root.window 对象总是存在, 因此垃圾回收器可以检查他和他所有子对象是否存在(即不是垃圾). 所有的 roots 被检查和标记为激活(即不是垃圾). 所有的子对象也被递归检查, 检查从 root 开始的所有对象, 如果是可达的, 则打上标记. 标记阶段到此结束.
清除阶段: 标记阶段完成时, 被标记的对象被视为 "存活" 对象. 然后重新进入递归扫描阶段, 将没有标记的对象进行回收. 这一阶段称为清除阶段. 在扫描的同时, 还需要将存活的对象的标记清除掉, 以便下一次 GC 操作做准备.
引用计数(reference counting)
引用计数的含义是跟踪记录每个值被引用的次数. 当声明了一个变量并将一个引用类型赋给该变量时, 则这个值的引用次数就是 1. 如果同一个值又被赋给另一个变量, 则将该引用次数再加 1. 相反, 如果包含对这个值得引用变量又取得了另一个值, 则这个值的引用次数就减 1. 当这个值得引用次数变为 0 时, 则说明没有办法在访问这个值了, 因而久可以将其占用的内存空间回收回来. 这样, 垃圾回收器下次再运行时, 会释放那些引用次数为 0 的值所占用的内存.
内存泄漏
内存泄漏是指应用程序使用过的内存片段, 在不需要的时候, 不能返回到操作系统或可用的内存池中的情况. javascript 中的内存泄漏同理, 当一个值不再需要被引用了, 但却无法被垃圾回收器回收的情况, 就是内存泄露.
javascript 中 3 种常见的内存泄漏
1. 意外的全局变量
举个例子 (普通变量):
function fun(){
globalVar = "this is a hidden global variable";
}
// 在函数体内部定义变量, 如果没有使用 var 声明, 则定义的变量是全局变量, 即成为 window 的一个属性
// 而 window 总是存在, 因此不会被回收
再举个例子 (由 this 创建的变量):
function foo() {
this.variable = "potential accidental global";
console.log(this)
}
// Foo 调用自己, this 指向了全局对象(window)
// 而不是 undefined
foo();
image.png
解决方案: 在 js 文件头部加上(ES5)use strict, 使用严格模式, 可以避免此类错误的发生. 如果必须使用全局变量存储大量数据时, 确保用完以后把它设置为 null 或者重新定义.
2. 被遗忘的计数器或者回调函数
举个例子:
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
// 处理 node 和 someResource
node.innerhtml = JSON.stringify(someResource));
}
}, 1000);
// 重复计数器并没有使用 clearInterval() 终止, 导致内存无法回收
对于观察者的例子, 一旦它们不再需要(或者关联的对象变成不可达), 明确地移除它们非常重要. 老的 IE 6 是无法处理循环引用的. 如今, 即使没有明确移除它们, 一旦观察者对象变成不可达, 大部分浏览器是可以回收观察者处理函数的.
3. 脱离 DOM 的引用
有时, 保存 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 回收.
}
4. 闭包(正常情况下不会造成内存泄漏)
由于 IE9 之前的版本对 JScript 对象和 COM 对象使用不同的垃圾收集. 因此闭包在 IE 的这些版本中会导致一些特殊的问题. 具体来说, 如果闭包的作用域链中保存着一个 HTML 元素, 那么就意味着该元素将无法被销毁.
reference -- javascript 高级程序设计第三版
举个例子:
function closure() {
var element = document.getElementById("someElement");
element.onclick = function() {
alert(element.id);
};
}
以上代码创建了一个作为 element 元素事件处理程序的闭包, 而这个闭包则又创建了一个循环引用. 由于匿名函数保存了一个对 closure() 的活动对象的引用, 因此就会导致无法减少 element 的引用数. 只要匿名函数存在, element 的引用数至少也是 1, 因此它所占用的内存就永远不会被回收.
解决方案: 把 element.id 的一个副本保存在一个变量中, 从而消除闭包中该变量的循环引用同时将 element 变量设为 null.
function closure() {
var element = document.getElementById("someElement");
var id = element.id;
element.onclick = function() {
alert(id);
};
element = null;
}
因此可以看出, 闭包并不会引起内存泄漏, 只是由于 IE9 之前的版本对 JScript 对象和 COM 对象使用不同的垃圾收集, 从而导致内存无法进行回收, 这是 IE 的问题, 所以正常使用闭包的情况下不会导致内存泄漏.
reference -- GC 的三大基础算法 reference -- javascript 内存泄露及其避免 闭包会造成内存泄漏吗?
来源: http://www.jianshu.com/p/93911f8f9e95