为什么C#中没有引用计数和垃圾回收机制?

65

我来自C++背景,已经用C#工作了一年。和许多人一样,我感到困惑的是为什么语言本身没有内置确定性资源管理机制。相比于确定性析构函数,我们有了Dispose模式。因此,人们开始怀疑在他们的代码中传播IDisposable的影响是否值得努力。

在我被C++所影响的大脑中,使用具有确定性析构函数的引用计数智能指针似乎比需要实现IDisposable并调用Dispose来清理非内存资源的垃圾回收器是一个重大进步。诚然,我不是很聪明...所以我纯粹是想更好地了解为什么事情会变成现在这个样子。

如果修改C#,使得:

对象采用引用计数方式。当对象的引用计数降为零时,在该对象上确定性地调用资源清理方法,然后标记该对象进行垃圾回收。垃圾回收将在某个非确定性时间点发生,此时将回收内存。在这种情况下,您无需实现IDisposable或记得调用Dispose。如果您有非内存资源要释放,只需实现资源清理功能即可。

  • 这样做为什么不好?
  • 这会否破坏垃圾收集器的目的?
  • 实现这样的东西是否可行?

编辑: 根据迄今为止的评论,这是一个不好的想法,因为

  1. 没有引用计数的GC更快
  2. 处理对象图中的循环问题

我认为第一点是有效的,但第二点可以使用弱引用轻松解决。

那么,速度优化是否超过以下缺点:

  1. 可能无法及时释放非内存资源
  2. 可能过早释放非内存资源

如果您的资源清理机制是确定性的且内置于语言中,则可以消除这些可能性。


5
苹果刚刚宣布,iOS 5 中的 Objective C 将支持“自动引用计数”。所有 Objective C 指针都将自动进行引用计数和保留释放。但是,Objective C 仍支持 dealloc,这提供了一个确定性的“析构函数”,您可以在其中释放非内存资源,无需处理 IDisposable。他们明确表示不会为 Objective C 支持垃圾回收。在我看来,这听起来是正确的方法。我认为 Java 和 .NET 在这方面做得不对。 - Skrymsli
4
我担心的一件事情是,在垃圾回收(GC)过程中,即使实现了弱事件模式,事件订阅者仍然会在等待进行垃圾回收时继续接收通知。这就需要在所有事件订阅者类中强制实现IDisposable接口。 - user815431
1
是的,从被接受的答案中可以看出:“我们认为解决循环问题非常重要,而不强迫程序员理解、追踪和设计这些复杂的数据结构问题。”今天我被迫理解、追踪和设计复杂的数据结构和对象生命周期交互,以找出为什么内存没有被释放。有人忘记在Dispose()中取消订阅全局事件处理程序的对象。反应式扩展使一切都可以被处理。 - Skrymsli
你说的“非内存资源”是什么意思? - Assimilater
文件句柄、互斥锁、本地代码引用。除了内存之外,为您的进程分配的任何需要清理的系统资源。 - Skrymsli
1
关于原因#1(没有引用计数的GC更快),那种速度是相对的。我们设计了处理数百万个长期存在的类实例的系统,并发现性能大大降低(构建GC可达图成为一个杀手)。在这种情况下,引用计数的成本将被引用跟踪的成本所掩盖。我们已经采取了a)将类迁移到结构体中,以便容器只需要跟踪支持数组引用 - 和b)使用非托管内存。您的提议将极大地受益于我们这样的情况。 - BlueStrat
10个回答

56

Brad Abrams发布了一封来自Brian Harry的电子邮件,该电子邮件是在开发.Net框架期间编写的。它详细说明了为什么即使早期的优先事项之一是保持与使用引用计数的VB6的语义等效性,也没有使用引用计数。它探讨了诸如有些类型进行引用计数而其他类型不进行 (IRefCounted!),或对特定实例进行引用计数的可能性,以及为什么这些解决方案都被认为不可接受。

