java.io.Closeable
接口和“try-with-resources”块),这是完全确定的。因此,有关“清理操作”的确定性的部分答案是错误的。 - Rogério垃圾回收可以解决RAII无法解决的某些资源问题。基本上,这归结为循环依赖性问题,在此之前你不能确定循环。
这给它带来了两个优点。首先,会有一些RAII无法解决的问题类型。但在我看来,这些情况很少见。
更重要的是,它让程序员变得懒惰,不需要关心内存资源生命周期和某些你不介意延迟清理的其他资源。当你不必关心某些类别的问题时,你就可以更多地关注其他问题。这使你能够专注于你想关注的问题部分。
缺点是没有RAII,管理希望受到约束的生命周期资源很难。GC语言基本上将你限制为具有极其简单的作用域绑定生命周期或需要手动执行资源管理(如在C中),手动声明你已经完成了一个资源。它们的对象生命周期系统与GC密切相关,并且对于大型复杂(但无循环)系统的紧密生命周期管理效果不佳。
公平地说,C++中的资源管理需要大量工作才能在大型复杂(但无循环)系统中正确执行。 C#和类似的语言只是增加了一点难度,以换取易于处理的简单情况。
大多数GC实现也强制执行非局部全面类;创建通用对象的连续缓冲区或将通用对象组合成一个较大的对象并不是大多数GC实现容易实现的。另一方面,C#允许您创建具有一定限制功能的值类型struct
。在当前的CPU架构时代,高速缓存友好性至关重要,而GC导致的局部性缺失是沉重的负担。由于这些语言大部分采用字节码运行时,理论上JIT环境可以将常用数据移动在一起,但通常情况下,与C ++相比会不断发生缓存未命中,导致均匀的性能损失。
GC的最后一个问题是释放时间不确定,有时会导致性能问题。现代GC使得这种情况比过去少了很多问题。
malloc
实现中没有这样的逻辑。这种逻辑可能会导致虚假共享,这是多线程环境下的一个问题,但这是另一回事了。在C语言中,您可以使用显式技巧来改善局部性,但如果您不这样做,我认为GC会更好。我错过了什么吗? - SergGrmalloc
强制了非局部性,而不是GC,因此我认为你在回答中声称“大多数GC实现也强制非局部性”的说法并不真实。 - SergGrFoo
对象的FooWidgetManager
,它很可能会在一个无限增长的数据结构中存储已注册的Foo
对象。这样一个“已注册的Foo
”对象超出了垃圾回收的范围,因为FooWidgetManager
的内部列表或其他东西包含对它的引用。要释放这块内存,你需要请求FooWidgetManager
取消注册该对象。如果你忘记了,这基本上就像是“new without delete”,只是名称已更改...而垃圾回收器无法解决这个问题。 - H Waltersstd :: vector >
),引用计数是隐式的(因为它只能为0或1),并且实际上被省略,但是构造函数和析构函数(对于RAII至关重要)的行为好像有一个引用计数位(几乎不存在)。在std :: shared_ptr
中存在真正的引用计数器。但是内存仍然是 隐式地手动管理的(通过在构造函数和析构函数中触发new
和delete
),但是那个“隐含”的delete
(在析构函数中)给了自动内存管理的假象。但是,调用new
和delete
仍然会发生(它们需要时间)。
顺便提一句,GC 实现 可以(并且经常)以某种特殊方式处理循环依赖,但您将这个负担留给GC(例如阅读 Cheney算法)。
一些GC算法(尤其是分代复制垃圾回收器)不会打扰释放单个对象的内存,而是在复制后集中释放。实际上,Ocaml GC(或SBCL GC)对于某些算法(而不是所有算法)可以比真正的C++ RAII编程风格更快。顺便说一句,我对寻找这样的C++17 Scheme实现很感兴趣(但不太想自己编写),最好还具备一些多线程能力。
shared_ptr
这样简单的实现也没有显式计数器)都会在处理涉及循环引用的情况时遇到麻烦。如果你只谈论一个资源仅在单个方法内使用的简单情况,甚至不需要shared_ptr
,但这只是非常有限的子空间,GC-based世界也使用类似的手段,如C#的“using”或Java的“try-with-resources”。但现实世界还有更复杂的情况。 - SergGrunique_ptr
可以处理循环引用了?这个回答明确声称“所谓的RAII技术”“实际上是指引用计数”。我们可以(我也这么做)拒绝这种说法,因此就可以质疑这个回答的大部分内容(无论是准确性还是相关性),而不必否定其中每一项。 (顺便说一句,现实中也存在不能处理循环引用的垃圾收集器。) - ruakhRAII和GC从完全不同的方向解决问题。尽管有些人会说它们是相同的,但它们完全不同。
两者都解决了资源管理的难题。垃圾回收通过使开发人员不需要太关注管理这些资源来解决此问题。 RAII通过使开发人员更容易关注其资源管理来解决此问题。任何声称它们做同样事情的人都在向你推销某种东西。
如果您观察语言中的最新趋势,您会看到同一语言中使用两种方法,因为您实际上需要拼图的两侧。您会看到许多语言使用某种垃圾回收,以便您不必过多关注大多数对象,并且这些语言还提供RAII解决方案(例如Python的with
运算符),以便在您真正想关注它们时使用。
shared_ptr
提供GC(如果我可以认为引用计数和GC属于同一类解决方案,因为它们都旨在帮助您不需要关注生命周期)with
提供RAII,通过引用计数系统和垃圾回收器提供GCIDisposable
和using
提供RAII,通过分代垃圾收集器提供GC这些模式在每种语言中都会出现。
垃圾回收机制面临的一个问题是很难预测程序的性能。
采用 RAII(资源获取即初始化)技术,你会知道在确切的时间资源将离开作用域,然后清除一些内存并且这需要一定的时间。但是如果你不是垃圾回收器设置方面的专家,你就无法预测何时进行清理操作。
例如:使用垃圾回收机制可以更有效地清理一堆小对象,因为它可以释放大块内存,但这是一个不快速的操作,而且很难预测何时会发生和由于“大块清理”需要一定的处理器时间,可能会影响您程序的性能。
“高效”是一个非常宽泛的术语,在开发工作中RAII通常比GC不够有效率,但就性能而言,GC通常比RAII不够高效。然而,对于这两种情况都可以提供反例。在使用托管语言时,如果您有非常明确的资源(释放)模式,则处理通用GC可能会相当麻烦,就像使用shared_ptr
没有理由地用于所有内容时,使用RAII的代码可能会出乎意料地低效。