JS工作原理:內存管理与常见内存泄露分析

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

在几周前我们就开始了一系列针对JavaScript工作原理的挖掘:我们认为当我们认识JavaScript的组成部分以及了解它们是如何搞在一起的,我们就可以写出更好的代码和应用。

该系列的第一篇文章重点介绍了引擎,运行时和调用堆栈 an overview of the engine, the runtime, and the call stack。第二篇文章阐述了Google V8 JavaScript引擎内部,并提供了一些有关如何编写更好的JavaScript代码的建议 How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code

在第三篇文章,我们来讨论另一个重要的话题,由于日常使用的编程语言的日益成熟和复杂性,开发人员更加容易忽略的一点 ———— 内存管理。我们会讲一下关于我们是如何处理我们网站上JavaScript泄露的问题。

概述

语言,像C语言会提供一个低级别的内存管理原函数,如malloc()free()。这些原函数用来让开发者分配和释放操作系统里的内存。

同时,当事物(对象,字符串等)被创建时,JavaScript分配内存,并且“自动的”在不再使用时释放它们,这个过程成为垃圾回收(GC)。看似自动的释放资源其实是一个混乱的根源,它给了JavaScript的开发人员(或其他的高级语言)一个错误的印象,他们可以选择不是关心内存管理,这个想法是错误的!

即使当我们使用着高级的语言,开发者都应该要了解内存管理机制(或者至少了解基础)。有时候开发者需要了解自动内存管理可能会遇到的问题(例如bugs或者垃圾回收的实现限制等等)以达到解决它们的目的。

内存的生命周期

不管你再用什么语言,内存的生命周期都是大体相同的。
Memory life cycle

然后说说每个生命周期步骤发生了什么:

  • 分配内存 —— 内存通过操作系统来被分配给我们的程序使用。在低级的语言(例如C语言),这个步骤是需要开发者去负责和操作的。而对于高级语言来说,我们可以省心很多。
  • 使用内存 —— 这时我们的程序确实使用分配过来的内存的时间。我们的代码正在使用分配的变量来进行操作。
  • 释放内存 —— 当我们不需要使用这部分内存的时候就要释放它们,让它们重新变成空闲和可用状态。跟分配内存操作一样,在低级语言是需要开发者去处理的。

要快速了解调用堆栈和内存堆的概念,可以阅读我们的第一篇文章。

什么是内存

在直接讨论JavaScript的内存管理之前,我们先来简要讨论一下一般的内存以及它的工作原理。

在硬件层次,计算机内存是由大量的触发器组成的。每个触发器包含几个晶体管,并且能够存储一个位。单个触发器可通过唯一的标识符寻址,因此我们可以读取并覆盖它们。因此,我们可以把整个计算机的内存看作是我们可以阅读和写入的大量位数组。

自从作为人类,我们并不善于进行基于位的思考和算术。我们将它们组织成更大的组,使得它们可以一起组合用于表示数字。8位表示1字节。除了字节以外,还有字(有时是16位,有时是32位,当然还有64位)。

许多东西都会存在这个内存中:
1.所有的变量以及被程序使用的其他数据。
2.程序的代码,包含操作系统的。

编译器和操作系统共同为你管理内存,但是我们建议你还是可以去看看底层的东西。

当在编译你的代码的时候,编译器可以检查原始数据类型,并提前计算出需要多少内存。然后按照将所需的数量分配给调用栈空间中的程序。这些变量被分配的空间称为堆栈空间,它们的内存会加到现有内存的顶部。当它们结束的时候,它们会通过LIFO(后进先出)的顺序移除栈空间。例如下面的声明:

1
2
3
4
5
6
7
8



int n; // 4 bytes

int x[4]; // array of 4 elements, each 4 bytes

double m; // 8 bytes

编译器可以立即看到这段代码需要的空间为:
4 + 4 %uD7 4 + 8 = 28 bytes.

这是当前可行的integers和doubles的大小。在20年前的时候,integers是2bytes,double是4bytes。你的代码永远不要依赖目前的数据类型的大小。

编译器会