JavaScript垃圾回收是什么?

307

什么是JavaScript垃圾回收?一个web程序员需要了解JavaScript垃圾回收的重要性,以便编写更好的代码。


3
请参考https://dev59.com/7nRA5IYBdhLWcg3w8igl。 - Ian Ringrose
10个回答

198
Eric Lippert曾经写过一篇关于这个主题的详细博客文章(并将其与VBScript进行了比较)。更准确地说,他写的是JScript,这是微软自己实现的ECMAScript,尽管非常类似于JavaScript。我想你可以假设在Internet Explorer的JavaScript引擎中,绝大部分行为都是相同的。当然,不同浏览器的实现会有所不同,但我认为你可以运用许多共同原则,并将它们应用到其他浏览器上。
从那个页面引用:
JScript使用非代际的标记-清除垃圾收集器。其工作原理如下: - 每个"在作用域内"的变量称为"清道夫"。一个清道夫可以引用数字、对象、字符串等等。我们维护一个清道夫列表,当变量进入作用域时,将其移动到该列表上,并在变量离开作用域时将其移出该列表。 - 垃圾收集器会定期运行。首先,它会在GC跟踪的所有内存上放置一个"标记",包括对象、变量、字符串等(JScript在内部使用VARIANT数据结构,其中有许多未使用的额外位,因此我们只需设置其中一个即可)。 - 其次,它清除了清道夫和清道夫引用的传递闭包上的标记。因此,如果一个清道夫对象引用了一个非清道夫对象,那么我们会清除非清道夫对象以及它所引用的所有对象上的位。(我在这里使用"闭包"一词与之前的帖子中使用的含义不同) - 此时,我们知道所有仍然被标记的内存都是分配的内存,不能从任何作用域内的变量到达。所有这些对象都被指示自我拆除,从而销毁任何循环引用。
垃圾回收的主要目的是让程序员无需担心他们创建和使用的对象的内存管理,尽管有时候不可避免 - 至少大概了解垃圾回收如何工作总是有益的。
历史注释:答案的早期版本中引用了错误的delete运算符。在JavaScript中,delete 运算符从一个对象中删除属性,与C / C ++中的delete完全不同。

28
苹果指南有缺陷:作者错误地使用了 delete;例如,在第一个例子中,你应该先通过 window.removeEventListener() 移除事件监听器,然后使用 foo = null 覆盖变量,而不是使用 delete foo。在 IE 中,如果 foo 是全局的,delete window.foo(但不是 delete foo)也可以起作用,但即使如此,在 FF 或 Opera 中仍然行不通。 - Christoph
3
请注意,埃里克的文章只应被视为“仅供历史参考”。但它仍然具有信息价值。 - Peter Ivan
2
请注意 - IE 6和7不使用非代际标记和扫描垃圾收集器。它们使用简单的引用计数垃圾收集器,这更容易受到垃圾收集中循环引用问题的影响。 - Doug
1
ECMAScript的delete是一个一元运算符(表达式),而不是语句(例如:delete 0,delete 0,delete 3)。当作为表达式语句时,它看起来像语句。 - user5066707
是的,现在这个答案已经过时了。自2012年以来,现代浏览器使用标记/清除算法,因此它不再依赖于作用域。参考:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management - sksallaj

56

当涉及DOM对象时,请注意循环引用:

JavaScript中的内存泄漏模式

请记住,只有当没有对对象的活动引用时,才可以回收内存。这是闭包和事件处理程序的常见陷阱,因为某些JS引擎不会检查内部函数实际上引用了哪些变量,并且仅保留封闭函数的所有局部变量。

以下是一个简单的示例:

function init() {
    var bigString = new Array(1000).join('xxx');
    var foo = document.getElementById('foo');
    foo.onclick = function() {
        // this might create a closure over `bigString`,
        // even if `bigString` isn't referenced anywhere!
    };
}

一个天真的JS实现在事件处理程序存在时无法收集bigString。有几种解决这个问题的方法,例如在init()的结尾处设置bigString = null(delete对于局部变量和函数参数不起作用:delete从对象中删除属性,变量对象是不可访问的 - 即使在严格模式下,ES5也会抛出ReferenceError如果您尝试删除局部变量!)。
我建议尽可能避免不必要的闭包,如果您关心内存消耗。

21
DOM 环状引用 bug 只存在于 JScript 中——其他浏览器没有这个问题,只有 IE 有。实际上我相当确定 ECMAScript 规范明确规定垃圾回收器必须能够处理这种循环引用的情况 :-/ - olliej
@olliej:我在ECMAScript规范中没有看到任何关于GC的提及。 - Janus Troelsen
请参见http://point.davidglasser.net/2013/06/27/surprising-javascript-memory-leak.html。 - Christoph

17

从博客中摘取的好引言

DOM组件和JScript组件都会被"垃圾回收",这意味着如果你在任一组件中创建一个对象,然后失去了该对象的跟踪,它最终将被清理掉。

例如:

function makeABigObject() {
var bigArray = new Array(20000);
}
当您调用该函数时,JScript组件会创建一个对象(名为bigArray),该对象在函数内部可访问。但是,一旦函数返回,您就“失去了”bigArray,因为没有办法再引用它。好吧,JScript组件意识到您已经失去了它,因此bigArray被清理 - 它的内存被回收。DOM组件也是同样的原理。如果您说document.createElement('div')或类似的内容,则DOM组件会为您创建一个对象。一旦您以某种方式失去了该对象的跟踪,DOM组件将清理相关内容。

