> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec17-virtual-memory-for-applications-frans/17.5-bakers-garbage-collector.md).

# 17.5 Baker's Real-Time Copying Garbage Collector

接下来我会讨论另一个例子，也就是Garbage Collector（注，后面将Garbage Collector和Garbage Collection都简称为GC），并且我也收到了很多有关Garbage Collector的问题。

GC是指编程语言替程序员完成内存释放，这样程序员就不用像在C语言中一样调用free来释放内存。对于拥有GC的编程语言，程序员只需要调用类似malloc的函数来申请内存，但是又不需要担心释放内存的过程。GC会决定内存是否还在使用，如果内存并没有被使用，那么GC会释放内存。GC是一个很好的特性，有哪些编程语言带有GC呢？Java，Python，Golang，几乎除了C和Rust，其他所有的编程语言都带有GC。

你可以想象，GC有很大的设计空间。这节课讨论的论文并没有说什么样的GC是最好的，它只是展示了GC可以利用用户空间虚拟内存特性。论文中讨论了一种特定的GC，这是一种copying GC。什么是copying GC？假设你有一段内存作为heap，应用程序从其中申请内存。你将这段内存分为两个空间，其中一个是from空间，另一个是to空间。当程序刚刚启动的时候，所有的内存都是空闲的，应用程序会从from空间申请内存。假设我们申请了一个类似树的数据结构。树的根节点中包含了一个指针指向另一个对象，这个对象和根节点又都包含了一个指针指向第三个对象，这里构成了一个循环。

![](/files/-MXHU5f-lQwwO8ebOQLD)

或许应用程序在内存中还有其他对象，但是没有别的指针指向这些对象，所以所有仍然在使用的对象都可以从根节点访问到。在某个时间，或许因为之前申请了大量的内存，已经没有内存空间给新对象了，也就是说整个from空间都被使用了。

Copying GC的基本思想是将仍然在使用的对象拷贝到to空间去，具体的流程是从根节点开始拷贝。每一个应用程序都会在一系列的寄存器或者位于stack上的变量中保存所有对象的根节点指针，通常来说会存在多个根节点，但是为了说明的简单，我们假设只有一个根节点。拷贝的流程会从根节点开始向下跟踪，所以最开始将根节点拷贝到了to空间，但是现在根节点中的指针还是指向着之前的对象。

![](/files/-MXHY5Mt8J9H9Xq6jEVz)

之后，GC会扫描根节点对象。因为程序的运行时知道对象的类型是什么，当然也就知道对象中的指针。接下来GC会将根节点对象中指针指向的对象也拷贝到to空间，很明显这些也是还在使用中的对象。当一个对象被拷贝到to空间时，根节点中的指针会被更新到指向拷贝到了to空间的对象。

![](/files/-MXHZUk7J-7vL2Ta9Q1V)

在之后的过程中，我们需要记住这个对象已经被拷贝过了。所以，我们还会存储一些额外的信息来记住相应的对象已经保存在了to空间，这里会在from空间保留一个forwarding指针。这里将对象从from空间拷贝到to空间的过程称为forward。

![](/files/-MXH_NJflePtRz5OWpMC)

接下来还剩下一个对象，我们将这个对象从from空间拷贝到to空间，这个对象还包含一个指针指向第二个对象。

![](/files/-MXHaLxor4VYu2F0rQP6)

但是通过查看指针可以看到这个对象已经被拷贝了，并且我们已经知道了这个对象被拷贝到的地址（注，也就是之前在from空间留下的forwarding指针）。所以我们可以直接更新第三个对象的指针到正确的地址。

![](/files/-MXHaklxwXsV0gfF_J2a)

现在与根节点相关的对象都从from空间移到了to空间，并且所有的指针都被正确的更新了，所以现在我们就完成了GC，from空间的所有对象都可以被丢弃，并且from空间现在变成了空闲区域。

![](/files/-MXHbWYrQQiiWFY0DfUS)

以上就是copying GC的基本思路。论文中讨论的是一种更为复杂的GC算法，它被称为Baker算法，这是一种很老的算法。它的一个优势是它是实时的，这意味着它是一种incremental GC（注，incremental GC是指GC并不是一次做完，而是分批分步骤完成）。在Baker算法中，我们还是有from和to两个空间。假设其中还是包含了上面介绍的几个对象。

![](/files/-MXHeIRaz8zijOARDLd4)

这里的基本思想是，GC的过程没有必要停止程序的运行并将所有的对象都从from空间拷贝到to空间，然后再恢复程序的运行。GC开始之后，唯一必要的事情，就是将根节点拷贝到to空间。所以现在根节点被拷贝了，但是根节点内的指针还是指向位于from空间的对象。根节点只是被拷贝了并没有被扫描，其中的指针还没有被更新。

![](/files/-MXHgYCXEY6ttGaL4a3r)

如果应用程序调用了new来申请内存，那就再扫描几个对象，并将这些对象从from空间forward到to空间。这很好，因为现在我们将拷贝heap中还在使用的所有对象的过程，拆分成了渐进的步骤。每一次调用new都使得整个拷贝过程向前进一步。

![](/files/-MXHi85kNBmXRo8YM71F)

当然应用程序也会使用这里对象所指向的指针。举个例子，现在当根节点需要读出其指向的一个对象时，这个对象仍然在from空间。这是危险的，因为我们不应该跟踪from空间的指针（注，换言之GC时的指针跟踪都应该只在同一个空间中完成）。所以每次获取一个指针指向的对象时（dereference），你需要检查对象是否在在from空间，如果是的话，将其从from空间forward到to空间。所以应用程序允许使用指针，但是编译器需要对每个指针的访问都包上一层检查，这样我们就可以保证在to空间的任何指针指向的是位于to空间的对象。我们需要确保这一点，因为在最后当GC完成了所有对象的跟踪之后，我们会清空from部分并重用这部分内存。

![](/files/-MXHnwGKEdnVGQ6bvjov)

论文对于这里的方案提出了两个问题：

* 第一个是每次dereference都需要有以上的额外步骤，每次dereference不再是对于内存地址的单个load或者store指令，而是多个load或者store指令，这增加了应用程序的开销。
* 第二个问题是并不能容易并行运行GC。如果程序运行在多核CPU的机器上，并且你拥有大量的空闲CPU，我们本来可以将GC运行在后台来遍历对象的图关系，并渐进的拷贝对象。但是如果应用程序也在操作对象，那么这里可能会有抢占。应用程序或许在运行dereference检查并拷贝一个对象，而同时GC也在拷贝这个对象。如果我们不够小心的话，我们可能会将对象拷贝两遍，并且最后指针指向的不是正确的位置。所以这里存在GC和应用程序race condition的可能。
