JavaScript 内存管理和垃圾回收

像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。相反,JavaScript具有自动垃圾收集机制,执行环境会负责管理代码执行过程中使用的内存,在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放,释放的过程称为垃圾回收。这种垃圾收集的机制很简单,找出那些不在继续使用的变量,然后释放其占用的存。为此,垃圾收集器就会按照固定的时间间隔(或代码执行中预定的收集时间),周期性的执行这一操作。

1. 局部变量的生命周期

局部变量只在函数执行的过程中存在,在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储他们的值。然后在函数中使用这些变量,直至函数执行结束。结束后,局部变量不再使用,也就没有存在的必要,因此释放他们的内存以供将来使用。

2. 内存的生命周期

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还
JavaScript 的内存分配
2.1. 值的初始化

为了不让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。

var n = 123; // 给数值变量分配内存var s = "azerty"; // 给字符串分配内存
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);
2.2. 通过函数调用分配内存

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

var d = new Date(); // 分配一个 Date 对象
var e = document.createElement('div'); // 分配一个 DOM 元素

有些方法分配新变量或者新对象:

// s2 是一个新的字符串
// 因为字符串是不变量
// JavaScript 可能决定不分配内存
// 只是存储了 [0-3] 的范围。
var s = "azerty";
var s2 = s.substr(0, 3); 
// 新数组有四个元素,是 a 连接 a2 的结果
var a = ["ouais ouais", "nan nan"];var a2 = ["generation", "nan nan"];var a3 = a.concat(a2); 
2.3. 使用值

使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

2.4. 当内存不再需要使用时释放

大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“哪些被分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。

3. 垃圾回收

如上文所述自动寻找是否一些内存“不再需要”的问题是无法判定的。因此,垃圾回收实现只能有限制的解决一般问题。本节将解释必要的概念,了解主要的垃圾回收算法和它们的局限性。

3.1. 引用

垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。

3.2. 引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。含义是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型值赋给该变量时,则这个值得引用次数是1,如果这个值又被赋给另一个变量,那么引用次数+1,相反,如果包含这个值得引用变量又被赋予了另外一个值,则这个值的引用次数-1,当这个值得引用次数为0的时候,说明没有变量再使用这个值了,因而就将其占用的内存空间再收回来。当垃圾收集器再运行时,就是释放哪些引用次数为0的值所占用的空间。

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

// 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
o = 1; 

// 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
var oa = o2.a; 
               
// 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收
o2 = "yo"; 

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

Netspace Navigator3.0 最早使用引用计数,但是很快就发现了一个严重的问题:循环引用
循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的指针。
该算法有个限制:无法处理循环引用的事例。在下面的例子中,两个对象被创建,并互相引用,形成了一个循环。它们被调用之后会离开函数作用域,所以它们已经没有用了,可以被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

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

在这个例子中,oA和oB互相引用,即两个对象的引用次数都为2,在采用计数策略,在函数执行完毕后,oA和oB还继续存在,假如这个函数被多次调用,就会导致大量的内存得不到回收,因为他们的引用次数永远不会为0,但在标记策略不是问题

3.2.2. 实际例子

IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏:

var div;
window.onload = function(){
  div = document.getElementById("myDivElement");
  div.circularReference = div;
  div.lotsOfData = new Array(10000).join("*");
};

在上面的例子里,myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。

3.3. 标记-清除算法

这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。、
当变量进入环境时,就将这个变量标记为“进入环境”,一般不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它。当变量离开环境时,将其标记为“离开环境”。

3.3.1 标记的方式:

可以使用任何方式来标记变量,比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境,或者使用一个“进入环境”的变量列表和一个“离开环境”的变量列表来跟踪哪个变量发生了变化。然而这并不重要,关键在于采取什么策略。

垃圾收集器会在运行的时候给存储在内存中的所有变量都加上标记,会去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上标记的变量将被视为准备删除的变量,因为环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。

这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。

这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定,参考“循环引用”。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。

3.3.2 循环引用不再是问题了

在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。

3.3.3 限制: 那些无法从根对象查询到的对象都将被清除

尽管这是一个限制,但实践中我们很少会碰到类似的情况,所以开发者不太会去关心垃圾回收机制。

4. 性能问题

在所有的浏览器中,可以触发垃圾回收过程,但是不建议这么做。在IE中,调用window.CollectGarbage()方法会立即执行垃圾收集。在Opera7及更高的版本中,调用window.opera.collect()也会启动垃圾收集例程。

5. 内存管理

JavaScript分配给web浏览器的可用内存数量通常要比分配给桌面应用的少。这样做是出于安全方面的考虑,目的是防止运次JavaScript的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。
确保占用最少的内存可以让页面获得更好的性能。优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其设置为null来释放其引用–这个做法叫做解除引用。这一做法适用于大多数全局变量和全局对象的属性。局部变量在他们离开执行环境时自动被解除引用。

funtion createPerson(name){
  var localPerson = new Object();
  localPerson.name = name;
  return localPerson;
} 
Var globalPerson = createPerson(‘Nicholas’);
// 手工解除globalPerson 的引用
globalPerson = null;

解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。

参考:
[1]: https: //developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
[2]: JavaScript高级程序设计(第3版)78-81

发布了5 篇原创文章 · 获赞 0 · 访问量 125

猜你喜欢

转载自blog.csdn.net/forteenBrother/article/details/105352944