JavaScript是如何工作的:内存管理 + 如何处理4个常见的内存泄露

原文出处 How JavaScript works: memory management + how to handle 4 common memory leaks

How JavaScript works: memory management + how to handle 4 common memory leaks

JavaScript是如何工作的:内存管理 + 如何处理4个常见的内存泄露

A few weeks ago we started a series aimed at digging deeper into JavaScript and how it actually works: we thought that by knowing the building blocks of JavaScript and how they come to play together you’ll be able to write better code and apps. 几周前,我们开始了一系列深入挖掘JavaScript和它的工作原理的工作:我们认为通过了解JavaScript的构建块以及他们是如何共同协作的会帮助你编写出更好的代码和应用程序。

The first post of the series focused on providing an overview of the engine, the runtime, and the call stack. Thе second post examined closely the internal parts of Google’s V8 JavaScript engine and also provided a few tips on how to write better JavaScript code. 这一系列的第一部分重点介绍了引擎、运行时、调用栈的相关概述。第二部分仔细检测了谷歌的V8 JavaScript引擎的内部机制,并且提供了一些关于如何写出更好的JavaScript代码的建议。

In this third post, we’ll discuss another critical topic that’s getting ever more neglected by developers due to the increasing maturity and complexity of programming languages that are being used on a daily basis — memory management. We’ll also provide a few tips on how to handle memory leaks in JavaScript that we at SessionStack follow as we need to make sure SessionStack causes no memory leaks or doesn’t increase the memory consumption of the web app in which we are integrated. 在第三部分,由于目前正在使用的编程语言日益成熟和复杂,我们将会讨论另一个越来越被开发者忽视的重要主题 -- 内存管理。我们也会提供一些关于如何处理JavaScript内存泄露的技巧,在SessionStack中遵循我们需要确保sessionStack不会造成内存泄露或者不会增加我们集成的web应用的内存消耗的原则。(这个逼作者肯定是seesionstack的内部成员,开局结尾都在打广告)


Languages, like C, have low-level memory management primitives such as malloc() and free(). These primitives are used by the developer to explicitly allocate and free memory from and to the operating system. 部分语言,例如C语言,都有低级别的原生内存管理例如malloc()free()。开发人员使用这些原生语句显式地分配和释放操作系统的内存。

At the same time, JavaScript allocates memory when things (objects, strings, etc.) are created and “automatically” frees it up when they are not used anymore, a process called garbage collection. This seemingly “automatical” nature of freeing up resources is a source of confusion and gives JavaScript (and other high-level-language) developers the false impression they can choose not to care about memory management. This is a big mistake. 同时,JavaScript在变量(对象,字符串,等等)创建的时候为其分配内存,当不再被使用的时候会“自动地”释放这些内存,这个过程被称为垃圾回收。这个看似“自动的”释放资源的本质是一个混乱的来源,给JavaScript(和其他高等级语言)开发者可以不去关心内存管理的错误印象。这是一个很大的错误

Even when working with high-level languages, developers should have an understanding of memory management (or at least the basics). Sometimes there are issues with the automatic memory management (such as bugs or implementation limitations in the garbage collectors, etc.) which developers have to understand in order to handle them properly (or to find a proper workaround, with a minimum trade off and code debt). 即使使用高等级语言时,开发者们也应该对于内存管理有一定的理解(至少有基本的理解)。有时自动内存管理存在一些问题(例如垃圾回收的缺陷或者实施限制等等),开发者不得不为了合理地处理内存管理问题而去弄明白这些问题是怎么回事(或者找到一个合适解决方法,以最小的交易代码债务(应该是指代码的修改量、修改代码产生的关联影响等等))

Memory life cycle 内存生命周期

No matter what programming language you’re using, memory life cycle is pretty much always the same: 无论你用哪一种编程语言,内存生命周期几乎总是一样的:

Here is an overview of what happens at each step of the cycle: 这是对生命周期中的每一步大概的说明:

  • Allocate memory — memory is allocated by the operating system which allows your program to use it. In low-level languages (e.g. C) this is an explicit operation that you as a developer should handle. In high-level languages, however, this is taken care of for you.
  • 分配内存— 内存是被操作系统分配,这允许程序使用它。在低级语言中(例如C),这是一个作为开发者需要处理的显式操作。在高级语言中,然而,这些操作都代替开发者进行了处理。

  • Use memory — this is the time when your program actually makes use of the previously allocated memory. Read and write operations are taking place as you’re using the allocated variables in your code.

  • 使用内存—当你的程序实际使用以前分配的内存的时候。在你的代码中使用分配的变量时候,的操作正在对内存产生作用。(这句话翻译得不好,就是说在代码运行的时候对变量的赋值读取等操作,就是在对内存进行读写操作,也就是在使用内存)
  • Release memory — now is the time to release the entire memory that you don’t need so that it can become free and available again. As with the Allocate memory operation, this one is explicit in low-level languages.
  • 释放内存 — 现在是时候释放整个你不需要的内存空间了,因此这些内存再次变得可用。与分配内存操作一样,释放内存在低级语言中也需要显式操作。

