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空间申请内存。假设我们申请了一个类似树的数据结构。树的根节点中包含了一个指针指向另一个对象,这个对象和根节点又都包含了一个指针指向第三个对象,这里构成了一个循环。

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

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

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

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

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

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

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

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

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

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

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

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

  • 第一个是每次dereference都需要有以上的额外步骤,每次dereference不再是对于内存地址的单个load或者store指令,而是多个load或者store指令,这增加了应用程序的开销。

  • 第二个问题是并不能容易并行运行GC。如果程序运行在多核CPU的机器上,并且你拥有大量的空闲CPU,我们本来可以将GC运行在后台来遍历对象的图关系,并渐进的拷贝对象。但是如果应用程序也在操作对象,那么这里可能会有抢占。应用程序或许在运行dereference检查并拷贝一个对象,而同时GC也在拷贝这个对象。如果我们不够小心的话,我们可能会将对象拷贝两遍,并且最后指针指向的不是正确的位置。所以这里存在GC和应用程序race condition的可能。

Last updated