垃圾回收如何以本地编译的语言工作?


79

浏览了堆栈溢出的几个答案之后,很明显,一些本机编译的语言具有垃圾回收功能。但是我不清楚这将如何工作。

我了解垃圾收集如何与解释语言一起工作。垃圾收集器将仅与解释器一起运行,并从程序的内存中删除未使用和无法访问的对象。他们俩一起跑。

但是,如何使用编译语言呢?我的理解是,一旦编译器将源代码编译为目标代码(特别是本机代码),就完成了。它的工作完成了。那么,编译后的程序又如何被垃圾回收呢?

在执行程序删除“垃圾”对象时,编译器是否以某种方式与CPU一起工作?还是编译器在编译程序的可执行文件中包括一些最小的垃圾回收器。

我相信我的后一种说法将比前一种更具有效性,这是由于从Stack Overflow的答案中摘录的:

一种这样的编程语言是埃菲尔。大多数Eiffel编译器出于可移植性原因而生成C代码。该C代码用于通过标准C编译器生成机器代码。埃菲尔实现为该已编译的代码提供GC(有时甚至是精确的GC),并且不需要VM。特别是,VisualEiffel编译器直接在完全GC支持下生成了本机x86机器代码

最后一条语句似乎暗示编译器在最终的可执行文件中包含一些程序,该程序在运行时充当垃圾回收器。

在页面d语言的关于垃圾收集的网站这是本地编译并具有可选的垃圾收集器- -似乎也暗示了一些后台程序运行沿着原来的可执行程序来实现垃圾回收。

D是一种支持垃圾回收的系统编程语言。通常,没有必要显式释放内存。只需根据需要进行分配,垃圾收集器就会定期将所有未使用的内存返回到可用内存池。

如果方法上面提到的使用,究竟会工作的呢?编译器是否存储一些垃圾回收程序的副本并将其粘贴到它生成的每个可执行文件中?

还是我的思维有缺陷?如果是这样,使用什么方法来实现编译语言的垃圾回收,它们将如何工作?


1
如果此问题的亲密投票者能确切说明出什么问题可以解决,我将不胜感激。
Christian Dean

6
如果您接受GC基本上是特定编程语言实现所需的库的一部分的事实,那么您的问题要点与GC本身无关,而与静态链接与动态链接有关
Theodoros Chatzigiannakis

7
您可以将垃圾收集器视为实现该语言等效于的运行时库的一部分malloc()
Barmar

9
垃圾收集器的操作取决于分配器的特性,而不取决于编译模型。分配器知道已分配的每个对象。它分配了他们。现在,您需要的是某种方式来知道哪些对象仍然存在,并且收集器可以取消分配除它们之外的所有对象。该描述中与编译模型没有任何关系。
埃里克·利珀特

1
GC是动态内存的功能,而不是解释器的功能。
德米特里·格里戈里耶夫

Answers:


52

编译语言中的垃圾回收与解释语言中的垃圾回收相同。像Go这样的语言使用跟踪垃圾收集器,即使它们的代码通常会提前被编译为机器代码。

(跟踪)垃圾收集通常从遍历当前正在运行的所有线程的调用堆栈开始。这些堆栈上的对象始终是活动的。此后,垃圾收集器遍历活动对象指向的所有对象,直到发现整个活动对象图。

显然,这样做需要C等语言不提供的额外信息。特别是,它需要每个函数的堆栈框架的映射,其中包含所有指针的偏移量(可能还有它们的数据类型),以及包含相同信息的所有对象布局的映射。

但是,很容易看出具有强类型保证的语言(例如,如果不允许将指针强制转换为不同的数据类型)确实可以在编译时计算这些映射。它们只是将指令地址与堆栈框架映射之间的关联以及数据类型与对象布局映射之间的关联存储在二进制文件内。然后,此信息使他们可以进行对象图遍历。

垃圾收集器本身不过是一个链接到程序的库,类似于C标准库。例如,malloc()如果内存压力很高,该库可以提供类似于运行收集算法的功能。


9
在实用程序库和JIT编译之间,“编译为本机”和“在运行时环境中运行”之间的界限变得越来越模糊。
corsiKa

6
补充一点GC不支持的语言:的确,C和其他此类语言不提供有关调用堆栈的信息,但是如果您可以接受某些平台特定的代码(通常包括一些代码),汇编代码)仍然可以实现“保守的垃圾收集”。该贝姆GC是这个在现实生活中使用的程序的例子。
Matti Virkkunen

2
@corsiKa或者更确切地说,这一行要清晰得多。现在我们看到那些是不同的不相关的概念,而不是彼此的反义词。
Kroltan