13
据我所知,JavaScript的对象在没有对该对象的引用时定期进行垃圾回收。这是一种自动发生的过程,但如果您想更深入地了解其工作原理,可以查看C++级别的WebKitV8源代码
通常情况下,您不需要考虑它,但在旧版浏览器中(如IE 5.5和早期版本的IE 6,以及可能的现行版本),闭包会创建循环引用,如果未加检查,将最终耗尽内存。在我提到的闭包特定情况下,是指当您向DOM对象添加JavaScript引用和向DOM对象添加引用回JavaScript对象的对象时。基本上,它永远不能被回收,最终会导致测试应用程序中的循环崩溃,使操作系统变得不稳定。实际上,这些泄漏通常很小,但为了保持代码清洁,应删除JavaScript对DOM对象的引用。
通常最好使用delete关键字立即取消引用大型对象,例如您已经接收并完成所需操作的JSON数据,尤其是在移动web开发中。这将导致下一个GC扫描删除该对象并释放其内存。

JavaScript -> DOM -> JavaScript 循环引用问题在较新版本的IE中是否得到解决?如果是,从什么时候开始?我认为这个问题在架构上非常深入,不太可能被修复。你有任何来源吗? - erikkallen
仅仅是个人经验而已。我没有注意到在标准模式下运行的IE 8存在疯狂泄漏,也没有在损坏模式下出现问题。我会调整我的回答。 - Heat Miser
1
@erikkallen:是的,IE 8+版本已经修复了GC错误,因为旧版本使用了非常幼稚的垃圾回收算法,这使得无法对相互引用的一对对象进行GC。新的“标记-清除”样式算法可以解决这个问题。 - kumarharsh

8

垃圾回收(GC)是一种自动内存管理的形式,通过移除不再需要的对象来实现。

任何处理内存的过程都遵循以下步骤:

1 - 分配所需的内存空间

2 - 进行一些处理

3 - 释放该内存空间

有两种主要算法用于检测哪些对象不再需要。

引用计数垃圾回收:该算法将“一个对象不再需要”定义为“没有其他对象引用它”,如果没有引用指向它,则将删除该对象。

标记-清除算法:将每个对象连接到根源。任何未连接到根源或其他对象的对象都将被删除。

目前大多数现代浏览器使用第二种算法。


1
要添加来源,请参见 MDN:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management - Xenos

5
在计算机科学中,垃圾回收(GC)是一种自动内存管理形式。垃圾回收器或者称为收集器,试图回收垃圾,即应用程序永远不会再次访问或更改的对象所使用的内存。

所有JavaScript引擎都有自己的垃圾收集器,并且它们可能不同。大多数情况下,您无需处理它们,因为它们只会执行它们应该执行的操作。

编写更好的代码主要取决于您对编程原则、语言和特定实现的了解程度。


3

引用类型并不直接将对象存储到分配给它的变量中,因此下面示例中的对象变量实际上并不包含对象实例。相反,它保存一个指向内存中对象存在位置的指针(或引用)。

var object = new Object();

如果你将一个引用类型的变量赋值给另一个变量,每个变量都会得到指针的副本,并且两个变量仍然引用内存中的同一个对象。

var object1 = new Object();
var object2 = object1;

指向同一个对象的两个变量

JavaScript是一种垃圾回收语言,因此使用引用类型时不需要担心内存分配。然而,最好是取消引用不再需要的对象,以便垃圾回收器可以释放那部分内存。这样做的最佳方式是将对象变量设置为null。

var object1 = new Object();
// do something
object1 = null; // dereference

取消引用对象在使用数百万个对象的非常大型应用程序中尤为重要。

出自《面向对象的JavaScript原理》- NICHOLAS C. ZAKAS


1
在JavaScript中,垃圾回收是非确定性的,对象何时被清除或是否会被清除都不确定。这适用于强引用的对象。强引用的对象受到垃圾回收的保护。
在ES12之后,可以执行以下实现来检查对象何时被垃圾回收。
要了解更多关于JavaScript中垃圾回收的知识,可以使用ES12之后提供的Finalisers。
let a = new Array(200).fill(true);

构建 finaliaser

const cleanup = new FinalizationRegistry(key => {
  // your code here
});

cleanup.register(a, 'wewew');

对象'a'现在不可达,垃圾回收后将发生终结器回调


1
JavaScript垃圾回收是什么?
请查看this 对于Web程序员来说,了解JavaScript垃圾回收的重要性是什么,以便编写更好的代码?
在JavaScript中,您不需要关心内存分配和释放。整个问题都由JavaScript解释器处理。在JavaScript中仍然可能存在泄漏,但它们是解释器的错误。如果您对此主题感兴趣,可以阅读更多www.memorymanagement.org

在你提供的文章中各种内存管理系统中,JavaScript使用的是哪一种?“JavaScript仍然可能存在泄漏问题,但这是解释器的错误。”- 这并不意味着JS程序员可以简单地忽略整个问题,例如,在早期的IE版本中有一个相当著名的JS<->DOM循环引用问题,您可以在JS代码中解决它。此外,JS闭包的工作方式是一种设计特性,而不是错误,但如果您不适当地使用闭包,则可能会捆绑更大的内存块(我 _不_是说不要使用它们)。 - nnnnnn
3
内存泄漏是 JavaScript 中的一种难题。如果您只是编写一个简单的“大学项目”应用程序,那就没问题。但是,当您开始编写高性能企业级应用程序时,JavaScript 的内存管理就必不可少了。 - Doug

1
在Windows上,您可以使用Drip.exe来查找内存泄漏或检查您的免费mem例程是否有效。
这非常简单,只需输入网站URL,您将看到集成IE渲染器的内存消耗。然后点击刷新,如果内存增加,则说明在网页某处发现了内存泄漏。但这也非常有用,可以查看释放IE内存的例程是否有效。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接