由于[资源管理和确定性终结问题]是一个非常敏感的话题,我将尽可能准确和完整地解释。很抱歉这个邮件长度很长。这封邮件前90%试图说服您,问题确实很难。在最后一部分中,我将谈论我们正在尝试做的事情,但您需要第一部分才能了解为什么我们正在看这些选项。
...
我们最初的想法是,解决方案将采用自动引用计数的形式(这样程序员就不会忘记),再加上一些其他的东西来自动检测和处理循环。... 最终,我们得出结论,在一般情况下,这种方法行不通。
...
总之:
我们认为至关重要的是解决循环问题,而不强迫程序员理解、跟踪和设计这些复杂的数据结构问题。
我们希望确保我们拥有高性能(速度和工作集)的系统,我们的分析表明,对于系统中的每个对象都使用引用计数将无法实现这个目标。
由于各种原因,包括组合和转换问题,没有简单透明的解决方案,只需对那些需要进行引用计数的对象进行计数。
我们选择不选择为单个语言/上下文提供确定性终结的解决方案,因为它会影响与其他语言的互操作性,并通过创建特定于语言的版本来导致类库的分裂。

7
请务必仔细阅读完整封邮件——其中详细解释了该决定的原因。 - Daniel Brückner
3
微软已经破坏了他们所有的博客链接。我认为这是与此回复中链接的资源管理文章有关的新主页。我花了一段时间才找到它。 - Adam Davidson

31

垃圾回收器不需要你为定义的每个类/类型编写Dispose方法。只有在需要显式清理时(即已经分配了本地资源时)才会定义一个Dispose方法。大多数情况下,即使只是像new()这样创建对象,GC也会回收内存。

GC使用引用计数,但是它以一种不同的方式进行,通过在每次进行集合时找到可达的对象(Ref Count>0)... 它并不以整数计数的方式进行。 不可访问的对象将被收集(Ref Count=0)。这样,运行时就不必在每次分配或释放对象时进行管理/更新表格...应该更快。

C++(确定性)和C#(非确定性)之间唯一的主要区别在于对象何时被清除。在C#中,您无法预测对象将被收集的确切时间。

最后推荐:如果您真的对GC如何工作感兴趣,我建议阅读Jeffrey Richter在CLR via C#中的关于GC的章节。


3
因此,避免使用RAII是因为引用计数更慢,而且您不必在每个类上处理Dispose。对我来说,这似乎是一个糟糕的权衡。 - Skrymsli
2
我不理解你在这里的评论... 我只是解释了实现Dispose并不是每个C#类型/类都必须的。即使您的类没有实现Dispose,GC也可以完美地工作。 - Gishu
1
我不确定我的问题是否被完全理解。框架中有许多类实现了IDisposable接口,必须在这些类上调用dispose方法。如果能够使用RAII来帮助处理这个问题就太好了。无论如何,似乎大多数人都同意你的观点。 - Skrymsli
10
事实上,并不是每个IDisposable的实现类都必须调用dispose方法。如果对象编写得当/具有终结器,资源仍将被收集。IDisposable提供了一种确定性释放的方式,如果你需要确定性释放,就可以使用它。如果你只是偶尔获取特定操作系统资源,那么非确定性终结可能已经足够好了。把它看作是将经典的C++“选择加入”概念扩展到资源管理。 - Greg D
2
@Skrymsli:Rex M是正确的。可以放心地假设框架中的所有内容(这是你在我的评论之前提到的)都是写得很好的。 - Greg D
显示剩余2条评论

23

参考计数在C#中曾经被尝试过。我相信,发布Rotor的人们(一个CLR的参考实现,其源代码是可用的)只是为了看看基于参考计数的垃圾收集器与代际式垃圾收集器相比如何。结果出乎意料——“原生”的垃圾收集器速度快得让人难以置信。我不记得当时从哪里听到这个消息了,我想可能是Hanselmunte播客中的一个。如果你想要看到C++在性能比较方面被C#彻底碾压的情况,可以搜索Raymond Chen的汉语词典应用程序。他做了一个C++版本,然后Rico Mariani做了一个C#版本。我认为Raymond花了6次迭代才最终打败了C#版本,但那时他不得不放弃了所有优雅的C++的面向对象特性,降到了win32 API级别。整个过程变成了一个性能hack。同时,C#程序只优化了一次,在最终看起来仍像一个体面的面向对象项目。