4
在编译时和解释时运行时中需要注意的另一种复杂性与答案中的这一句话有关:“(跟踪)垃圾回收通常从遍历当前正在运行的所有线程的调用堆栈开始。” 我在编译环境中实现GC的经验是,仅跟踪堆栈是不够的。起点通常是将线程挂起足够长的时间以从其寄存器进行跟踪,因为它们可能在那些尚未存储在堆栈中的寄存器中具有引用。对于口译员来说,通常不是这样...
Jules

……这是一个问题,因为环境可以安排GC在“安全点”进行,解释器知道所有数据都安全地存储在解释的堆栈中。
Jules

123

编译器是否存储某些垃圾回收程序的副本并将其粘贴到其生成的每个可执行文件中?

听起来很古怪而奇怪,但是是的。编译器具有一个完整的实用程序库,其中不仅包含垃圾回收代码,还包含许多其他内容,对该库的调用将插入到其创建的每个可执行文件中。这称为运行时库,您会惊讶于它通常执行多少个不同的任务。


51
@ChristianDean注意,即使C都具有运行时库。尽管它没有GC,但是它仍然通过该运行时库执行内存管理:malloc()并且free()未内置于该语言中,也不是操作系统的一部分,而是该库中的函数。C ++有时也使用垃圾回收库进行编译,即使该语言在设计时并未考虑到GC。
阿蒙

18
C ++还包含一个运行时库dynamic_cast,即使不添加GC,它也可以执行make 和exception这样的工作。
塞巴斯蒂安·雷德尔

23
运行时库不一定复制到每个可执行文件中(称为静态链接),只能在运行时对其进行引用(包含该库的二进制文件的路径)并进行访问:这是动态链接。
mouviciel

16
也不需要编译器直接跳入程序的入口点,而不会发生任何其他情况。我正在做出一个有根据的猜测,即每个编译器实际上在调用之前都会插入一堆特定于平台的初始化代码main(),例如在该代码中启动GC线程是完全合法的。(假定在内存分配调用中没有完成GC。)在运行时,GC仅真正需要知道对象的哪些部分是指针或对象引用,并且编译器需要发出代码以将对象引用转换为指针。如果GC重定位对象。
millimoose

15
@millimoose:是的。例如,在GCC,这段代码是crt0.o(代表“ Ç řŤ IME,非常基础”),用于获取与每一个节目(或至少每程序,它是未链接自由站立)。
约尔格W¯¯米塔格

58

还是编译器在编译后的程序代码中包括一些最小的垃圾回收器。

这是一种奇怪的说法,“编译器将程序与执行垃圾回收的库链接在一起”。但是,是的,就是这样。

这没什么特别的:编译器通常将大量的库链接到它们所编译的程序中。否则,如果不从头开始重新实现许多事情,已编译的程序将无法完成很多工作:即使将文本写入屏幕/文件/…也需要一个库。

但是也许GC与这些其他库不同,其他库提供用户调用的显式API?

否:在大多数语言中,除了GC之外,运行时库在没有面向公众的API的情况下也会执行许多幕后工作。考虑以下三个示例:

  1. 异常传播和堆栈展开/析构函数调用。
  2. 动态内存分配(即使没有垃圾回收,通常也不只是像C中那样调用函数)。
  3. 跟踪动态类型信息(用于强制转换等)。

因此,垃圾收集库一点都不特殊,先验与程序是否提前编译无关。


这似乎并没有提供任何实质性的制作上分和解释最多的回答之前发布3小时
蚊蚋

11
@gnat我觉得它很有用/必要,因为到目前为止,最主要的答案还不够强大:它提到了类似的事实,但是它并没有指出,单单收集垃圾是完全人为的区别。从根本上讲,OP的假设有缺陷,而最高答案没有提到这一点。我的确实做到了(同时避免了相当残酷的术语“有缺陷的”)。
Konrad Rudolph

并不是所有的特别之处,但是我想说这有些特别,因为通常人们将库视为他们从代码中明确调用的东西。而不是基本语言语义的实现。我认为OP在这里的错误假设是,编译器仅以或多或少的直接方式来翻译代码,而不是使用作者未指定的库调用对其进行检测。
millimoose

7
@millimoose运行时库在无需显式用户交互的情况下以多种方式在后台运行。考虑异常传播和堆栈展开/析构函数调用。考虑动态内存分配(即使没有垃圾回收,通常也不只是像C中那样调用函数)。考虑处理动态类型信息(用于强制转换等)。因此,GC确实不是唯一的。
康拉德·鲁道夫

3
是的,我承认我的措辞很奇怪。那仅仅是因为我对编译器的实际运行情况持怀疑态度。但是现在我想到了,它的确有意义得多。编译器可以像标准库的其他任何部分一样简单地链接垃圾收集器。我相信我的一些困惑源于将垃圾收集器仅视为解释器实现的一部分,而不是本身独立的程序。
Christian Dean

23

但是,如何使用编译语言呢?

