在本书的开头,我向你保证,我们不会走任何捷径,用我们自己的双手,从头开始,不使用任何第三方工具,构建一个功能齐全的解释器。我们做到了! 但现在我有一个小小的告白。
考虑一下当我们在解释器中运行这段 Monkey 代码时会发生什么:
let counter = fn(x) {
if (x > 100) {
return true;
} else {
let foobar = 9999;
counter(x + 1);
}
};
counter(0);
显然,它会在对计数器的主体进行 101 次评估后返回“true”。 但是直到最后一次对计数器返回的递归调用为止,发生了很多事情。
第一件事是评估 if-else-expression 条件:x > 100。如果产生的值不真实,则评估 if-else-expression 的替代项。 在替代方案中,整数文字 9999 被绑定到名称 foobar,该名称不再被引用。 然后计算 x + 1。 然后将调用 Eval 的结果传递给另一个对 counter 的调用。 然后一切重新开始,直到 x > 100 评估为 TRUE。
重点是:在每次调用 counter 时,都会分配很多对象。 或者就我们的 Eval 函数和我们的对象系统而言:每次对计数器主体的评估都会导致大量 object.Integer 被分配和实例化。 未使用的 9999 整数文字和 x + 1 的结果是显而易见的。 但即使是文字 100 和 1 也会在每次计算 counter 主体时产生新的 object.Integers。
如果我们修改我们的 Eval 函数来跟踪 &object.Integer{} 的每个实例,我们会看到运行这个小代码片段会导致大约 400 个分配的 object.Integers。
这有什么问题?
我们的对象存储在内存中。 我们使用的对象越多,我们需要的内存就越多。 即使与其他程序相比,示例中的对象数量很少,但内存也不是无限的。
每次调用来计算我们的解释器进程的内存使用量都应该增加,直到它最终耗尽内存并且操作系统杀死它。 但是如果我们在运行上面的代码片段时监控内存使用情况,我们会看到它不会稳定上升也不会下降。 相反,它会增加和减少。 为什么?
这个问题的答案是我必须承认的核心:我们正在重用 Go 的垃圾收集器作为我们的访客语言的垃圾收集器。 我们不需要自己写。
Go 的垃圾收集器 (GC) 是我们不会耗尽内存的原因。 它为我们管理内存。 即使我们从上面多次调用计数器函数并因此添加更多未使用的整数文字和对象分配,我们也不会耗尽内存。 因为 GC 会跟踪哪些 object.Integer 仍然可以被我们访问,哪些不能。 当它注意到某个对象不再可访问时,它会再次使该对象的内存可用。
上面的示例生成了许多在调用 counter 后无法访问的整数对象:文字 1 和 100 以及无意义的 9999 绑定到 foobar。 计数器返回后无法访问这些对象。 在 1 和 100 的情况下,很明显它们是不可达的,因为它们没有绑定到名称。 但是即使是绑定到 foobar 的 9999 也是无法访问的,因为函数返回时 foobar 超出了范围。 为评估 counter 主体而构建的环境被破坏(也被 Go 的 GC 破坏,请注意!)以及 foobar 绑定。
这些无法访问的对象是无用的并且占用内存。 这就是 GC 收集它们的原因 并释放他们使用的内存。
这对我们来说非常方便!这为我们节省了很多工作!如果我们用像 C 这样的语言编写解释器,而我们没有 GC,我们需要自己实现一个来为解释器的用户管理内存。
这样一个假设的 GC 需要做什么?简而言之:跟踪对象分配和对对象的引用,为将来的对象分配留出足够的内存,并在不再需要时将内存归还。最后一点是垃圾收集的全部内容。没有它,程序会“泄漏”并最终耗尽内存。
有无数种方法可以完成上述所有任务,涉及不同的算法和实现。例如,有基本的“标记和清除”算法。为了实现它必须决定 GC 是否是分代 GC,或者它是停止世界 GC 还是并发 GC,或者它如何组织内存和处理内存碎片。在决定了所有这些之后,一个有效的实施仍然是一项艰巨的工作。
但也许你会问自己:好的,所以我们有 Go 的 GC 可用。但是我们不能为访客语言编写我们自己的 GC 并使用它吗?
抱歉不行。 我们必须禁用 Go 的 GC 并找到一种方法来接管它的所有职责。 说起来容易做起来难。 这是一项艰巨的任务,因为我们还必须自己负责分配和释放内存——使用一种默认情况下完全禁止的语言。
这就是为什么我决定不在本书中添加“让我们在 Go 的 GC 旁边编写我们自己的 GC”部分,而是重用 Go 的 GC。 垃圾收集本身是一个巨大的话题,添加解决现有 GC 的维度超出了本书的范围。 但是,我仍然希望本节让您大致了解 GC 的作用以及它解决哪些问题。 也许您现在知道如果要将我们在这里构建的解释器翻译成另一种没有垃圾收集的宿主语言该怎么办。
有了这个……我们就完成了! 我们的解释器工作。 剩下的就是扩展它并通过添加更多数据类型和函数使其更有用。
< 3.10函数和函数调用 | > 4.1数据类型&函数 |
---|