# 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的可能。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec17-virtual-memory-for-applications-frans/17.5-bakers-garbage-collector.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