For a quick overview of the concepts of the call stack and the memory heap, you can read our first post on the topic. 想要快速的了解堆栈和内存的概念,可以阅读这个主题的第一部分。

What is memory? 什么是内存

Before jumping straight to memory in JavaScript, we’ll briefly discuss what memory is in general and how it works in a nutshell. 在直接探讨Javascript中的内存之前,我们先简要的讨论一下什么是内存、内存大概是怎么样工作的。

On a hardware level, computer memory consists of a large number of flip flops. Each flip flop contains a few transistors and is capable of storing one bit. Individual flip flops are addressable by a unique identifier, so we can read and overwrite them. Thus, conceptually, we can think of our entire computer memory as a just one giant array of bits that we can read and write.

Since as humans, we are not that good at doing all of our thinking and arithmetic in bits, we organize them into larger groups, which together can be used to represent numbers. 8 bits are called 1 byte. Beyond bytes, there are words (which are sometimes 16, sometimes 32 bits). 作为人类,我们不善于把我们所有的思想和算术用bit运算来完成,我们把这些小东西组织成一个大家伙,这些大家伙可以用来表现数字。8个比特位被称为一个字节。在字节之上还有其他的单词(有的是表示16位,有的表示32位)

A lot of things are stored in this memory: 许多东西被存储在内存中:

  1. All variables and other data used by all programs. 所有的变量和所用程序中用到的数据

  2. The programs’ code, including the operating system’s. 程序的代码,包括操作系统的。

The compiler and the operating system work together to take care of most of the memory management for you, but we recommend that you take a look at what’s going on under the hood. 编译器和操作系统共同工作帮助开发者完成大部分的内存管理,但是我们推荐你了解一下底层到底发生了什么。

When you compile your code, the compiler can examine primitive data types and calculate ahead of time how much memory they will need. The required amount is then allocated to the program in the call stack space. The space in which these variables are allocated is called the stack space because as functions get called, their memory gets added on top of the existing memory. As they terminate, they are removed in a LIFO (last-in, first-out) order. For example, consider the following declarations: 当你在编译你的代码的时候,编译器会解析原始的数据类型,提前计算出他们需要多大的内存空间。然后将所需的数量分配给调用堆栈空间中的程序。这些变量被分配的空间被称为堆栈空间,由于在函数被调用的时候,他们的内存被添加在现有内存之上(就是会在栈的最上面添加一个堆栈帧来指向存储函数内部变量的空间)。当他们终止的时候,他们以LIFO(后进先出)的顺序被移除。例如,思考下面的声明:

int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes //4个元素的数组 每一个元素4个字节
double m; // 8 bytes

The compiler can immediately see that the code requires 编译器可以立刻观察出代码所需要的内存 4 + 4 × 4 + 8 = 28 bytes.

That’s how it works with the current sizes for integers and doubles. About 20 years ago, integers were typically 2 bytes, and double 4 bytes. Your code should never have to depend on what is at this moment the size of the basic data types. 这就是这个时代它如何处理整型和双精度的大小。大约在20年以前,整型通常只需要2个字节,双精度需要4个字节,你的代码占据的内存不必取决于现在基础数据类型的大小。

The compiler will insert code that will interact with the operating system to request the necessary number of bytes on the stack for your variables to be stored. 编辑器会插入与操作系统交互的代码,来请求堆栈中必要大小的字节来储存你的变量。

In the example above, the compiler knows the exact memory address of each variable. In fact, whenever we write to the variable n, this gets translated into something like “memory address 4127963” internally. 在上面的例子中,编辑器知道每个变量准确的地址。事实上,无论什么时候我们写变量n,将会在内部被翻译成类似“memory address 4127963”的语句。

Notice that if we attempted to access x[4] here, we would have accessed the data associated with m . That’s because we’re accessing an element in the array that doesn’t exist — it’s 4 bytes further than the last actual allocated element in the array which is x[3], and may end up reading (or overwriting) some of m’s bits. This would almost certainly have very undesired consequences for the rest of the program. 注意,如果我们尝试访问x[4]的内存(开始声明的x[4]是长度为4的数组,x[4]表示第五个元素),我们会访问m的数据。那是因为我们正在访问一个数组里不存在的元素,m比数组中实际分配内存的最后一个元素x[3]要远4个字节,可能最后的结果是读取了(或者覆盖了)m的一些位。这肯定会对其他程序产生不希望产生的结果。