4
参考计数法一般比追踪式垃圾回收慢,但它具有更低的最大暂停时间和通常会尽快释放内存的好处。 - Unknown
6
从我的记忆中,我认为转子运行时并没有进行过很好的优化,所以它不是一个很好的数据点。感谢你提供有趣的字典应用参考。这是该参考链接:http://blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx - Luke Quinane
8
尽管如此,事实仍然是,在目前可用的工具下,编写高效的C#代码比编写高效的C++代码更容易、更快、更少出错。 - Greg D
8
引用计数具有更低的最大暂停时间,而且内存通常会尽快释放。相反,引用计数会引入无界的暂停时间,因为整个堆可在析构函数和引用计数的雪崩下被收集,并且引用计数会推迟收集,直到作用域结束,这可能比生存期结束要晚(而且经常如此)。 - J D
1
@LukeQuinane:所以当你说“更快”时,你指的是延迟而不是吞吐量? - J D
显示剩余5条评论

15

智能指针的引用计数和垃圾回收的引用计数有所不同。我也在我的博客中讨论了它们之间的差异,但以下是一个快速摘要:

C++风格的引用计数:

  • 减法操作会产生成本问题:如果大型数据结构的根被减为零,则释放所有数据将产生成本问题。

  • 手动循环收集:为防止循环数据结构泄漏内存,程序员必须手动使用弱智能指针来打破任何潜在的结构。这是可能存在缺陷的另一个来源。

引用计数垃圾回收

  • 延迟引用计数更改: 对于堆栈和寄存器引用,对象引用计数的更改将被忽略。相反,在触发GC时,这些对象通过收集根集来保留。 可以推迟引用计数的更改并进行批处理。这会导致更高的吞吐量

  • 合并:通过使用写屏障,可以合并引用计数的变化。这使得可以忽略大部分对象引用计数的变化,从而提高经常执行引用计数操作的性能。

  • 循环检测:对于完整的GC实现,还必须使用一个循环检测器。然而,可以以增量方式执行循环检测,这反过来意味着有界的GC时间。

基本上,可以为Java的JVM和.NET CLR运行时实现高性能的RC基垃圾回收器。

我认为追踪收集器在某种程度上是由于历史原因而被使用:许多最近的引用计数改进是在JVM和.NET运行时发布之后进行的。研究工作也需要时间才能转化为生产项目。

确定性资源处理

这基本上是一个单独的问题。.NET运行时使用IDisposable接口使其成为可能,以下是示例。我还喜欢Gishu的回答。


@Skrymsli,这就是“using”关键字的目的。例如:

public abstract class BaseCriticalResource : IDiposable {
    ~ BaseCriticalResource () {
        Dispose(false);
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this); // No need to call finalizer now
    }

    protected virtual void Dispose(bool disposing) { }
}

然后,要添加一个带有关键资源的类:

public class ComFileCritical : BaseCriticalResource {

    private IntPtr nativeResource;

    protected override Dispose(bool disposing) {
        // free native resources if there are any.
        if (nativeResource != IntPtr.Zero) {
            ComCallToFreeUnmangedPointer(nativeResource);
            nativeResource = IntPtr.Zero;
        }
    }
}
  

那么使用它就非常简单:

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Some actions on fileResource
}

// fileResource's critical resources freed at this point

请参见正确实现IDisposable


2
有趣的博客文章...我不太关心释放内存,而是释放那些可能具有时间关键性的非内存资源。 (想想一个持有文件锁的COM对象句柄。您希望在完成后释放该锁定,而不是在GC在未来某个时间运行终结器时释放它。)我认为必须有一些GC和智能指针的组合,提供既能获得出色的内存管理,又能在不给程序员带来过多负担的情况下确定地释放关键资源的最佳解决方案。 - Skrymsli
@Skrymsli,你并不孤单,我几年前也建议过这个:http://www.dotnet247.com/247reference/msgs/26/131667.aspx - alpav
2
可以为Java的JVM和.Net CLR运行时实现基于RC的高性能垃圾收集器。使用跟踪而不是RC的Production GC,正是因为这不是真的。 - J D
1
@LukeQuinane他们声称引用计数“可以在对象不再被引用时立即回收”,但随后承认这将是“极其昂贵的”,并声明他们“不知道任何依赖引用计数的高性能系统”。此外,假装引用计数比跟踪收集更增量是错误的。天真的引用计数会导致雪崩,但可以使其成为增量式。天真的标记-清除是批处理操作,但Dijkstra风格的三色标记可以使其变得与增量式一样。 - J D
我想看到实验测量来证明他们关于“恢复的迅速性”的说法。根据我的经验,基于作用域的引用计数比分代标记清除产生更多的浮动垃圾,并且速度慢得多。延迟会提高性能,但也会增加浮动垃圾的数量。顺便说一句,我使用的唯一基于引用计数的生产系统是Mathematica,它非常糟糕(非常缓慢,长时间暂停)。 - J D
显示剩余6条评论

