Javascript中的内存管理

  • 版本网址缩略名: JavaScript/Javascript中的内存管理
  • 版本标题: Javascript中的内存管理
  • 版本 id: 265458
  • 创建于:
  • 创建者: james.li
  • 是否是当前版本?
  • 评论 15 words added, 72 words removed

修订内容

介绍

Edit section

低阶语言,比如C,拥有像malloc()和free()这样的低阶内存管理单元。另一方面,在javascript中,值(内存)在内容(对象,字符串等)创建时分配,并在它们不再被使用时“自动”释放。后者被称为垃圾回收。这种“自动化”会产生混淆,并容易使Javascript(以及高阶语言)的开发者产生这样的印象:他们可以不关心内存管理。这是错误的。

内存的生命周期

Edit section

无论是哪种程序语言,内存的生命周期差不多总是相同的:

  1. 分配你需要的内存
  2. 使用它(读、写)
  3. 当不再需要已分配的内存时释放它

前两部分在所有语言中都是明确的。最后一部分在低阶语言中是明确的,但在像Javascript这样的高阶语言中则大多是模糊的。

Javascript中的内存分配

Edit section

值初始化

Edit section

在分配内存时,为了不给开发者带来麻烦,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);  

通过函数调用的分配

Edit section

一些函数调用也会产生对象类型(区别于值类型)(james: 但是函数调用返回值为值类型的情况也应当考虑在内)的内存分配

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

一些方法会分配新的值或对象:

    var s = "azerty";  
    var s2 = s.substr(0, 3); 
      //由于字符串是不可改变的值,Javascript可能不分配内存给新的值,而是储存[0,3]范围。
      
    var a = ["ouais ouais", "nan nan"];  
    var a2 = ["generation", "nan nan"];  
    var a3 = a.concat(a2);  //新的4元素的数组由a和a2中的元素连接而成。

使用值

Edit section

使用值本质上就是读写已分配的内存。这个过程可以是读写一个变量或对象属性的值,或给一个函数传入参数。

当内存不再需要时释放它

Edit section

大部分内存管理的问题出现在这个阶段。这里,最困难的工作在于找出什么时候“不再需要某个已分配的内存了”。这常常需要开发者去决定在程序的什么位置不再需要某个内存然后释放它。

高阶语言的解释器包含一个称为“垃圾收集器”的软件,它的工作是追踪内存分配和使用,以便于在不再需要某个已分配的内存时发现,并自动释放它。这个过程值是近似的,因为通常问题在于某段内存是否还会被使用是无法确定的(不能通过一种算法来解决)。

垃圾收集

Edit section

如上所述,基本问题在于无法确定一些内存是否“不再被需要”,因此也就无法自动找出它们。作为结果,垃圾收集器实现了一种有限的解决方案。这一节将会解释一些基本观点,以便于理解其主要的垃圾收集算法,以及存在的限制。

引用

Edit section

垃圾收集算法的主要观点是依赖于引用的概念。在内存管理的上下文中,当一个对象可以访问另一个对象(无论显式地还是隐式的)时,我们就称前者引用后者。例如,一个Javascript对象有一个它的原型的引用(隐式引用),同时有对它的属性值的引用(显式引用)。

在这里,“对象”的概念是泛指的,而不限于通常的Javascript objects,它也包括了函数作用域(或全局词汇作用域,译者按:全局对象)。

引用计数式的算法

Edit section

这是主要的本地垃圾收集算法。这个算法把“一个对象不再被需要”的定义缩小为“一个对象没有被其他对象引用”。当一个对象没有任何引用指向它时,它就被认为是可以被垃圾收集的。

Example

Edit section

    var o = {   
      a: {  
        b:2  
      }  
    }; //2个对象被创建。其中一个作为另一个的属性而被引用。
    //后者通过赋值给变量‘o’而被引用。
    //很明显,它们都不能被垃圾收集。
      
      
    var o2 = o;  //变量‘o2’是第二个引用这个对象的
    o = 1; //现在,起始于'o'的对象通过变量'o2'产生了一个独立的引用
      
    var oa = o2.a;   
    //这个对象现在有2个引用:一个作为属性,另一个作为变量'oa'。
      
    o2 = "yo"; //现在,没有任何引用指向起始于'o'的对象
      
    //然而,最初作为属性'a'的对象仍然被变量'oa'引用着,因此它不能被释放
      
    oa = null; //当'oa'重新赋值后,最初属性'a'指向的对象不再被引用
     

