内存管理速成手册

1,042 阅读5分钟
原文链接: github.com

原文地址

由于个人能力知识有限,翻译过程中难免有纰漏和错误,还望指正

这是本系列文章中的第一篇:

  1. 内存管理速成手册
  2. 通过漫画形式来解释 ArrayBuffers 和 SharedArrayBuffers
  3. 使用 Atomics 来在 SharedArrayBuffers 中避免竞用条件

在弄懂 ArrayBuffer 和 SharedArrayBuffer 为什么添加到 JavaScript 之前,你首先需要了解一些关于内存管理的知识。你可以把机器中的内存比喻成一组箱子,就像我把内存想象成办公室内部的信箱一样,或者是为学龄前儿童准备的用于存储杂物的小房间,如果你想给某位孩子准备一些礼物,你可以将物品放到某个箱子里。

A column of boxes with a child putting something in one of the boxes

在每个箱子旁边都有一个与之对应的数字,这就是内存地址。正是因为有了地址,你才能够告诉别人你为其准备动物品存放的位置。每个箱子具有相同的尺寸,也因此每个内存箱子也具有相同的容量来存储信息。箱子的尺寸是根据不同的机器而定的。箱子的尺寸被称作「字长」。它通常被标识为「32位」或者「64位」。但是为了简单的展示,在本文中我们使用「8位」的字长。(译者注:一个字长包含 8 个二进制位,也就是说一个内存单元的容量是 8 位)

A box with 8 smaller boxes in it

如果你打算将数字 2 放进其中一个内存箱子里,这将非常容易做到,因为数字可以很容易通过二进制来表示。

The number two, converted to binary 00000010 and put inside the boxes

倘若我们想放入内存箱子中的不是数字,而比如是字母「H」,怎么办呢?我们需要通过某种方法将其转化成可以使用数字来表示。为了完成此项工作,我们需要编码。类似于 UTF-8 。同时我们需要某种工具来按照 UTF-8 中的对应关系将字符转化成数字...比如说一个编码环。有了编码和编码环后,我们就可以将任意字符存入到内存箱子中了。

The letter H, put through an encoder ring to get 72, which is then converted to binary and put in the boxes

当我们打算将我们存入内存箱子中的信息取出时,我们需要将其放入一个解码器中,通过解码器将存放的数字转换成字母「H」。当你使用 JavaScript 工作时,你无须关心内存是怎样分配和使用的,因为在 JavaScript 中内存是自动管理的,内存管理和你的代码完全隔离。这意味着你不能够直接操作内存。JS 引擎将作为中介的角色,帮我们管理内存。

A column of boxes with a rope in front of it and the JS engine standing at that rope like a bouncer

让我们一些 JS 代码,比如在 React 中,我们需要创建一个变量并对其赋值。

Same as above, with React asking the JS engine to create a variable

JS 引擎的工作就是通过编码器将变量名转换成二进制表示。

The JS engine using an encoder ring to convert the string to binary

然后在内存中找到闲余的空间用来存放上面转换后的二进制表示。这一过程被称作分配内存。

The JS engine finding space for the binary in the column of boxes

接下来,JS 引擎会跟踪该变量并判断在程序中该变量是否还能够获取到。如果该变量不能够再被获取到,那么该内存箱子将会被回收再利用,以便 JS 引擎能够分配新的值到该内存中。

The garbage collector clearing out the memory

JS 引擎监听变量所代表的字符串、对象、以及内存中的其他数据类型的数据,当这些值不能再被获取到的时候,JS 引擎将会把它们清除出内存,这一过程被称作「垃圾回收」。比如 JavaScript 语言,代码不能够直接操纵内存,被称作自动内存管理语言。这一自动内存管理机制会使得开发变得相对简单。但是自动内存管理也会带来一些头疼的地方。比如自动内存管理可能会带来性能不可预测。而手动进行内存管理的语言就不会有这些问题。比如,通过 C 语言内存管理的方式来写 React 代码(当然,通过WebAssembly 已经使得其成为现实)。C 语言没有 JavaScript 自动内存管理的这一层功能抽象。所以,你可以直接操作内存,你可以从内存中对去数据,你也可以操作内存将数据存入内存中。

A WebAssembly version of React working with memory directly

当你讲其它语言比如 C 语言传递给 WebAssembly,你使用的工具将会添加一些代码到 WebAssembly 中,比如,将添加对字节进行编码和解码的代码。这些代码被称作运行时环境。运行时环境也将像 JS 引擎在 JavaScript 语言中的作用一样,处理一些与之相同的工作。

An encoder ring being shipped down as part of the .wasm file

但是对于手动内存管理的语言来说,运行时环境并不包含垃圾回收。这就意味着你必须手动来进行垃圾回收,即使是手动内存管理的语言,你通常也可以从该语言运行时环境中获取一些帮助。比如,在 C 语言中,C 语言运行时将会跟踪那些未被使用的内存,并将内存地址存储在一个链表中,该列表被称作「free list」。

A free list next to the column of boxes, listing which boxes are free right now

你可以使用 malloc 函数(memory allocate 简写)来请求运行时环境来寻找能够存放你数据的内存地址。这会使得这些内存地址从「free list」中移除。当你使用数据完成工作后,你必须通过free函数来讲该内存释放。这样该内存地址将会被重新添加至「free list」中。你必须知道什么时候该调用这些函数。这也是为什么称为手动内存管理的原因所在 -- 你完全自己管理程序中的内存。作为开发者,断定什么时候该清除内存是一件相当困难的事。如果在错误的时间点清除内存,将导致程序 bug,甚至一些安全漏洞。如果不对不在使用的内存进行处理,又将导致内存用尽。这也就是为什么现代语言都是用自动内存管理的原因 -- 避免人为错误。但是这也将会产生一些性能上的问题。我将在下一篇对此进行说明。