8
我来自C++背景,已经使用C#约一年。像许多人一样,我感到困惑的是为什么确定性资源管理不内置于语言中。 using结构提供了"确定性"资源管理,并内置于C#语言中。请注意,所谓"确定性"是指在using块后执行代码之前保证已调用Dispose。请注意,这并不是"确定性"的实际含义,但每个人似乎都滥用它,在这种情况下非常糟糕。
在我这个偏向C++的大脑中,使用具有确定性析构函数的引用计数智能指针似乎比需要实现IDisposable并调用dispose清理非内存资源的垃圾收集器更加重要。
垃圾收集器不需要你实现IDisposable。事实上,GC完全无视它。
承认,我并不是很聪明...所以我纯粹是想更好地理解为什么事情会变成这样。
跟踪垃圾收集是一种快速可靠的方法,可以模拟无限内存机器,使程序员免于手动内存管理的负担。这消除了几类错误(悬空指针、过早释放、双重释放、忘记释放)。
如果修改C#,使得:
对象进行引用计数。当对象的引用计数降至零时,在对象上确定地调用资源清理方法,
考虑一个在两个线程之间共享的对象。线程竞争将引用计数减少到零。其中一个线程会赢得比赛,而另一个线程将负责清理。这是不确定的。认为引用计数本质上是确定性的是一种谬论。
另一个常见的谬论是,引用计数在程序中最早可能的点上释放对象。它并不是。减量总是被推迟,通常推迟到作用域结束。这使得对象的生存时间比必要的时间更长,留下所谓的“浮动垃圾”。请注意,特别是一些跟踪垃圾回收器可以并且确实比基于作用域的引用计数实现更早地回收对象。
然后标记对象进行垃圾收集。垃圾收集在未来的某个非确定性时间发生,此时回收内存。在这种情况下,您不必实现IDisposable或记住调用Dispose。

您不必为垃圾回收对象实现IDisposable,因此这并没有什么好处。

如果有非内存资源需要释放,只需实现资源清理函数即可。

为什么这是个坏主意?

天真的引用计数非常缓慢且泄漏循环。例如,Boost在C++中的shared_ptr比OCaml的跟踪GC慢了多达10倍。即使是天真的基于作用域的引用计数,在多线程程序存在的情况下也是不确定的(几乎所有现代程序都是如此)。

那样做会否背离垃圾回收器的初衷?

完全不会。实际上,这是一个在1960年代发明的坏主意,并在接下来的54年中受到了激烈的学术研究,得出结论:引用计数在一般情况下都很糟糕。

实现这样的东西可行吗?

绝对可以。早期的.NET和JVM原型使用了引用计数。他们也发现它很糟糕,并放弃了它,转而采用跟踪GC。

编辑:从迄今为止的评论来看,这是一个糟糕的想法,因为

垃圾回收不需要引用计数更快

是的。请注意,您可以通过延迟计数器的递增和递减使引用计数速度更快,但这会牺牲您非常渴望的确定性,并且仍然比具有当前堆大小的跟踪GC慢。但是,引用计数在渐近意义下更快,因此在将来当堆变得非常大时,也许我们会开始在生产自动内存管理解决方案中使用RC。

处理对象图中的循环的问题

试探删除是一种专门设计用于检测和收集引用计数系统中循环的算法。然而,它很慢且不确定。

我认为第一点是有效的,但是使用弱引用很容易处理第二点。

把弱引用称为“容易”是希望战胜现实的胜利。它们是一场噩梦。它们不仅不可预测且难以架构,而且还会污染API。

那么速度优化是否超过了您面临的缺点:

可能无法及时释放非内存资源

using 是否能够及时释放非内存资源?

可能会过早释放非内存资源 如果您的资源清理机制是确定性的并且内置于语言中,您可以消除这些可能性。 使用构造是确定性的并内置于语言中。 我认为您真正想问的问题是为什么IDisposable不使用引用计数。我的回答是轶事:我已经使用垃圾收集语言18年了,从来没有需要使用引用计数。因此,我更喜欢简单的API,不会被弱引用等偶然复杂性污染。