When functions call other functions, each gets its own chunk of the stack when it is called. It keeps all its local variables there, but also a program counter that remembers where in its execution it was. When the function finishes, its memory block is once again made available for other purposes. 当函数调用其他函数的时候,每一个函数被调用的时候都会获得自己的堆栈块。在自己的堆栈块里会保存函数内所有的变量,还有一个程序计数器会记录变量执行时所在的位置。当函数执行完之后,会释放它的内存以作他用。

Dynamic allocation 动态分配

Unfortunately, things aren’t quite as easy when we don’t know at compile time how much memory a variable will need. Suppose we want to do something like the following: 不幸的是,事情并不是十分简单,因为在编译的时候我们并不知道一个变量将会需要多少内存。假设我们做了下面这样的事:

int n = readInput(); // reads input from the user   //读取用户的输入

// create an array with "n" elements     //创建一个有n个元素的数组

Here, at compile time, the compiler does not know how much memory the array will need because it is determined by the value provided by the user. 这里,在编译的阶段,编辑器不知道这个数组将会需要多少内存,因为整个数组的内存是由用户提供的值确定的。

It, therefore, cannot allocate room for a variable on the stack. Instead, our program needs to explicitly ask the operating system for the right amount of space at run-time. This memory is assigned from the heap space. The difference between static and dynamic memory allocation is summarized in the following table: 因此,不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确的向操作系统请求合适的空间。内存从堆空间中被分配。静态与动态分配内存之间的不同在下面的表格中被总结出来:

Differences between statically and dynamically allocated memory 静态分配内存与动态分配内存的区别。

To fully understand how dynamic memory allocation works, we need to spend more time on pointers, which might be a bit too much of a deviation from the topic of this post. If you’re interested in learning more, just let me know in the comments and we can go into more details about pointers in a future post. 为了完全理解动态内存是如何分配的,我们需要花更多的时间在指针上,这个可能很大程度上偏离了这篇文章的主题。如果你有兴趣学习更多的知识,那就在评论中让我知道,我就可以在之后的文章中写更多关于指针的细节。

Allocation in JavaScript 在JavaScript中的分配

Now we’ll explain how the first step (allocate memory) works in JavaScript. 现在我们来解释JavaScript中的第一步(分配内存)是如何工作的。

JavaScript relieves developers from the responsibility to handle memory allocations — JavaScript does it by itself, alongside declaring values. JavaScript使开发者们免除了处理内存分配的问题---JavaScript自己处理内存,在声明值的时候。

var n = 374; // allocates memory for a number  //为一个数字分配内存
var s = 'sessionstack'; // allocates memory for a string  //为字符串分配内存

var o = {
  a: 1,
  b: null
}; // allocates memory for an object and its contained values  //为一个对象和它包含的值分配内存

var a = [1, null, 'str'];  // (like object) allocates memory for the   //(像对象一样)为数组和他包含的值分配内存
                           // array and its contained values

function f(a) {
  return a + 3;
} // allocates a function (which is a callable object) //分配一个函数(可调用的对象)

// function expressions also allocate an object  //函数表达式也会分配一个对象
someElement.addEventListener('click', function() { = 'blue';
}, false);

Some function calls result in object allocation as well: //一些函数调用也会导致对象分配

`var d = new Date(); // allocates a Date object`   //分配一个Date对象的内存

`var e = document.createElement('div'); // allocates a DOM element`   //分配一个DOM元素的内存

Methods can allocate new values or objects: //方法可以分配新的值或者对象

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string  //s2是一个新的字符串
// Since strings are immutable,      //因为字符串是不可变的
// JavaScript may decide to not allocate memory,     //JavaScript可能决定不会分配内存
// but just store the [0, 3] range.  //仅仅存储 0-3的范围

var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2); 
// new array with 4 elements being   //新的数组有4个元素是a1和a2连接起来的。
// the concatenation of a1 and a2 elements

Using memory in JavaScript 在JavaScript中使用内存

Using the allocated memory in JavaScript basically, means reading and writing in it. 基本上在JavaScript中使用被分配的内存,意味着在内存里进行读和写。

This can be done by reading or writing the value of a variable or an object property or even passing an argument to a function. 这个可以通过读或者写变量的值 或者 对象的属性甚至通过一个函数的参数来完成。

Release when the memory is not needed anymore 当内存不在被需要时释放内存

Most of the memory management issues come at this stage. 大部分的内存管理问题都在这个阶段出现。

The hardest task here is to figure out when the allocated memory is not needed any longer. It often requires the developer to determine where in the program such piece of memory is not needed anymore and free it. 这里最难的任务是找出这些被分配的内存什么时候不再被需要。这常常要求开发者去决定程序中的一段内存不在被需要而且释放它。

