javascript 内存管理 

介绍

低级语言(c语言),拥有底层的内存管理原语:malloc() 和 free()。而在 javascript 中,变量的内存是在创建过程中自动分配的,当变量不再被引用时会自动释放其内存。后面的这一处理过程被称为“垃圾回收”。这里的“自动”导致了一系列的困惑,并且给 javascript (高级语言) 的开发者带来一种错误的印象,即他们不需要关心内存的管理。

内存的生命周期

抛开编程语言来说,几乎所有的内存生命周期都具有相同的特点:

1. 按需分配内存

2. 读,写

3. 不再被引用时,释放

前两个特点在所有的语言中都是显而易见的,第三点在低级编程语言中也很容易能够办到,但是在大部分高级语言中(比如 javascript)则很复杂。

javascript 中内存的分配

  • 变量的初始化

为了方便开发者不去关心内存分配的事情,javascript 在变量申明的时候就已经顺便分配了内存。

var n = 123; // 给一个number分配内存
var s = "azerty"; // 给一个string分配内存
var o = {
  a: 1,
  b: null
}; // 给一个object分配内存

var a = [1, null, "abra"]; // (和object一样)给一个array及里面的值分配内存

function f(a){
  return a + 2;
} // 给一个function分配内存(function也可以当作一个object)

// 函数的表达式同样也是相当于给一个object分配内存
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);
  • 通过函数调用分配内存

一些函数调用会于给一个对象分配内存

var d = new Date();
var e = document.createElement('div');// 给一个dom元素分配内存

一些方法会导致给一个新的变量或对象分配内存

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的string
// 由于strings是一些不可变的值组成的,javascript可能不会给其分配内存,而只是存储一个[0,3]的范围

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); // 新的array,包含四个元素,分别由a和a2的元素组成
  • 变量的使用

使用一个变量基本上也就意味着内存的读和写,这个过程可以通过读或写变量的值或一个对象的属性值或者甚至是给函数传递参数完成。

  • 内存不再被引用时,释放

内存管理中遇到的大多数问题都会集中在这个阶段。这里最难的任务的如何判断一个已分配的内存不再继续被使用。通常这个问题是由开发者决定这段内存将不再被使用,然后释放它。

一般高级语言的编译器会内嵌一小段程序,它被称作“垃圾回收者",这个垃圾回收者的工作顾名思义,当然是回收垃圾的嘛,这里的"垃圾"指的就是将不再被使用的内存片段。这个过程也只是个近似的处理,因为目前为止,判断某个片段的内存是否正在被引用这个问题是不可判定的,也就是说不能在多项式时间内求解。

 

垃圾回收

上面已经提到判断一段内存是否不再被引用是图灵不可判定问题。因此垃圾回收机制也就采用了一个限制性的方法来解决这个问题。这部分先会介绍一些必要的概念以便理解垃圾回收的算法及其限制的地方。

引用

垃圾回收算法的主要思想主要依赖的一个概念就是引用。在一段内存管理的上下文中,如果一个对象访问了另一个对象(无论是显式的还是隐式的),我们就说前者引用了后者。例如,一个 javascript 对象可以访问它的 prototype 属性(隐式的), 也可以调用它自己的属性值(显式的)。

在这个上下文中,"object" 的概念被扩展了,它指代的是不仅是传统的 object 对象,同时还包含了函数域(或全局词法作用域)。

基于引用计数的垃圾回收

这是最基本的垃圾回收算法。这个算法把问题"一个对象不再被引用"简化成"一个对象没有被其他对象引用"。如果一个对象上的引用计数为0,则表明这个对象是可以被当作垃圾回收的。

 

示例:

var o = { 
  a: {
    b:2
  }
}; // 创建了两个对象,一个引用了另外一个对象作为其属性 显然,都不可以被回收


var o2 = o; // o2 也引用了 o
o = 1; // 现在,o中原始的对象有一个唯一的引用 o2的变量中 

var oa = o2.a; // 引用o2的a属性
// 这个对象产生了两个引用,一个是o2的a对象,另一个一个是oa变量

o2 = "yo"; // 原始的o对象现在是0引用
// o可以被回收
// 然而其属性a仍然被oa引用了,因此其内存不能被释放

oa = null; // 原始对象o中的a是0引用了
// 此时可以被回收

 

缺点:循环引用

这个基本算法有一个缺点,一个对象A引用了另一对象B,对象B又反过来引用了A。这样A,B之间就形成了一个循环。A,B可能都不会再引用,但是根据上面的算法A和B均不会被回收。

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

  return "azerty";
}

f();

 

现实生活中的例子

IE的6,7中针对 dom 对象使用的就是基于引用计数的垃圾回收机制。所以,在IE6,7中,很容易产生系统的内存泄露。

var div = document.createElement("div");
div.onclick = function(){
  doSomething();
}; // div 通过 事件引用了其onclick属性
// handler同样引用了div
// 循环引用,内存泄露

标记-删选 算法

这个算法将问题"一个对象不再被引用"简化成"一个对象是无法访问的"。

这个算法假设知道一组对象的根(在 javascript 中这个根就是全局变量 window),这个垃圾回收者会不间断的自根节点遍历其所有子节点。这样,垃圾回收者最后会找出所有可访问的对象及所有不可访问的对象。

这个算法好于之前的算法,因为“一个引用计数为0的对象”也是一个不可访问的对象。而反过来却不成立,比如循环引用。

截至2012年,所有现代浏览器都内置了一个标记和回收垃圾的收集器。在过去的几年中,javascript的垃圾回收领域(代/增量/并发/并行垃圾收集)中的所有改进都是在实现这种算法的改进而不是改善垃圾收集算法本身也不是在改进问题描述的简化模型上。

 

循环引用不再是一个问题

上面第一个示例中,函数调用之后,这两个对象将不再被引用,其他的变量直接可从全局变量中访问。因此, 它们将被垃圾回收者认为是不可访问的。

第二个示例也是如此,一旦这个 div 及其 handler不可被顶级 roots 访问,它们都将被回收掉,即使它们之间会产生互相引用。

 

缺点:对象需要显式的指明不可访问

虽然这被标记为一个缺点,其实这在现实中很难出现,因为也不会有很多人经常关注垃圾的回收。

更多

  • IBM article on "Memory leak patterns in JavaScript" (2007)
  • Kangax article on how to register event handler and avoid memory leaks (2010)
  • Performance:Leak Tools