限制:循环引用

Edit section

This naive algorithm has the limitation that if objects reference one another (and form a cycle), they may be "not needed anymore" and yet not garbage-collectable.这个本地算法有一个限制,那就是当对象彼此引用(表现为循环引用),即使当它们“不在被需要”时也不能被垃圾回收。

    function f(){  
      var o = {};  
      var o2 = {};  
      o.a = o2;   
      o2.a = o;   
      
      return "azerty";  
    }  
      
    f();  
    //创建了2个对象,并且彼此引用,因此产生了循环引用
    //在函数调用后它们仍不能超出函数的作用域,
    //因此调用后事实上它们就没用了,可以被释放。
    //然而,引用计数算法认为,由于这两个对象上最少都有一个引用,
    //因此它们都不能被垃圾收集

现实的例子

Edit section

已知IE6,7(8?)使用的是一个引用计数式的垃圾收集器。对于它们,有一个会系统地导致内存泄露的一般模式:

    var div = document.createElement("div");  
    div.onclick = function(){  
      doSomething();  
    }; 变量div通过它的onclick属性有一个指向事件处理的引用
    事件处理也有一个指向这个div的引用,因为变量'div'在处理函数的作用域中可以被访问到
    这个循环将导致两个对象都不能被垃圾收集,因此产生内存泄漏

标记-扫描式的算法

Edit section

这个算法将定义”一个对象不再被需要“缩小为”一个对象不能被到达“。

这个算法假设一组称为roots的对象(在Javascript中,root是全局对象)。垃圾收集器会定期地从roots开始查找所有被roots引用的对象,然后是所有被这些对象引用的对象,以此类推。由于是从roots开始,因此垃圾收集器将找到所有可以到达的对象,并收集所有不可到达的对象。

这个算法优于前一个,因为当”一个对象没有任何引用指向它“时必然使得这个对象不能被到达。但是相反则不成立,正如我们从循环引用中看到的那样。

截止到2012,所有的现代浏览器都使用了标记-扫描式的垃圾收集器。在过去几年中,Javascript垃圾回收领域的所有改进(世代/增量/并发/并行的垃圾收集)都是对这一算法的改进实现,而不是对垃圾回收算法本身的改进,也不是对它的定义”当一个对象不再被需要“的缩减。

循环引用不再是问题

Edit section

在上面的第一个例子中,在函数调用返回后,2个对象不再能够被从全局对象出发的对象所到达。因此,垃圾收集器将认为它们是不可到达的(而回收)。

同样的过程发生在第二个例子中。一旦div和它的处理函数不能从roots到达,它们都将被垃圾回收掉而无视彼此间的引用。

限制:对象需要是显式不可到达的

Edit section

虽然这一点标记为是一个缺陷,但在实际中它很少会发生,这也就是为什么通常没什么人关心垃圾回收的原因。

修订版来源

<h2 class="editable"><span>介绍</span></h2>
<div class="editIcon" style="visibility: hidden;"> <h2 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=1" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h2>
</div>
<p>低阶语言,比如C,拥有像malloc()和free()这样的低阶内存管理单元。另一方面,在javascript中,值(内存)在内容(对象,字符串等)创建时分配,并在它们不再被使用时“自动”释放。后者被称为<em>垃圾回收</em>。这种“自动化”会产生混淆,并容易使Javascript(以及高阶语言)的开发者产生这样的印象:他们可以不关心内存管理。这是错误的。</p>
<div id="section_2"> <h2 class="editable"><span>内存的生命周期</span></h2> <div class="editIcon" style="visibility: hidden;"> <h2 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=2" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h2> </div> <p>无论是哪种程序语言,内存的生命周期差不多总是相同的:</p> <ol> <li>分配你需要的内存</li> <li>使用它(读、写)</li> <li>当不再需要已分配的内存时释放它</li> </ol> <p>前两部分在所有语言中都是明确的。最后一部分在低阶语言中是明确的,但在像Javascript这样的高阶语言中则大多是模糊的。</p> <div id="section_3"> <h3 class="editable"><span>Javascript中的内存分配</span></h3> <div class="editIcon" style="visibility: hidden;"> <h3 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=3" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h3> </div> <div id="section_4"> <h4 class="editable"><span>值初始化</span></h4> <div class="editIcon" style="visibility: hidden;"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=4" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <p>在分配内存时,为了不给开发者带来麻烦,Javascript通过赋值的方式来完成这个过程。</p> <pre class="eval">    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);  </pre> </div> <div id="section_5"> <h4 class="editable"><span>通过函数调用的分配</span></h4> <div class="editIcon"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=5" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <p>一些函数调用也会产生对象类型(区别于值类型)(james: 但是函数调用返回值为值类型的情况也应当考虑在内)的内存分配</p> <pre class="eval">    var d = new Date();  
    var e = document.createElement('div'); // 分配内存给一个dom元素</pre> <p>一些方法会分配新的值或对象:</p> <pre class="eval">    var s = "azerty";  
    var s2 = s.substr(0, 3); 
      //由于字符串是不可改变的值,Javascript可能不分配内存给新的值,而是储存[0,3]范围。
      
    var a = ["ouais ouais", "nan nan"];  
    var a2 = ["generation", "nan nan"];  
    var a3 = a.concat(a2);  //新的4元素的数组由a和a2中的元素连接而成。

