Answers:
垃圾收集器会扫描堆栈-查看堆栈中的事物当前正在使用(指向)堆中的哪些事物。
垃圾收集器考虑收集堆栈内存是没有意义的,因为堆栈不是以这种方式管理的:堆栈上的所有内容都被视为“正在使用”。从方法调用返回时,堆栈使用的内存将自动回收。堆栈空间的内存管理是如此简单,廉价和容易,以至于您不希望涉及垃圾回收。
(在诸如smalltalk之类的系统中,堆栈帧是存储在堆中的头等对象,并且像所有其他对象一样收集垃圾。但这不是当今流行的方法。Java的JVM和Microsoft的CLR使用硬件堆栈和连续内存)
转个问题。真正的激励问题是,在什么情况下我们可以避免垃圾收集的成本?
嗯,首先,什么是垃圾回收的成本?有两个主要费用。首先,您必须确定什么还活着;这可能需要大量工作。其次,您必须缩小释放在两个仍然存在的事物之间分配的事物时形成的漏洞。那些孔是浪费的。但是压缩它们也很昂贵。
我们如何避免这些费用?
显然,如果您可以找到一种存储使用模式,在该模式中您从不分配长寿的东西,然后分配短寿的东西,然后分配长寿的东西,则可以消除漏洞的成本。如果可以保证对于存储的某个子集,每个后续分配的寿命都比该存储中的前一个分配寿命短,那么该存储中就不会有任何漏洞。
但是如果我们解决了漏洞问题,那么我们也解决了垃圾收集问题。您的存储中是否还有还活着的东西?是。一切都分配了吗?是的-该假设是我们消除漏洞的可能性。因此,您要做的就是说“最近的分配是否还活着?” 而且您知道该存储中的所有数据仍然有效。
我们是否有一组存储分配,可以知道每个后续分配的寿命都比先前分配的寿命短?是! 方法的激活框架总是以与创建它们相反的顺序销毁,因为它们的寿命总是比创建它们的激活寿命短。
因此,我们可以将激活框架存储在堆栈中,并且知道永远不需要收集它们。如果堆栈上有任何帧,则其下的整个帧的寿命更长,因此不需要收集它们。它们将按照与创建它们相反的顺序被销毁。因此消除了激活帧的垃圾收集成本。
这就是为什么我们首先将临时池放在堆栈上的原因:因为这是一种实现方法激活而又不会引起内存管理损失的简便方法。
(当然垃圾收集存储器的成本称为通过引用上的激活帧仍然存在。)
现在考虑一个控制流系统,其中激活框架不会以可预测的顺序被破坏。如果短暂激活会导致长期激活,会发生什么情况?您可能会想象,在这个世界上,您将无法再使用堆栈来优化收集激活的需求。激活集可以再次包含孔。
C#2.0具有的形式的此功能yield return
。在稍后的时间(下一次调用MoveNext的情况下)将重新激活产生收益的方法,并且这种情况何时发生是不可预知的。因此,通常将在迭代器块的激活帧的堆栈上存储的信息存储在堆中,在收集枚举器时将在此处进行垃圾收集。
同样,C#和VB的下一版本中的“异步/等待”功能将使您可以创建方法,这些方法在执行该方法的过程中定义明确的点处将其激活“屈服”和“恢复”。由于不再以可预测的方式创建和销毁激活帧,因此所有以前存储在堆栈中的信息都必须存储在堆中。
几十年来,我们碰巧决定,带有以严格有序方式创建和销毁的激活框架的语言是时髦的,这只是历史的偶然。由于现代语言越来越缺乏这种特性,因此希望看到越来越多的语言将连续性变成垃圾收集的堆,而不是堆栈。
最明显的答案(也许不是最完整的答案)是,堆是实例数据的位置。实例数据是指表示在运行时创建的类(又称为对象)实例的数据。这些数据本质上是动态的,这些对象的数量以及它们占用的内存量仅在运行时才知道。恢复该内存可能会有些痛苦,否则长时间运行的程序会逐渐消耗该内存中的所有内存。
类定义,常量和其他静态数据结构消耗的内存本质上不太可能未经检查地增加。由于每个未知数量的类的运行时实例在内存中只有一个类定义,因此这种结构类型不会对内存使用构成威胁是有道理的。
值得牢记的是我们进行垃圾回收的原因:因为有时很难知道何时释放内存。您确实只有堆有此问题。分配在堆栈上的数据最终将被释放,因此实际上并不需要进行垃圾回收。通常假定数据部分中的内容是为程序的生存期分配的。
它们的大小是可以预测的(除了堆栈,常数,并且堆栈通常限制为几个MB),并且通常很小(至少与大型应用程序可以分配的数百MB相比)。
动态分配的对象通常在较短的时间内可以到达。此后,就无法再引用它们了。将其与数据部分中的条目,全局变量等进行对比:通常,有一段代码直接引用它们(请考虑const char *foo() { return "foo"; }
)。通常,代码不会改变,因此该引用会保留下来,并且每次调用该函数时都会创建另一个引用(就计算机所知,此引用可以随时出现-除非您解决了暂停问题,即)。因此,您无论如何都无法释放大部分内存,因为它总是可以访问的。
在许多垃圾回收语言中,属于正在运行的程序的所有内容都是堆分配的。在Python中,根本没有任何数据部分,也没有栈分配的值(存在局部变量的引用,并且有调用栈,但int
在C中,这两个值都不具有相同的含义)。每个对象都在堆上。
正如许多其他响应者所说的那样,堆栈是根集的一部分,因此,对其进行扫描以查找引用,但并不对其进行“收集”。
我只想回应一些暗示堆栈上的垃圾无关紧要的评论。这样做是因为它可能导致堆上的更多垃圾被认为是可到达的。认真的VM和编译器编写者要么无效,要么从扫描中排除堆栈中的无效部分。在IIRC中,某些VM具有将PC范围映射到堆栈插槽活动性位图的表,而其他VM则使插槽无效。我不知道目前首选哪种技术。
用于描述这一特殊考虑的一个术语是“ 空间安全”。
让我指出您和其他许多人错误的一些基本误解:
“为什么垃圾收集仅清除堆?” 相反。只有最简单,最保守和最慢的垃圾收集器才能清除堆。这就是为什么他们这么慢。
快速垃圾收集器仅清除堆栈(以及可选的其他一些根,例如FFI指针的某些全局变量和活动指针的寄存器),并且仅复制堆栈对象可访问的指针。其余的被丢弃(即被忽略),根本不扫描堆。
由于堆的大小大约是堆栈的1000倍,因此这种堆栈扫描GC通常要快得多。〜15ms,而普通大小的堆为250ms。由于它是将对象从一个空间复制(移动)到另一个空间,因此它通常被称为半空间复制收集器,它需要2倍的内存,因此在诸如内存不多的手机之类的小型设备上大多不可用。它非常紧凑,因此对缓存非常友好,与简单的标记和清除堆扫描器不同。
由于指针在移动,因此FFI,身份和引用非常棘手。身份通常通过随机ID(通过转发指针进行引用)解决。FFI非常棘手,因为异物无法保留指向旧空间的指针。FFI指针通常保存在单独的堆域中,例如使用缓慢的标记扫掠静态收集器。或带有refcounting的琐碎malloc。请注意,malloc具有巨大的开销,并且需要更多的更新。
“标记并清除”的实现很简单,但不应在实际程序中使用,尤其不要作为标准收集器来学习。这种最快速的堆栈扫描复印收集器中最著名的就是切尼两指收集器。
堆栈上分配了什么?局部变量和返回地址(以C表示)。当函数返回时,其局部变量将被丢弃。扫栈并不是必需的,甚至是有害的。
许多动态语言以及Java或C#都是用系统编程语言(通常是C)实现的。您可以说Java是使用C函数实现的,并且使用C局部变量,因此Java的垃圾回收器不需要清除堆栈。
有一个有趣的例外:Chicken Scheme的垃圾收集器确实(以某种方式)清除了堆栈,因为其实现使用堆栈作为第一代垃圾收集空间:请参阅Chicken Scheme Design Wikipedia。