High-level languages embed a piece of software called garbage collector which job is to track memory allocation and use in order to find when a piece of allocated memory is not needed any longer in which case, it will automatically free it. 高等级的语言嵌入了一个叫垃圾回收的软件,它的工作是跟踪内存的分配和使用,以便于发现一些内存在一些情况下不再被需要,它将会自动地释放这些内存。

Unfortunately, this process is an approximation since the general problem of knowing whether some piece of memory is needed is undecidable (can’t be solved by an algorithm). 不幸的是,这个过程是一个近似的过程(表达不能十分明确的确定是否可以被回收吧),因为一般关于知道内存是否是被需要的问题是不可判断的(不能用一个算法解决)(用我的话来说:我们不能判断这个内存是否是被需要的。)

Most garbage collectors work by collecting memory which can no longer be accessed, e.g. all variables pointing to it went out of scope. That’s, however, an under-approximation of the set of memory spaces that can be collected, because at any point a memory location may still have a variable pointing to it in scope, yet it will never be accessed again. 大部分的垃圾回收器通过收集不再被访问的内存来工作,例如, 所有的变量指向他的父级作用域。然而,这是一组可以收集的内存空间的近似值,因为在任何时候,一个内存地址可能还有一个在作用域里的变量指向它,但是它将不会被再次访问。(玛德实在翻译不好这段,大概意思是说当函数执行完的时候本应该释放内存,但是有其他的变量引用了它导致内存不能释放)

Garbage collection 垃圾收集

Due to the fact that finding whether some memory is “not needed anymore” is undecidable, garbage collections implement a restriction of a solution to the general problem. This section will explain the necessary notions to understand the main garbage collection algorithms and their limitations. 由于找到一些内存是否是“不再被需要的”这个事实是不可判定的,垃圾回收实施对于一般的问题的解决方案的限制。这个章节将会解释必要的概念去理解主要的垃圾回收算法和它们的局限性。

Memory references 内存引用

The main concept garbage collection algorithms rely on is the one of reference. 垃圾回收算法依赖的主要改了是引用

Within the context of memory management, an object is said to reference another object if the former has an access to the latter (can be implicit or explicit). For instance, a JavaScript object has a reference to its prototype (implicit reference) and to its properties’ values (explicit reference). 在内存管理的上下文中。一个对象据说是引用了另一个对象,如果前者有对后者的访问权(可能是隐形的或者显性的)。例如,JavaScript对象对他的Prototype对象有一个引用(隐式引用),对prototype对象的属性值也有引用(显式引用)。