你的措辞是错误的。一个编程语言是一种规范编写一些技术报告(对于一个很好的例子,见R5RS)。实际上,您指的是某些特定的语言实现(这是一种软件)。

(某些编程语言的规范不正确,甚至缺少规范,或者仅与某些示例实现相符;尽管如此,编程语言还是定义了一种行为 -例如,它具有语法语义 -它不是软件产品,但可能是实现由一些软件产品,很多编程语言有几种实现方式;特别是,“编译”是一个形容词适用于实现 -即使有些编程语言更容易口译不是由编译器实现)。

我的理解是,一旦编译器将源代码编译为目标代码(特别是本机代码),就完成了。它的工作完成了。

注意,解释器和编译器的含义很松散,某些语言实现可能被视为两者兼而有之。换句话说,两者之间是连续的。阅读最新的《Dragon Book》,并考虑字节码JIT编译动态地发出被编译为某个“插件”的C代码,然后由同一进程dlopen(3)编辑(在当前机器上,此速度足够快以与交互式REPL,请参阅


我强烈建议您阅读GC手册需要一本书来回答。在此之前,请阅读垃圾收集维基页面(我假设您已阅读以下内容)。

编译语言实现的运行时系统包含垃圾回收器,并且编译器正在生成适合该特定运行时系统的代码。特别是,分配原语(被编译为机器代码)将(或可能)调用运行时系统。

那么,编译后的程序又如何被垃圾回收呢?

仅通过发出使用(且“友好”且“兼容”)运行时系统的机器代码即可。

请注意,您可以找到几个垃圾收集库,特别是Boehm GCRavenbrook的MPS甚至是我的(未维护的)Qish。而且,编写简单的 GC并不是很困难(但是,调试起来更困难,而编写具有竞争力的 GC则很困难)。

在某些情况下,编译器将使用保守的 GC(例如Boehm GC)。这样,就没有太多要编写的代码了。保守的GC有时会(当编译器调用其分配例程时,或整个GC例程时)扫描整个调用堆栈,并假定从调用堆栈可以(间接)访问的任何内存区域均处于活动状态。之所以称为保守 GC,是因为丢失了键入信息:如果调用堆栈上的整数恰好看起来像某个地址,则会跟随它,依此类推。

在其他(更困难的)情况下,运行时提供了分代复制垃圾收集(典型示例是Ocaml编译器,该编译器使用此类GC将Ocaml代码编译为机器代码)。然后的问题是精确地在调用堆栈中找到所有指针,其中一些指针由GC 移动。然后,编译器生成描述运行时使用的调用堆栈帧的元数据。因此,调用约定ABI变得特定于该实现(即编译器)和运行时系统。

在某些情况下,由编译器生成的机器代码(实际上甚至指向它的闭包本身都是垃圾收集的SBCL(一种很好的Common Lisp实现)尤其如此,它为每个REPL交互生成机器代码。这也需要一些元数据来描述代码及其内部使用的调用框架。

编译器是否存储一些垃圾回收程序的副本并将其粘贴到它生成的每个可执行文件中?

有点。不过,运行时系统可以是共享库,等等。有时(在Linux和其他几个POSIX系统),它甚至可以是一个脚本解释器,例如传递到(2)的execve家当。或ELF解释器,请参见elf(5)PT_INTERP,等等。

顺便说一句,当今大多数带有垃圾回收语言的编译器(及其运行时系统)都是免费软件。因此,请下载源代码并进行研究。


5
您的意思是,有许多没有明确说明的编程语言实现。是的,我同意这一点。但我的观点是,编程语言不是软件(例如某种编译器或某种解释器)。它具有某种语法和一种语义(也许都没有明确定义)。
巴西尔·斯塔林凯维奇

4
@KonradRudolph:这完全取决于您对“正式”和“规范”的定义:-D有ISO / IEC 30170:2012 Ruby编程语言规范,该规范指定了Ruby 1.8和1.9的一小部分。有Ruby Spec Suite,这是一组边界情况的示例,它们充当一种“可执行规范”。然后,David Flanagan和Yukihiro Matsumoto撰写的Ruby编程语言
约尔格W¯¯米塔格

4
另外,Ruby文档。关于Ruby Issue Tracker的问题的讨论。关于ruby-core(英语)和ruby-dev(日语)邮件列表的讨论。社区的常识性期望(例如Array#[]O(1)最坏情况,Hash#[]O(1)摊销最坏情况)。最后但并非最不重要的是:马茨的大脑。
约尔格W¯¯米塔格

6
@KonradRudolph:重点是:即使没有正式规范,只有一种补充的语言也可以分为“语言”(抽象规则和限制)和“实现”(根据这些规则和代码处理程序)限制)。尽管一个琐碎的实现,但实现仍然产生了一个规范,即:“代码所做的就是规范”。毕竟,这就是编写ISO规范,RubySpec和RDocs的方式:通过运用witth和/或逆向工程MRI。
约尔格W¯¯米塔格

1
很高兴您带来了Bohem的垃圾收集器。我建议对OP进行研究,因为它是一个很好的例子,说明即使“附加”到现有的编译器上,垃圾收集也可以多么简单。
Cort Ammon

6

已经有了一些好的答案,但是我想清除这个问题背后的一些误解。

本身没有“本地编译语言”这样的东西。例如,相同的Java代码在我的旧手机(Java Dalvik)上进行了解释(然后在运行时进行了部分即时编译),并在(我的)新手机(ART)上进行了(提前)编译。

本机运行代码和解释运行代码之间的区别远没有看起来那么严格。两者都需要一些运行时库和某些操作系统才能运行(*)。解释后的代码需要一个解释器,但是解释器只是运行时的一部分。但这还不严格,因为您可以用(即时)编译器替换解释器。为了获得最佳性能,您可能需要两者(桌面Java运行时包含一个解释器和两个编译器)。

无论如何运行代码,其行为都应相同。分配和释放内存是运行时的任务(就像打开文件,启动线程等一样)。用您的语言,您只是写作new X()或类似。语言规范说明应该发生什么,然后运行时执行。

分配了一些可用内存,调用了构造函数,等等。如果没有足够的内存,则会调用垃圾收集器。因为您已经在运行时(运行时是本机代码),所以解释器的存在根本无关紧要。

在代码解释和垃圾回收之间确实没有直接的联系。仅仅是像C这样的低级语言是为速度和对所有内容的细粒度控制而设计的,这与非本机代码或垃圾收集器的思想不太吻合。因此,只有一种相关性。

在过去,这是非常正确的,例如,Java解释器很慢,而垃圾收集器的效率很低。如今,事情已经大不相同了,谈论解释性语言已经失去了任何意义。


(*)至少在谈论通用代码时,不要使用引导加载程序和类似内容。


Ocaml和SBCL都是本机编译器。因此,有 “本地编译语言”的实现。
巴西尔·斯塔林凯维奇

@BasileStarynkevitch WAT?命名一些不太知名的编译器如何与我的答案相关?SBCL作为原始解释语言的编译器,是否不是我认为区分没有意义的主张?
maaartinus

Common Lisp(或任何其他语言)不会被解释或编译。它是一种编程语言(一种规范)。它的实现可以是编译器,也可以是解释器,也可以是介于两者之间的内容(例如,字节码解释器)。SBCL是Common Lisp的交互式编译实现。Ocaml也是一种编程语言(使用字节码解释器和本机编译器作为实现)。
巴西尔·斯塔林凯维奇

@BasileStarynkevitch我就是这个意思。1.没有诸如解释语言或编译语言这样的东西(尽管C很少被解释,而LISP以前很少被编译,但这并不重要)。2.大多数知名语言都有解释,编译和混合实现,没有任何语言排除编译或解释。
maaartinus

6
我认为您的论点很有道理。grok的关键点在于,您总是要运行“本机程序”或“从不”,但是您希望看到它。Windows本身没有可执行文件。它需要一个加载程序和其他OS功能才可以启动,实际上也被部分“解释”了。对于.net可执行文件,这一点变得更加明显。java myprog是尽可能多或尽可能少的天然grep myname /etc/passwdld.so myprog:它是接收一个参数,并用该数据执行操作的可执行(无论该装置)。
Peter A. Schneider

3

各个实现之间的细节有所不同,但通常是以下各项的组合:

  • 包含GC的运行时库。这将处理内存分配并具有其他一些入口点,包括“ GC_now”函数。
  • 编译器将为GC创建表,以便它知道引用哪些数据类型的字段。对于每个功能的堆栈框架也将执行此操作,以便GC可以从堆栈进行跟踪。
  • 如果GC是增量的(GC活动与程序交错)或并发的(在单独的线程中运行),则编译器还将包含特殊的目标代码,以在更新引用时更新GC数据结构。两者在数据一致性方面存在类似的问题。

在增量式和并发GC中,编译后的代码和GC需要配合以维护某些不变性。例如,在复制收集器中,GC的工作原理是将活动数据从空间A复制到空间B,而留下垃圾。对于下一个循环,它将翻转A和B并重复。因此,一条规则可以是确保用户程序在任何时候尝试引用空间A中的对象时,都会检测到该对象,并将该对象立即复制到空间B中,程序可以在该空间继续访问它。转发地址留在空间A中,以向GC指示已发生这种情况,以便在跟踪对象时对对象的任何其他引用进行更新。这被称为“读取屏障”。

从60年代开始就对GC算法进行了研究,并且有大量有关该主题的文献。Google如果您需要更多信息。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.