</pre> </div> </div> <div id="section_6"> <h3 class="editable"><span>使用值</span></h3> <div class="editIcon"> <h3 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=6" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h3> </div> <p>使用值本质上就是读写已分配的内存。这个过程可以是读写一个变量或对象属性的值,或给一个函数传入参数。</p> </div> <div id="section_7"> <h3 class="editable"><span>当内存不再需要时释放它</span></h3> <div class="editIcon"> <h3 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=7" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h3> </div> <p>大部分内存管理的问题出现在这个阶段。这里,最困难的工作在于找出什么时候“不再需要某个已分配的内存了”。这常常需要开发者去决定在程序的什么位置不再需要某个内存然后释放它。</p> <p>高阶语言的解释器包含一个称为“垃圾收集器”的软件,它的工作是追踪内存分配和使用,以便于在不再需要某个已分配的内存时发现,并自动释放它。这个过程值是近似的,因为通常问题在于某段内存是否还会被使用是<a class="external" href="http://en.wikipedia.org/wiki/Decidability_%28logic%29" title="http://en.wikipedia.org/wiki/Decidability_%28logic%29">无法确定的</a>(不能通过一种算法来解决)。</p> </div>
</div>
<div id="section_8"> <h2 class="editable"><span>垃圾收集</span></h2> <div class="editIcon"> <h2 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=8" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h2> </div> <p>如上所述,基本问题在于无法确定一些内存是否“不再被需要”,因此也就无法自动找出它们。作为结果,垃圾收集器实现了一种有限的解决方案。这一节将会解释一些基本观点,以便于理解其主要的垃圾收集算法,以及存在的限制。</p> <div id="section_9"> <h3 class="editable"><span>引用</span></h3> <div class="editIcon"> <h3 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=9" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h3> </div> <p>垃圾收集算法的主要观点是依赖于<em>引用</em>的概念。在内存管理的上下文中,当一个对象可以访问另一个对象(无论显式地还是隐式的)时,我们就称前者引用后者。例如,一个Javascript对象有一个它的原型的引用(隐式引用),同时有对它的属性值的引用(显式引用)。</p> <p>在这里,“对象”的概念是泛指的,而不限于通常的Javascript objects,它也包括了函数作用域(或全局词汇作用域,译者按:全局对象)。</p> </div> <div id="section_10"> <h3 class="editable"><span>引用计数式的</span>算法</h3> <div class="editIcon" style="visibility: hidden;"> <h3 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=10" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h3> </div> <p>这是主要的本地垃圾收集算法。这个算法把“一个对象不再被需要”的定义缩小为“一个对象没有被其他对象引用”。当一个对象没有任何引用指向它时,它就被认为是可以被垃圾收集的。</p> <div id="section_11"> <h4 class="editable"><span>Example</span></h4> <div class="editIcon" style="visibility: hidden;"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=11" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <pre class="eval">    var o = {   
      a: {  
        b:2  
      }  
    }; //2个对象被创建。其中一个作为另一个的属性而被引用。
    //后者通过赋值给变量‘o’而被引用。
    //很明显,它们都不能被垃圾收集。
      
      
    var o2 = o;  //变量‘o2’是第二个引用这个对象的
    o = 1; //现在,起始于'o'的对象通过变量'o2'产生了一个独立的引用
      
    var oa = o2.a;   
    //这个对象现在有2个引用:一个作为属性,另一个作为变量'oa'。
      
    o2 = "yo"; //现在,没有任何引用指向起始于'o'的对象
      
    //然而,最初作为属性'a'的对象仍然被变量'oa'引用着,因此它不能被释放
      
    oa = null; //当'oa'重新赋值后,最初属性'a'指向的对象不再被引用
     

