内存管理/垃圾收集
我们已经看到内存管理有多么复杂,特别是如果你手动分配和释放内存。值得庆幸的是,有一类系统称为 **垃圾收集器**,可以帮助自动执行内存回收过程。
垃圾收集器 (GC) 是用于自动管理动态内存的系统或子系统。它们的工作原理如下。
- 与直接调用
malloc
和free
不同,它们被替换为 GC 中名为 "gc_malloc
" 的函数。显然,在实践中这个函数可以命名为任何东西。 - 当我们在程序中调用
gc_malloc
时,垃圾收集器会调用malloc
从系统中分配内存,并找到某种方法来跟踪内存。跟踪内存有很多方法,我们将在以后的章节中讨论其中的一些方法。 - 程序像往常一样运行,在需要时从 GC 中分配内存,但从不显式释放内存。
- GC 间歇性地执行一个称为 **追踪** 的函数。这可能是一个同步或异步事件。在追踪期间,GC 从一组对系统立即可见的内存对象开始,称为内存对象的 **根集**。它跟踪这些内存对象中的指针指向子对象。当它到达一个对象时,它会将其标记为 **活动** 的。
- 当 GC 完成追踪且不再有指针可供跟踪时,回收阶段就会开始。所有未标记为活动的对象都被认为是 **死亡** 的,因为程序中没有指针指向它们,因此程序不可能访问它们。GC 会释放所有死亡对象。
垃圾收集器主要有两种类型,尽管通常会在这些类型之间采用混合方法来满足特定需求。第一种类型是可能最直观的,称为 **引用计数** 收集器。第二种类型,与我们上面描述的类型最相似,称为 **追踪** 收集器。
当 GC 分配一个新的内存对象时,它会给该对象一个整数计数字段。每次指向该对象的指针,即引用,计数都会增加。只要计数是一个正非零整数,该对象就处于活动引用状态并且仍然活动。
当对该对象的引用被移除时,计数会递减。当计数达到零时,该对象就会死亡,并且可以立即回收。
关于引用计数收集器,有一些要点需要记住。
- 循环引用永远不会被回收,即使整个对象集都死亡了。
- 引用计数是普遍存在的:整个程序必须意识到该系统,并且每个指针引用或取消引用都必须伴随着适当的增量或递减。即使在一个大型程序中只维护一次计数失败,也会为你的程序造成内存问题。
- 引用计数可能很昂贵,因为必须对每个指针操作进行计数操作,并且在每次递减时都必须将计数与零进行比较。这些操作如果使用得足够频繁,会为你的程序带来性能损失。
这些类型的收集器通常被称为 **协作收集器**,因为它们需要系统其余部分的协作来维护计数。
追踪收集器与引用计数收集器完全不同,并且拥有相反的优势和劣势。
当追踪 GC 分配一个新的内存块时,GC 不会创建计数器,但它会创建一个标志来确定何时标记了该项目,以及一个指向 GC 保留的该对象的指针。这些标志不是由程序本身操作的,而是在 GC 执行运行时由 GC 操作的。
在 GC 运行期间,程序执行通常会停止。这会导致程序出现间歇性暂停,如果要追踪的内存对象很多,这些暂停可能会非常长。
GC 会选择一组对当前程序范围和父范围可用的根对象。从这些对象开始,GC 会识别对象中所有指针,称为子对象。对象本身会被标记为活动,然后收集器会移动到每个子对象并以相同的方式标记它们。内存对象形成了某种树形结构,GC 使用递归或基于堆栈的方法遍历这棵树。
在 GC 运行结束时,当不再有子对象可供标记时,所有未标记的对象都被认为是不可到达的,因此是死亡的。所有死亡对象都会被收集。
关于追踪 GC,有一些要点需要记住。
- 追踪 GC 可以用于查找循环,即指针形成循环结构的内存对象。引用计数方案无法做到这一点。
- 追踪 GC 会导致程序暂停,这些暂停在某些使用大量小型内存对象的复杂程序中可能会变得过长。
- 死亡对象不会立即被回收。回收只会在 GC 运行之后发生。这会导致内存使用效率低下。
- 追踪收集器不需要程序明确地计算内存计数或内存状态更新。所有内存跟踪逻辑都存储在 GC 本身中。这使得为这些系统编写扩展更容易,并且也使得在现有系统中安装追踪 GC 比安装引用计数 GC 更容易。
追踪 GC 通常被称为 **非协作收集器**,因为它们不需要系统其余部分的协作才能正常运行。
有时,引用计数方案会利用追踪系统来查找循环垃圾。追踪系统可能会在非常大的对象上使用引用计数,以确保它们能够快速回收。这仅仅是两种混合垃圾收集器的例子,它们比上面描述的两种“纯”类型更常见。
在后面的章节中,我们将更详细地讨论垃圾收集器及其算法。
按任意顺序排列,
-
垃圾收集之前的示例。
-
垃圾收集之后的示例。