我希望能看到一种标准模式来处理存在多个独立引用的不可变对象的情况,该对象持有资源(例如位图或其他永远不会被修改的GDI对象),而且无法确定哪一个引用将是最后需要它的。如果在每次调用“AddOwner”时增加计数,而不是在每个引用副本上增加计数,并在“Dispose”时递减计数,那么类似引用计数的东西就很好了。因此,一个方法既可以通过创建新的位图来满足对位图的请求... - supercat
它可以通过返回对一个不再感兴趣的位图的引用,或者返回对一个没有被修改过的共享位图的引用来实现。也可以返回一个新的位图,其中没有调用AddOwner,或者在共享位图上调用AddOwner并返回对其的引用。接收方随后可以在任何情况下调用共享位图上的Dispose方法,只有当该接收方恰好是最后一个需要保持其存活的实体时,才会处理该位图。 - supercat
@supercat:我在想,在使用跟踪垃圾收集的环境中,引用计数是否能够很好地工作。对于从堆中永远不被引用的对象,这可能效果良好。 - J D
1
我认为关键是要跟踪包括所有权利益的引用。这些数量变化不会像存在的引用那样频繁。此外,我认为追踪垃圾回收作为引用计数的非常有用的补充,不是作为当事情没有被释放时的后备,而是相反的:确保如果资源在代码尝试使用它时被释放,代码可以找到它,而不仅仅是漫游到未定义行为区域。 - supercat

5
我了解一些垃圾回收的知识。以下是简短的总结,因为完整的解释超出了这个问题的范围。
.NET使用复制和压缩的分代垃圾回收器。这比引用计数更先进,并且能够收集直接或通过链相互引用的对象。
引用计数无法收集循环引用。引用计数的吞吐量较低(总体速度较慢),但最大暂停时间较短,优于跟踪式垃圾回收器。

1
这一点可能存在争议,但似乎在使用弱引用避免引用计数系统中的循环比使用IDisposable避免资源泄漏或延迟释放更容易。 - Skrymsli
1
@Skrymsli 实际上这一点是无法争辩的。有时候你不知道何时会创建一个引用循环。 - Unknown
1
仅采用天真的引用计数实现将无法收集循环引用。对于像.net这样的运行时,也可以实现高性能的引用计数:http://cs.anu.edu.au/~Steve.Blackburn/pubs/papers/urc-oopsla-2003.pdf - Luke Quinane
@LukeQuinane:“主要是使用引用计数垃圾收集器”。分配的所有对象中有多少比例能够存活到进行引用计数?少于50%,对吧?那么它怎么算是“主要”采用RC呢? - J D
@JonHarrop 这将根据运行的应用程序而变化很大。这还取决于旧一代对象的生存时间有多长(有时会有数百个GC)。您可以查看一些“真实世界”程序如何分配其对象(不幸的是没有功能语言基准测试,这将非常有趣):http://users.cecs.anu.edu.au/~steveb/dacapobench.org/dacapo-graphs/index.html在GC中总是存在某种权衡。 - Luke Quinane
显示剩余2条评论

4
这里涉及到很多问题。首先需要区分释放托管内存和清理其他资源之间的区别。前者可能非常快,而后者可能非常慢。在.NET中,这两者是分开的,这允许更快速地清理托管内存。这也意味着,只有在有托管内存以外的东西需要清理时才应该实现Dispose/Finalizer。
.NET采用标记和扫描技术,在遍历堆查找对象根时。根实例将幸存于垃圾回收中。其他所有内容都可以通过简单地回收内存来进行清理。垃圾回收器不时必须压缩内存,但除此之外,即使在回收多个实例时,回收内存也是一个简单的指针操作。与C++中多次调用析构函数相比较。

2
+1 是为了注意到两个不同的方面。人们往往认为 Dispose() 是用于释放内存的... - Lucas
自从我第一次提出这个问题已经过去了10年,现在我已经习惯了GC。今天重新阅读这些评论时,再次强调我不需要IDisposable来使GC工作。我问这个问题时就知道了。这是无关紧要的。抱怨的是必须调用dispose。我仍然希望回到过去,当时查找内存泄漏就像运行valgrind一样简单。在C#中,有一堆分析器,我觉得很难使用。如今,在C#中内存泄漏的典型来源是事件订阅,在Dispose中取消订阅,但没有调用Dispose。 - Skrymsli