</pre> </div> <div id="section_12"> <h4 class="editable"><span>限制:循环引用</span></h4> <div class="editIcon"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=12" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <p>This naive algorithm has the limitation that if objects reference one another (and form a cycle), they may be "not needed anymore" and yet not garbage-collectable.这个本地算法有一个限制,那就是当对象彼此引用(表现为循环引用),即使当它们“不在被需要”时也不能被垃圾回收。</p> <pre class="eval">    function f(){  
      var o = {};  
      var o2 = {};  
      o.a = o2;   
      o2.a = o;   
      
      return "azerty";  
    }  
      
    f();  
    //创建了2个对象,并且彼此引用,因此产生了循环引用
    //在函数调用后它们仍不能超出函数的作用域,
    //因此调用后事实上它们就没用了,可以被释放。
    //然而,引用计数算法认为,由于这两个对象上最少都有一个引用,
    //因此它们都不能被垃圾收集

</pre> </div> <div id="section_13"> <h4 class="editable"><span>现实的例子</span></h4> <div class="editIcon"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=13" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <p>已知IE6,7(8?)使用的是一个引用计数式的垃圾收集器。对于它们,有一个会系统地导致内存泄露的一般模式:</p> <pre class="eval">    var div = document.createElement("div");  
    div.onclick = function(){  
      doSomething();  
    }; 变量div通过它的onclick属性有一个指向事件处理的引用
    事件处理也有一个指向这个div的引用,因为变量'div'在处理函数的作用域中可以被访问到
    这个循环将导致两个对象都不能被垃圾收集,因此产生内存泄漏

</pre> </div> </div> <div id="section_14"> <h3 class="editable"><span>标记-扫描式的算法</span></h3> <div class="editIcon"> <h3 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=14" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h3> </div> <p>这个算法将定义”一个对象不再被需要“缩小为”一个对象不能被到达“。</p> <p>这个算法假设一组称为roots的对象(在Javascript中,root是全局对象)。垃圾收集器会定期地从roots开始查找所有被roots引用的对象,然后是所有被这些对象引用的对象,以此类推。由于是从roots开始,因此垃圾收集器将找到所有<em>可以到达的</em>对象,并收集所有不可到达的对象。</p> <p>这个算法优于前一个,因为当”一个对象没有任何引用指向它“时必然使得这个对象不能被到达。但是相反则不成立,正如我们从循环引用中看到的那样。</p> <p>截止到2012,所有的现代浏览器都使用了标记-扫描式的垃圾收集器。在过去几年中,Javascript垃圾回收领域的所有改进(世代/增量/并发/并行的垃圾收集)都是对这一算法的改进实现,而不是对垃圾回收算法本身的改进,也不是对它的定义”当一个对象不再被需要“的缩减。</p> <div id="section_15"> <h4 class="editable"><span>循环引用不再是问题</span></h4> <div class="editIcon" style="visibility: hidden;"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=15" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <p>在上面的第一个例子中,在函数调用返回后,2个对象不再能够被从全局对象出发的对象所到达。因此,垃圾收集器将认为它们是不可到达的(而回收)。</p> <p>同样的过程发生在第二个例子中。一旦div和它的处理函数不能从roots到达,它们都将被垃圾回收掉而无视彼此间的引用。</p> </div> <div id="section_16"> <h4 class="editable">限制:对象需要是显式不可到达的</h4> <div class="editIcon" style="visibility: hidden;"> <h4 class="editable"><a href="/en/JavaScript/Memory_Management?action=edit&amp;sectionId=16" title="Edit section"><span class="icon"><img alt="Edit section" class="sectionedit" src="/skins/common/icons/icon-trans.gif"></span></a></h4> </div> <p>虽然这一点标记为是一个缺陷,但在实际中它很少会发生,这也就是为什么通常没什么人关心垃圾回收的原因。</p> </div> </div>
</div>
恢复到这个版本