垃圾回收机制

JavaScript 的内存分配

JavaScript 在定义变量时就完成了内存分配。

var n = 123; // 给数值变量分配内存
var s = "jirengu"; // 给字符串分配内存

var o = {
  a: 1,
  b: null
}; // 给对象及其包含的值分配内存

// 给数组及其包含的值分配内存(就像对象一样)
var a = [1, null, "abra"]; 

// 给函数(可调用的对象)分配内存
function f(a){
  return a + 2;
} 

// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

有些函数调用结果是分配对象内存:

var d = new Date(); // 分配一个 Date 对象

var e = document.createElement('div'); // 分配一个 DOM 元素

当内存不再需要使用时释放,大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。

高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

垃圾回收

什么是垃圾?我们做个粗浅的定义,凡是之后不再将被使用的对象,都是垃圾。

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。

在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

示例

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 现在,“这个对象”的原始引用o被o2替换了

var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 最初的对象现在已经是零引用了
           // 他可以被垃圾回收了
           // 然而它的属性a的对象还在被oa引用,所以还不能回收

oa = null; // a属性的那个对象现在也是零引用了
           // 它可以被垃圾回收了

该算法有个限制:无法处理循环引用。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

标记-清除算法 (mark-and-sweep)

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

该算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。 此算法可以分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。

  • 标记阶段,垃圾回收器会从根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。
  • 清除阶段,垃圾回收器会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作。

现在现代浏览器都使用了标记-清除垃圾回收算法或基于该算法的改进。

内存泄露

内存中的某些对象因被引用,即使后续不再被使用也不会被回收。因某些高频率或者长时间的操作,导致该对象被不断创建,对内存的占用越来越高,导致系统剩余可用内存越来越少。

对于一般的浏览器页面,我们不必过于关心因使用闭包或者事件绑定未移除带来的内存不被释放,因为内存消耗并没有”持续变多“,当用户刷新页面或者关闭当前页面所有内存自然被释放。但在某些场景下我们需要特别关注内存泄露的问题。

场景1,高频操作。比如onscroll、onresize、setInterval、循环语句等高频场景里存在的不会被回收的对象,或者如实现动画等高频渲染的场景。

场景2,长时操作。比如使用nodejs作为后端语言搭建的网站。如果内存的使用持续增长,最终会导致服务器崩溃。

参考

results matching ""

    No results matching ""