1

确定性非内存资源管理是该语言的一部分,但不是通过析构函数实现的。

你的观点在来自C++背景的人中很普遍,试图使用RAII设计模式。在C++中,你唯一能保证某些代码将在作用域结束时运行(即使抛出异常),就是在堆栈上分配一个对象并将清理代码放在析构函数中。

在其他语言(如C#、Java、Python、Ruby、Erlang等)中,您可以使用try-finally(或try-catch-finally)来确保清理代码始终会运行。

// Initialize some resource.
try {
    // Use the resource.
}
finally {
    // Clean-up.
    // This code will always run, whether there was an exception or not.
}

在C#中,你也可以使用using结构:

using (Foo foo = new Foo()) {
    // Do something with foo.
}
// foo.Dispose() will be called afterwards, even if there
// was an exception.

因此,对于C++程序员来说,将“运行清理代码”和“释放内存”视为两个不同的事情可能会有所帮助。将您的清理代码放在一个finally块中,让垃圾回收器负责处理内存即可。

1

实现IDisposable接口的对象在用户没有显式调用Dispose时,也必须实现一个终结器由GC调用-请参阅MSDN上的IDisposable.Dispose

IDisposable的整个目的是GC在某些非确定性时间运行,因此您实现IDisposable是因为持有宝贵的资源并希望在确定性时间释放它。

因此,您的建议在IDisposable方面不会有任何改变。

编辑:

抱歉。 没有正确阅读您的建议。 :-(

维基百科对引用计数GC的缺点有简单的解释。


就此而言,维基百科关于垃圾回收和引用计数的内容大多是错误的,包括你引用的那一节。 - J D

1

引用计数

使用引用计数的成本有两个方面:首先,每个对象都需要特殊的引用计数字段。通常,这意味着必须在每个对象中分配额外的存储字。其次,每当一个引用被分配给另一个引用时,引用计数必须进行调整。这显著增加了赋值语句所需的时间。

.NET 中的垃圾回收

C# 不使用对象的引用计数。相反,它维护了从堆栈中的根开始导航的对象引用图,并覆盖所有引用的对象。图中所有引用的对象都被压缩到堆中,以便为未来的对象提供连续的内存。对于所有不需要完成的未引用对象,将回收其内存。那些未引用但具有要在后台执行的终结器的对象,则移动到称为 f-reachable 队列的单独队列中,垃圾回收器会在后台调用它们的终结器。

除了上述内容,GC还使用代的概念来进行更有效的垃圾回收。它基于以下概念: 1. 对托管堆的一部分进行内存整理比对整个托管堆进行内存整理更快。 2. 新对象的生命周期较短,旧对象的生命周期较长。 3. 新对象往往与彼此相关,并在应用程序周围同时访问。
托管堆被分成三代:0、1和2。新对象存储在第0代中。未在GC周期中回收的对象将提升到下一代。因此,如果处于第0代中的新对象在GC周期1中存活,则它们将被提升到第1代。其中在GC周期2中存活的对象将提升到第2代。由于垃圾收集器仅支持三代,因此在第2代中存活的对象在未来的回收中被确定为不可达时,会保留在第2代中。
垃圾回收器在第0代满了且需要为新对象分配内存时执行回收。如果第0代的回收不能回收足够的内存,垃圾回收器可以先回收第1代再回收第0代。如果这样仍然不能回收足够的内存,垃圾回收器可以回收第2代、第1代和第0代。
因此,垃圾回收比引用计数更有效率。

从技术上讲,这是一个图形而不是树形结构,并且垃圾回收器并不会“维护”它,它只是遍历对象内部的引用。此外,我不确定你所说的“后台[GC]清除”是什么意思 - 你是指扫描阶段吗?我认为那紧随其后。 - Simon Buchan
谢谢Simon。根据您的建议编辑了回复。我不确定扫描阶段是什么意思,我的意思是在堆压缩后,需要终结的超出范围的对象会由GC在后台单独处理。 - Rashmi Pandit
1
-1 对于不支持的语句“因此,垃圾回收比引用计数更有效率”。 - GS - Apologise to Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接