yfaming on Nostr: 看完了 Crafting Interpreters 第 25 章,Garbage Collection。 ...
看完了 Crafting Interpreters 第 25 章,Garbage Collection。
这一章实现了垃圾回收 Garbage Collection,使用的是简单的 Mark-Sweep 算法。
正如其名字所示,mark-sweep 的工作分两个阶段。
- mark 标记。从 root 开始,遍历或追踪 root 引用的所有对象。这是对所有可达对象的经典图遍历。
- sweep 清理。标记阶段完成后,堆中所有可达对象都已被标记。这意味着任何未标记的对象都是不可达的,可以进行回收。我们遍历所有未标记的对象,并逐一释放它们。
root 根。
root 是指虚拟机可以直接访问的任何对象(无需通过其他对象的引用)。
大多数根是全局变量或位于栈上,但还有其他一些地方。就 clox 来说,包括:
- vm 操作数栈上的值、调用栈上的值、全局变量
- compiler 使用的值
GC 的时机。现有文献对于如何进行 GC,没有给出好的答案。需要在 throughput 和 latency 之间权衡。
clox 是在内存的分配和释放函数 `reallocate` 中执行 gc 动作的。它采用了常见且简单的策略,当活动内存量的增加,我们会降低 gc 频率,以避免因重新遍历不断增长的活动对象堆而牺牲 thoughput。而当活动内存量的减少,我们会提高 gc 频率,以降低 latency。
有一类 gc bug 需要注意。clox 的 gc 可以遍历 VM 的操作数栈和调用栈,但无法访问 C 栈。如果 C 栈中有一个指针类型的局部变量,它指向的对象可能会被 gc 认为无法触达,从而错误地将其释放。一个办法是,把这个指针引用的对象放到操作数栈里面,这样,mark 阶段就会被识别为 root 从而不会被错误释放。
最后需要强调一下,clox 中所有需要在堆上分配的对象,都是由 `Obj` struct 表示,并由 `allocateObject` 完成分配。而且,`Obj` 对象形成 intrusive 链表,即 `VM.objects`。而内存的释放由 gc 负责,在 mark 阶段完成后,由 sweep 函数负责释放,它会释放内存并更新 `VM.objects`。这些基础设施,是 gc 算法的基础。
接下来,开始 Crafting Interpreters 第二部分,用 C 实现的 Lox 字节码解释器 clox。
在第一部分,用 Java 实现的 tree-walk interpreter jlox 中,我们完整实现了 Lox 解释器,包括 scanner、parser、resolver、interpreter 等等。
但是,第一部分的解释器利用了 Java 语言的许多特性。
比如,Lox 的值,全部用 Java 的 Object 类型表示。
Lox 里的 return,用 Java 的异常来实现。
而 Java 自带 GC,所以 Lox 中对象的生命周期,我们也完全没考虑过。
在第二部分,我们将使用 C 语言实现解释器。C 比 Java 更加 low-level,它不支持 OOP,不支持异常和 GC。
因此,这几个问题,我们就需要在 C 里面手动处理了。
而且,第二部分我们实现的是字节码解释器。我们需要定义字节码,并在 parsing 将 ast 编译为字节码。
用不同语言实现同一门语言,但采用略有不同的技术,可以加深我们的理解,并学习 compiler/interpreter 领域的不同主题。
Published at
2026-09-06 13:54:37 UTCEvent JSON
{
"id": "86abdf0b35ad0acfbe937c2cce59f45e2f3d9b440b8cd6b3f69680a04f92aef9",
"pubkey": "908fbc3babc322eea473a0ab1e6bf6b3adcf899288ac97b2fa79f34ad408d4e4",
"created_at": 1788702877,
"kind": 1,
"tags": [
[
"q",
"1aea59b1f714db6bebf6019f30c55e36795d9bbba666682c752163c90db1acff",
"wss://lang.relays.land/zh",
"908fbc3babc322eea473a0ab1e6bf6b3adcf899288ac97b2fa79f34ad408d4e4"
]
],
"content": "看完了 Crafting Interpreters 第 25 章,Garbage Collection。\n\n这一章实现了垃圾回收 Garbage Collection,使用的是简单的 Mark-Sweep 算法。\n\n正如其名字所示,mark-sweep 的工作分两个阶段。\n\n- mark 标记。从 root 开始,遍历或追踪 root 引用的所有对象。这是对所有可达对象的经典图遍历。\n- sweep 清理。标记阶段完成后,堆中所有可达对象都已被标记。这意味着任何未标记的对象都是不可达的,可以进行回收。我们遍历所有未标记的对象,并逐一释放它们。\n\nroot 根。\nroot 是指虚拟机可以直接访问的任何对象(无需通过其他对象的引用)。\n大多数根是全局变量或位于栈上,但还有其他一些地方。就 clox 来说,包括:\n- vm 操作数栈上的值、调用栈上的值、全局变量\n- compiler 使用的值\n\nGC 的时机。现有文献对于如何进行 GC,没有给出好的答案。需要在 throughput 和 latency 之间权衡。\n\nclox 是在内存的分配和释放函数 `reallocate` 中执行 gc 动作的。它采用了常见且简单的策略,当活动内存量的增加,我们会降低 gc 频率,以避免因重新遍历不断增长的活动对象堆而牺牲 thoughput。而当活动内存量的减少,我们会提高 gc 频率,以降低 latency。\n\n有一类 gc bug 需要注意。clox 的 gc 可以遍历 VM 的操作数栈和调用栈,但无法访问 C 栈。如果 C 栈中有一个指针类型的局部变量,它指向的对象可能会被 gc 认为无法触达,从而错误地将其释放。一个办法是,把这个指针引用的对象放到操作数栈里面,这样,mark 阶段就会被识别为 root 从而不会被错误释放。\n\n最后需要强调一下,clox 中所有需要在堆上分配的对象,都是由 `Obj` struct 表示,并由 `allocateObject` 完成分配。而且,`Obj` 对象形成 intrusive 链表,即 `VM.objects`。而内存的释放由 gc 负责,在 mark 阶段完成后,由 sweep 函数负责释放,它会释放内存并更新 `VM.objects`。这些基础设施,是 gc 算法的基础。\n\nnostr:nevent1qvzqqqqqqypzpyy0hsa6hseza6j88g9tre4ldvade7ye9z9vj7e0570nft2q348yqyvhwumn8ghj7mrpdenjuun9d3shjuewd3skuep00f5qzxrhwden5te0wfjkccte9ehx7um5wfaxstn0wfnj7qpqrt49nv0hzndkh6lkqx0np327xeu4mxam5enxstr4y93ujrd34nls7vs4y9",
"sig": "47146f18eba894d985e05fa50b9ec3ee9a02b38102c2f331790407ae223819977c71648b6d6f565a1455637ec77fe394b85abea0338e0beac4c836f62361d927"
}