In this context, the idea of an “object” is extended to something broader than regular JavaScript objects and also contains function scopes (or the global lexical scope). 在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广的范围,并且还包含函数作用域。(或者global词法作用域

Lexical Scoping defines how variable names are resolved in nested functions: inner functions contain the scope of parent functions even if the parent function has returned. 词法作用域定义了变量的名字在嵌套的函数中如何被解析:内部的函数包含了父级函数的作用域,即使父级函数已经return了(就是执行完了);

Reference-counting garbage collection 引用计数垃圾回收

This is the simplest garbage collection algorithm. An object is considered “garbage collectible” if there are zero references pointing to it. 这是最简单的垃圾回收算法。 一个对象在它没有其他的引用指向它的时候就被认为“可被回收的”。

Take a look at the following code: 看一下下面的代码:

var o1 = {
  o2: {
    x: 1

// 2 objects are created.  //2个对象被创建
// 'o2' is referenced by 'o1' object as one of its properties.  //'o2'被'o1'引用作为'o1'的属性
// None can be garbage-collected   //没有可以被回收的

var o3 = o1; // the 'o3' variable is the second thing that  //'o3'变量是第二个对'o1'指向的对象有一个引用
            // has a reference to the object pointed by 'o1'.

 o1 = 1;      // now, the object that was originally in 'o1' has a   //现在'o1'最初的对象有了一个单一的引用
            // single reference, embodied by the 'o3' variable     //体现在'o3'变量上

var o4 = o3.o2; // reference to 'o2' property of the object.  //引用'o3'对象的'o2'属性
                // This object has now 2 references: one as  //'o2'对象这时有了2个引用: 一个是作为对象的属性
                // a property. 
                // The other as the 'o4' variable        //另一个是作为'o4'变量

o3 = '374'; // The object that was originally in 'o1' has now zero //'o1'原来的对象现在有0个对它的引用
            // references to it. 
            // It can be garbage-collected.     //'o1'可以被垃圾回收了。
            // However, what was its 'o2' property is still   //然而他的'o2'属性依然被'o4'变量引用,所以'o2'不能被释放。
            // referenced by the 'o4' variable, so it cannot be
            // freed.

o4 = null; // what was the 'o2' property of the object originally in  
           // 'o1' has zero references to it.  //'o1'中的'o2'属性没有被其他的引用了
           // It can be garbage collected.   //'o2'可以被垃圾回收了

Cycles are creating problems 循环引用创造麻烦

There is a limitation when it comes to cycles. In the following example, two objects are created and reference one another, thus creating a cycle. They will go out of scope after the function call, so they are effectively useless and could be freed. However, the reference-counting algorithm considers that since each of the two objects is referenced at least once, neither can be garbage-collected. 在涉及循环引用的时候有一个缺陷。在下面的例子中,2个对象被创建了,而且相互引用,这样创建了一个循环。他们将会在函数调用的时候被创建,因此他们实际上是无用的,可以被释放。然而引用计数算法考虑到2个对象中的每一个至少被引用了一次,因此都不可以被回收。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1\. This creates a cycle.


Mark-and-sweep algorithm 标记清除算法

In order to decide whether an object is needed, this algorithm determines whether the object is reachable. 为了决定一个对象是否被需要。这个算法决定了对象是否可到达。

The algorithm consists of the following steps: 这个算法包含了以下步骤。

  1. The garbage collector builds a list of “roots”. Roots usually are global variables to which a reference is kept in the code. In JavaScript, the “window” object is an example of a global variable that can act as a root. 垃圾回收器生成一个列表的“根”。根通常是将引用保存在代码中的全局变量。在JavaScript中,“窗口”对象是一个可以作为根的全局变量的示例。

  2. All roots are inspected and marked as active (i.e. not garbage). All children are inspected recursively as well. Everything that can be reached from a root is not considered garbage. 所有的根都被检查和标记成活跃的(也就是不是垃圾),所有的子变量也被递归检查。所有可能从跟元素到达的都不被认为是垃圾。

  3. All pieces of memory not marked as active can now be considered garbage. The collector can now free that memory and return it to the OS. 所有没有被标记成活跃的内存都被认为是垃圾。垃圾回收器就可以释放内存并且把内存还给操作系统。

A visualization of the mark and sweep algorithm in action 上图就是标记清除的动作。

This algorithm is better than the previous one since “an object has zero reference” leads to this object being unreachable. The opposite is not true as we have seen with cycles. 这个算法就比之前的(引用计算)要好些,因为“一个对象没有被引用”导致这个对象不能被访问。相反的是不是真的在循环引用的时候如我们看到的一样。

As of 2012, all modern browsers ship a mark-and-sweep garbage-collector. All improvements made in the field of JavaScript garbage collection (generational/incremental/concurrent/parallel garbage collection) over the last years are implementation improvements of this algorithm (mark-and-sweep), but not improvements over the garbage collection algorithm itself, nor its goal of deciding whether an object is reachable or not. 在2012年,所有的现在浏览器安装了一个标记清除垃圾回收器。在过去几年中,JavaScript垃圾回收领域中的所有改进(代/增量/并行/并行垃圾收集)都是由这个算法(标记清除法)改进实现的,但是并不是对垃圾收集算法本身的改进,也不是它的目标觉得一个对象是否可到达。

In this article, you can read in a greater detail about tracing garbage collection that also covers mark-and-sweep along with its optimizations. 在这篇文章中(玛德不是这篇文章哦,是他本文有一个链接),你能够阅读更多关于跟踪垃圾回收的细节,包括了标记清除法和它的优化算法。

Cycles are not a problem anymore 循环引用不再是问题

In the first example above, after the function call returns, the two objects are not referenced anymore by something reachable from the global object. Consequently, they will be found unreachable by the garbage collector. 在上面的例子中(循环引用的那个),在函数执行完之后,这个2个对象没有被任何可以到达的全局对象所引用。因此,他们将会被垃圾回收器发现为不可到达的。

Even though there are references between the objects, they’re not reachable from the root. 尽管在这两个对象之间有相互引用,但是他们不能从全局对象上到达。

Counter intuitive behavior of Garbage Collectors 垃圾回收器的直观行为

Although Garbage Collectors are convenient they come with their own set of trade-offs. One of them is non-determinism. In other words, GCs are unpredictable. You can’t really tell when a collection will be performed. This means that in some cases programs use more memory that it’s actually required. In other cases, short-pauses may be noticeable in particularly sensitive applications. Although non-determinism means one cannot be certain when a collection will be performed, most GC implementations share the common pattern of doing collection passes during allocation. If no allocations are performed, most GCs stay idle. Consider the following scenario: 尽管垃圾回收器很方便,但是他们有一套自己的方案。其中之一就是非决定论。换句话说,GCs(我猜GC代表garbage collect垃圾回收)是不可预测的。你不可能知道一个回收器什么时候会被执行。这意味着程序在某些情况下会使用比实际需求还要多的内存。在其他情况下,在特别敏感的应用程序中,短停顿可能是显而易见的。尽管非决定论意味着不能确定回收工作何时执行。如果没有分配工作被执行,大部分的垃圾回收就保持空闲。参考下面的情况。

  1. A sizable set of allocations is performed. 执行相当大的一组分配。

  2. Most of these elements (or all of them) are marked as unreachable (suppose we null a reference pointing to a cache we no longer need). 这些元素中的大部分(或者所有的)都被标记为不可到达的(假设我们清空了一个指向我们不再需要的缓存的引用。)

  3. No further allocations are performed. 没有更多的分配被执行

In this scenario, most GCs will not run any further collection passes. In other words, even though there are unreachable references available for collection, these are not claimed by the collector. These are not strictly leaks but still, result in higher-than-usual memory usage. 在这种局面下,大多数垃圾回收将不会运行任何进一步的回收传递。换句话说,尽管这里不可到达的引用变量可供回收,这些也不会被回收器回收。这些是不严格的泄露但是仍然导致比通常情况下更多内存使用。

What are memory leaks? 什么是内存泄漏

In essence, memory leaks can be defined as memory that is not required by the application anymore but for some reason is not returned to the operating system or the pool of free memory. 从本质上来讲,内存泄漏可以被定义为不再被应用所需要的内存,但是由于某种原因,这些内存没有被归还给操作系统或者可用的内存池。

Programming languages favor different ways of managing memory. However, whether a certain piece of memory is used or not is actually an undecidable problem. In other words, only developers can make it clear whether a piece of memory can be returned to the operating system or not. 编程语言喜欢不同的管理内存方式。然而,一段确定的内存是否被使用是一个不可判断的问题。换句话说,只有开发者才能弄清楚,是否一段内存可以被还给操作系统。

Certain programming languages provide features that help developers do this. Others expect developers to be completely explicit about when a piece of memory is unused. Wikipedia has good articles on manual and automatic memory management. 某些编程语言为开发者提供了释放内存功能。另一些则期待开发者清楚的知道一段内存什么时候是没用的。Wikipedia有一篇非常好的关于内存管理的文章。

The four types of common JavaScript leaks 4种常见的JavaScript内存泄漏

1: Global variables 1:全局变量

JavaScript handles undeclared variables in an interesting way: a reference to an undeclared variable creates a new variable inside the global object. In the case of browsers, the global object is window. In other words: JavaScript用一个有趣的方式管理未被声明的变量:对未声明的变量的引用在全局对象里创建一个新的变量。在浏览器的情况下,这个全局对象是window。换句话说:

function foo(arg) {
    bar = "some text";

is the equivalent of: 等同于

function foo(arg) { = "some text";

If bar was supposed to hold a reference to a variable only inside the scope of the foo function and you forget to use var to declare it, an unexpected global variable is created. 如果bar被假定只在foo函数的作用域里引用变量,但是你忘记了使用var去声明它,一个意外的全局变量就被声明了。

In this example, leaking a simple string won't do much harm, but it could certainly be worse. 在这个例子里,泄漏一个简单的字符串不会造成很大的伤害,但是它确实有可能变得更糟。

Another way in which an accidental global variable can be created is through this: 另外一个意外创建全局变量的方法是通过this:

function foo() {
    this.var1 = "potential accidental global";

// Foo called on its own, this points to the global object (window) Foo作为函数调用,this指向全局变量(window)
// rather than being undefined.   而不是undefined

To prevent these mistakes from happening, add 'use strict'; at the beginning of your JavaScript files. This enables a stricter mode of parsing JavaScript that prevents accidental global variables. Learn more about this mode of JavaScript execution. 为了防止这些问题发生,可以在你的JaveScript文件开头使用'use strict';。这个可以使用一种严格的模式解析JavaScript来阻止意外的全局变量。

Even though we talk about unsuspected globals, it’s still the case that much code is filled with explicit global variables. These are by definition non-collectible (unless assigned as null or reassigned). In particular, global variables that are used to temporarily store and process big amounts of information are of concern. If you must use a global variable to store lots of data, make sure to assign it as null or reassign it after you are done with it. 即使我们谈论未知的全局变量,仍然存在很多代码填充显式全局变量的情况。这些都被定义为不被回收的(除非被指定为null或者重新分配)。特别是,全局变量被用来暂时存储和处理大量涉及到的信息。如果你必须要使用全局变量来存储大量数据,确保在是使用完成之后指定为null或者重新分配

2: Timers or callbacks that are forgotten 被遗忘的定时器或者回调

The use of setInterval is quite common in JavaScript. 在JavaScript中使用setInterval是十分常见的。

Most libraries, that provide observers and other facilities that take callbacks, take care of making any references to the callback unreachable after their own instances become unreachable as well. In the case of setInterval, however, code like this is quite common: 大部分的插件,提供观察器和其他设施来回调,小心对回调函数产生任何不可到达的引用即使他们自己的实例也变得不可到达。在setInterval中,像这样的代码十分常见:

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
}, 5000); //This will be executed every ~5 seconds.

This example illustrates what can happen with timers: timers that make reference to nodes or data that is no longer required. 这个例子说明了定时器可能发生什么:定时器产生了对不再需要的节点或者数据的引用

The object represented by renderer may be removed in the future, making the whole block inside the interval handler unnecessary. However, the handler cannot be collected as the interval is still active, (the interval needs to be stopped for this to happen). If the interval handler cannot be collected, its dependencies cannot be collected either. This means that serverData, which presumably stores quite a big amount of data, cannot be collected either. renderer对象在将来有可能被移除,让interval处理器内部的整个块都不被需要。然而当处理器interval仍然起作用时并不能被回收(interval在对象被移除时需要被停止),如果interval不能被回收,它的依赖也不可能被回收。这就意味着serverData,大概保存了大量的数据,也不可能被回收。

In the case of observers, it is important to make explicit calls to remove them once they are not needed anymore (or the associated object is about to be made unreachable). 在观察者的情况下,在他们不再被需要的时候明确的调用移除是非常重要的(否则相关对象将会变得不可到达)

In the past, this used to be particularly important as certain browsers (the good old IE 6) were not able to manage well cyclic references (see below for more info). Nowadays, most browsers can and will collect observer handlers once the observed object becomes unreachable, even if the listener is not explicitly removed. It remains good practice, however, to explicitly remove these observers before the object is disposed of. For instance: 在过去,这一点尤其重要,因为某些浏览器(旧的IE6)不能很好的管理循环引用(更多信息见下文)。如今,大部分的浏览器都能而且会在对象变得不可到达的时候回收观察处理器,甚至监听器没有被明确的移除掉。然而,在对象被处理之前,要显式地删除这些观察者,这仍然是好的做法。例如:

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
   element.innerHtml = 'text ' + counter;

`element.addEventListener('click', onClick);`

`// Do stuff`

element.removeEventListener('click', onClick);

// Now when element goes out of scope,   当元素被销毁时。
// both element and onClick will be collected even in old browsers // that don't handle cycles well.
//元素和事件都会即使在老的浏览器里也会被回收  //也不会处理寻混引用

Nowadays, modern browsers (including Internet Explorer and Microsoft Edge) use modern garbage collection algorithms that can detect these cycles and deal with them correctly. In other words, it’s not strictly necessary to call removeEventListener before making a node unreachable. 如今,现在的浏览器(包括IE和Edge)使用现代的垃圾回收算法,可以立即发现并处理这些循环引用。换句话说,在一个节点变得不可到达之前调用removeEventListener不是严格地需要的。

Frameworks and libraries such as jQuery do remove listeners before disposing of a node (when using their specific APIs for that). This is handled internally by the libraries which also make sure that no leaks are produced, even when running under problematic browsers such as … yeah, IE 6. 框架和插件例如jQuqery在处理节点(当使用具体的api的时候)之前会移除监听器。这个是插件内部的处理也可以确保不会产生内存泄漏,甚至运行在有问题的浏览器上 你想的没错 我特么当然说的是IE6啊

3: Closures 闭包

A key aspect of JavaScript development are closures: an inner function that has access to the outer (enclosing) function’s variables. Due to the implementation details of the JavaScript runtime, it is possible to leak memory in the following way: 闭包是JavaScript开发的一个关键方面:一个内部函数使用了外部(封闭)函数的变量。由于JavaScript运行事实的细节,它可能以下面的方式造成内存泄漏:

var theThing = null;

var replaceThing = function () {

  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // a reference to 'originalThing'

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {

setInterval(replaceThing, 1000);

This snippet does one thing: every time replaceThing is called, theThing gets a new object which contains a big array and a new closure (someMethod). At the same time, the variable unused holds a closure that has a reference to originalThing (theThing from the previous call to replaceThing). Already somewhat confusing, huh? The important thing is that once a scope is created for closures that are in the same parent scope, that scope is shared. 这些代码做了一件事:每次ReplaceThing被调用,theThing获得一个包含大数组和新的闭包(someMethod)的对象。同时,变量unused保持了一个引用originalThing(theThing是上次调用replaceThing生成的值)的闭包。已经有点困惑了吧?最重要的事情是一旦为同一父域中的作用域产生闭包,则该作用域是共享的。

In this case, the scope created for the closure someMethod is shared with unused. unused has a reference to originalThing. Even though unused is never used, someMethod can be used through theThing outside of the scope of replaceThing (e.g. somewhere globally). And as someMethod shares the closure scope with unused, the reference unused has to originalThing forces it to stay active (the whole shared scope between the two closures). This prevents its collection. 在这个案例中,作用域产生了闭包,someMethodunused共享这个闭包中的内存。unused引用了originalThing。尽管unused不会被使用,someMethod可以通过theThing来使用replaceThing作用域外的变量(例如某些全局的)。而且someMethodunused有共同的闭包作用域,unusedoriginalThing的引用强制oriiginalThing保持激活状态(两个闭包共享整个作用域)。这阻止了它的回收。

When this snippet is run repeatedly a steady increase in memory usage can be observed. This does not get smaller when the GC runs. In essence, a linked list of closures is created (with its root in the form of the theThing variable), and each of these closures' scopes carries an indirect reference to the big array, resulting in a sizable leak. 当这段代码重复执行,可以观察到被使用的内存在持续增加。垃圾回收运行的时候也不会变小。从本质上来说,闭包的连接列表已经创建了(以theThing变量为根),这些闭包每个作用域都间接引用了大数组,导致大量的内存泄漏。

This issue was found by the Meteor team and they have a great article that describes the issue in great detail. 这个问题被流行团队发现,他们有一篇非常好的文章描述了闭包大量的细节。

4: Out of DOM references 来自Dom引用

Sometimes it may be useful to store DOM nodes inside data structures. Suppose you want to rapidly update the contents of several rows in a table. It may make sense to store a reference to each DOM row in a dictionary or an array. When this happens, two references to the same DOM element are kept: one in the DOM tree and the other in the dictionary. If at some point in the future you decide to remove these rows, you need to make both references unreachable. 有的时候在数据结构里存储DOM节点是非常有用的。假如你想要去快速的更新一个表格几排的内容。存储每一排的dom节点的引用在一个字典或者数组里是非常有意义的。当你保存一个dom的2个引用:1个在dom树中,另外一个在字典中。如果在未来的某个时候你想要去移除这些排,你需要确保2个参考都不可到达。

var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image')

function doStuff() {
    image.src = '';

function removeImage() {
    // The image is a direct child of the body element.  //image是body元素的子节点

 // At this point, we still have a reference to #button in the  
    //global elements object. In other words, the button element is
    //still in memory and cannot be collected by the GC.
    //这个时候我们在全局的element对象里仍然有一个对#button的引用。换句话说,buttom元素仍然在内存中而且不能被回收。(我怀疑原文写错了 把image写成了button);

There’s an additional consideration that has to be taken into account when it comes to references to inner or leaf nodes inside a DOM tree. Say you keep a reference to a specific cell of a table (a tag) in your JavaScript code. One day you decide to remove the table from the DOM but keep the reference to that cell. Intuitively one may suppose the GC will collect everything but that cell. In reality, this won’t happen: the cell is a child node of that table and children keep references to their parents. That is, the reference to the table cell from JavaScript code causes the whole table to stay in memory. Consider this carefully when keeping references to DOM elements. 当涉及到DOM树内部或子节点时,需要考虑额外的考虑因素。例如,你在JavaScript中保持对某个表的特定单元格的引用。有一天你决定从DOM中移除表格但是保留了对单元格的引用。人们也许会认为除了单元格其他的都会被回收。实际并不是这样的:单元格是表格的一个子节点,子节点保持了对父节点的引用。确切的说,JS代码中对单元格的引用造成了整个表格被留在内存中了,所以在移除有被引用的节点时候要仔细考虑。

We at SessionStack try to follow these best practices in writing code that handles memory allocation properly, and here’s why: 我们在sessionstack努力遵循这些最佳实践在编写代码的内存分配的正确,因为:

Once you integrate SessionStack into your production web app, it starts recording everything: all DOM changes, user interactions, JavaScript exceptions, stack traces, failed network requests, debug messages, etc. 一旦你整合你的SessionStack到你的生产web应用中,他就开始记录所有的事情:所有的dom变化,用户交互,JS的例外,堆栈跟踪,失败的网络请求,调试信息等等。

With SessionStack, you replay issues in your web apps as videos and see everything that happened to your user. And all of this has to take place with no performance impact for your web app. Since the user can reload the page or navigate your app, all observers, interceptors, variable allocations, etc. have to be handled properly, so they don’t cause any memory leaks or don’t increase the memory consumption of the web app in which we are integrated. 通过SessionStack,你可以重现WEB APP中的问题作为录像,可以看到你的用户的所有动作。所有这些都不会对你的web app产生性能的影响。因此用户可以重现加载页面或者操作app,所有的观察者、拦截器、变量分配等。必须被合理的处理,因此他们不会造成任何的内存泄漏,也不会增加整个web app的内存消耗

There is a free plan so you can give it a try now. 这是一个免费的计划 你现在可以尝试一下。