文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

本文探讨了内存管理和垃圾收集机制的关系。

简介

文本简要介绍了JavaScript的垃圾收集机制。我会罗列出一些好的书籍和好的网站。我在JavaScript中发现了一些关于垃圾回收机制的丰富的技术细节。我想要将所学的东西写在一个地方。

背景

  • JavaScript是一个垃圾回收语言。这里的托管环境是浏览器,它会负责所有决策。
  • 在C/C++类似的语言中,你必须自己负责内存管理,例如内存的分配和释放。
  • 然而在JavaScript中并不需要关心内存管理。垃圾回收器会帮你负责这件事。
  • 在回收一个变量或者对象的内存之前,它们的生命周期和使用情况会被确定。如果该变量不再被需要,那么它的内存会被回收作为他用。
  • 一个变量或者对象会在没进一步的使用后被垃圾回收。这件事能通过查看该变量的执行上下文来确定。

 

第 1 段(可获 2.21 积分)

我们将简要地看看什么是变量的执行上下文。

变量执行上下文

变量执行上下文定义了我们能访问那些其他的数据。 

有两种上下文:

  • 全局上下文
  • 局部上下文

下面的例子程序(受《Wrox.Professional JavaScript》的启发)解释了局部上下文和全局上下文之间的差异。T

/* 这是一个全局上下文, 我们命名其为 "G" */
var globalVal = "Hi";

 function changeValue () {
    /* 这是函数“changeValue”的局部上下文, 我们命名其为 "CV" */
    var differentVal = "Hello";
    
    function  swapValues() {
      /* 这是函数"swapValues"的局部上下文, 我们命名其为 "SV" */
      var tempVal = differentVal;
      differentVal = globalVal;
      globalVal = tempVal;
      // 变量"globalVal", "differentVal"和"tempVal"能在这里访问
    }
    // 变量"globalVal", "differentVal"能在这里访问
    
    swapValues ();
 }
 
 // 变量globalVal能在这里访问
 changeValue ();

 

第 2 段(可获 0.81 积分)

如代码所示,我们将执行上下文分成了下面几个部分:

  • 全局上下文: 命名为 "G".
  • 局部上下文: 进一步划分为"changeValue"函数的上下文"CV",和swapValues 函数的上下文"SV".
  • 和之后,一个作用域链被创建出来,它描述了变量们的可访问性。这也帮助垃圾回收器了解哪些变量能被使用以及哪些能被回收。
  • 查看下图能够更清晰地展示:

  • 查看上图,你可以看到按照最少使用量来垃圾回收的变量将是:
    • tempVal (属于上下文"SV")
    • differentVal (属于上下文"CV")
    • globalVal (属于上下文"G")
  • 变量"tempVal"被检查是否有进一步的使用。它的作用域和使用终止于上下文"SV"之中,然后便不会被使用,故而该变量在函数结束之后立刻被回收
  • 上下文"SV"的作用域结束后,"CV" 的作用域随着"swapValues()"函数执行的结束而开始生效,尔后当前执行函数又变成了 "changeValue()".
  • 现在, "differentVal" 变量在当函数 "changeValue" 结束时亦被垃圾回收。这之后,当前执行序列又变回到了全局上下文,即“G”。
  • 最终,变量 "globalVal"也随着你关闭网页而被垃圾回收。
第 3 段(可获 2.81 积分)

垃圾回收策略

标记清除

  • 若变量在执行上下文中,它会被垃圾回收器标记,且只要在上下文中就不会释放它的内存。一旦脱离上下文,该变量会被重新标记为脱离上下文的状态。
  • 因此我们可以推测,浏览器维护了两个列表 "in-Context" 和 "Out-Of-Context"。
  • 垃圾回收器会为所有分配了内存的变量做标记。它会检查变量的上下文是属于"in-context" 还是 "Out-Of-Context"。
  •  "in-context" 中的变量在执行作用域中,不会被当做垃圾回收。 "out-of-context" 中的变量会被回收,且它们占用的空间会被垃圾回收器释放。
第 4 段(可获 1.51 积分)

引用计数

  • 这是种不怎么重要的垃圾回收机制。这种方式的基本原理是跟踪对象的引用数量。
  • 为了跟踪对象的引用数量,需要维护一个引用计数器。
  • 比如下面这个例子:
  • var objectA = new object();
    objectA.value = 10;
     
    var objectB = new object();
    objectB.value = objectA.value; //object B references the value of A	
  • 在这个例子中objectB引用了objectA的value。因此,计数器会递增。若在其它地方有类似的引用,计数器会一直递增下去。
  • 现在假设objectB的value属性(引用了objectA的value)被重写或者更改,计数器会递减。
  • 垃圾回收器会检查这个计数器,如果变为0,说明没有进一步引用这个对象,对象的内存会被回收。
  • 这种方式有个明显的缺点,考虑下如下代码:
    var objectA = new object();
    objectA.newobject = objectB; //object A 引用 Object B
    
    var objectB = new object();
    objectB.newobject = objectA; //object B 引用 Object A
  • 上面的代码造成循环引用问题。观察下,你会发现两个对象的引用计数器是一样的。
  • 除非你手动删除这些对象,否则它们的内存是不会被释放的,如果这段代码中不是对象而是函数呢,不就会造成递归调用了?
  • 这种方式会造成严重的内存泄露,因此,这个问题的解决方案是把上面代码中的对象设置为NULL。
  • 用这种方式进行垃圾回收必须注意这个问题,然而标记清除法在上面这段代码脱离执行上下文后,考虑到了这种情况。
第 5 段(可获 3.09 积分)

因此,我们的目标是使用一种避免了上述问题的内存管理方式。下面讨论的话题是内存管理的问题及解决方案。

内存管理

  • 当数据不会再使用时,最好设为NULL,特别是你声明的全局对象。在你的任务结束后,你应该把全局对象置空。
  • 内存泄露 是另一个问题,假设你循环引用了(上文我们提到过),即使你重新加载页面或者关闭抑或刷新,都解决不了这个问题。这种情况经常发生在IE8以下浏览器。问题的解决方案还是一样,在你完成任务后把对象置空。
  • 最好在编写冗长的代码时,对内存的使用做基准测试,因为即使很小的内存泄露在递归后都会造成严重的后果。
  • 同开发的桌面应用相比,web应用分配和使用的内存更小。这绝对非常有利,因为这样系统就不会挂起或崩溃了。
  • 在垃圾回收语言中,人为造成的内存泄露主要是因为本不需要的引用。
  • 下面场景中,会给出一些避免内存泄露的想法。
第 6 段(可获 2.64 积分)

场景 1

问题

/*下面函数会造成内存泄露,存储在"someLargeData"中的数据,会在inner函数活跃期间,一直持保存内存中。 */
function outer()
{
  var someLargeData = "abcdefghijkl mnopqrstuvwxyz abcdefghijkl mnopqrstuvwxyz 
                       abcdefghijkl mnopqrstuvwxyz";
  
  /*函数"inner"内部使用存储在变量"someLargeData"中的数据*/
  function inner()
  {
    // 处理保存在"someLargeData"的数据
  }

  return fromInnerFunction;
}

解决方案

由于原文有误,译者根据自身编码经验补充:

/*上例演示了闭包造成无意的内存泄露,作者本意可能是提醒读者不要使用没用的闭包*/
function outer()
{
  var someLargeData = "abcdefghijkl mnopqrstuvwxyz abcdefghijkl mnopqrstuvwxyz 
                       abcdefghijkl mnopqrstuvwxyz";
    
  // 直接在outer中处理"someLargeData"变量
}
第 7 段(可获 0.13 积分)

场景 2

问题

/*下面这个函数没有声明就直接使用"data"变量 */
function f()
{
  data = "some value";
}

/*"data"变量其实可以扩展为"window.data" ,变为一个未声明的全局变量*/
function f()
{
  window.data = "some value"
}

解决方案

/*添加 "use strict" ,在意外声明全局变量时抛出错误(译者注:严格模式下没有变量没有声明会报错)*/
function f()
{
  "use strict";
  data = "some value";
}  

最后,我们可以用内存分析工具找出内存泄露,来避免或减少内存泄露问题,尤其在你编写大型应用时特别需要。

本文参考文献和拓展阅读

第 8 段(可获 0.79 积分)

文章评论