在Java中,只要对象不再具有任何引用,就可以删除它,但是JVM决定何时实际删除该对象。要使用Objective-C术语,所有Java参考本质上都是“强”的。但是,在Objective-C中,如果对象不再具有任何强引用,则该对象将立即删除。为什么在Java中不是这种情况?

评论

您不必在乎实际删除Java对象的时间。这是一个实现细节。

@BasileStarynkevitch您应该绝对关心并挑战系统/平台的工作方式。提出“如何”和“为什么”的问题是成为更好的程序员(更笼统地说,是更聪明的人)的最佳方法之一。

有循环引用时,Objective C会做什么?我以为只是泄漏了他们?

@ArturBiesiadowksi:不,Java规范没有说明何时删除对象(对于R5RS同样如此)。如果删除从未发生过,您可能并且应该开发Java程序(对于像Java hello world这样短暂的进程,确实不会发生)。您可能会关心活动对象集(或内存消耗),这是另一回事。

有一天,新手对主人说:“我有解决分配问题的方法。我们将为每个分配提供一个参考计数,当计数达到零时,我们可以删除该对象”。主人回答:“有一天,新手对主人说:“我有解决办法...

#1 楼

首先,Java具有弱引用和另一个尽力而为的类别,称为软引用。弱引用与强引用是与引用计数与垃圾回收完全不同的问题。

其次,内存使用中的某些模式可以通过牺牲空间来提高垃圾回收的效率。例如,较新的对象比旧的对象更有可能被删除。因此,如果在两次扫描之间稍等片刻,则可以删除大多数新一代内存,同时将少数幸存者移至长期存储。长期存储可以不那么频繁地被扫描。
通过手动内存管理或引用计数立即删除更容易产生碎片。

有点像每张工资单去杂货店购物和每天花钱买足够的东西之间的区别食物一天。您的一次大旅行会比一次小旅行花费更长的时间,但总的来说,您最终会节省时间,甚至可能节省金钱。

评论


程序员的妻子将他送到超市。她对他说:“买一条面包,如果你看到鸡蛋,那就抓一打。”程序员随后带着胳膊将十几条面包带回来。

–尼尔
18年5月8日在7:10



我建议提到,新一代gc时间通常与活动对象的数量成正比,因此,删除更多的对象意味着在许多情况下根本不会支付其费用。删除就像翻转幸存者空间指针并有选择地将一个大内存集中的整个内存空间清零一样简单(不确定是在gc结束时完成还是在当前jvm中分配tlab或对象本身时摊销)

– Artur Biesiadowski
18年5月8日在7:39

@Neil不应该是13条面包吗?

–贾德
18年5月8日在8:18

“在过道7上因一个错误关闭”

– joeytwiddle
18年5月8日在10:53

@JAD我会说13,但大多数都不倾向于这样做。 ;)

–尼尔
18年5月8日在11:39

#2 楼

因为正确地知道不再引用某些东西并不容易。甚至还不容易。

如果两个对象相互引用怎么办?他们会永远留下来吗?将这种思路扩展到解决任意数据结构,您将很快了解为什么JVM或其他垃圾收集器被迫采用更加复杂的方法来确定仍然需要什么以及可以做什么。

评论


或者,您可以采用Python方法,在其中尽可能多地使用refcounting,当您期望有循环依赖关系会泄漏内存时,请诉诸于GC。我不明白为什么他们除了GC之外还不能进行点钞?

–user541686
18年5月8日在7:35



@Mehrdad他们可以。但是可能会更慢。没有什么能阻止您实施此操作,但是不要指望在Hotspot或OpenJ9中击败任何GC。

–约瑟夫说恢复莫妮卡
18年5月8日在7:39

@ jpmc26,因为如果立即删除不再使用的对象,则在高负载情况下删除对象的可能性很高,这会进一步增加负载。当负载较小时,GC可以运行。引用计数本身对于每个引用都是很小的开销。同样,使用GC时,如果不处理单个对象,通常可以丢弃没有引用的大部分内存。

–约瑟夫说恢复莫妮卡
18年5月8日在11:53

@Josef:正确的引用计数也不是免费的;参考计数更新需要原子增量/减量,这是令人惊讶的高成本,尤其是在现代多核体系结构上。在CPython中,这并不是什么大问题(CPython本身非常慢,并且GIL将其多线程性能限制在单核级别),但是在一种还支持并行性的更快的语言上,这可能是个问题。 PyPy完全摆脱引用计数而只使用GC的机会不大。

–意大利Matteo
18年5月8日在12:51



@Mehrdad一旦实现了Java的引用计数GC,我将很乐意对其进行测试,以发现其性能比任何其他GC实现都要差的情况。

–约瑟夫说恢复莫妮卡
18年5月8日在13:49

#3 楼

AFAIK,JVM规范(用英语编写)没有提到何时应删除确切的对象(或值),而是将其留给实现(对于R5RS同样如此)。它以某种方式要求或建议使用垃圾收集器,但将细节留给实现。 Java规范同样如此。

请记住,编程语言是规范(语法,语义等),而不是软件实现。诸如Java(或其JVM)之类的语言具有许多实现。它的规范已发布,可下载(您可以研究)并以英语编写。 §2.5.3JVM规范的堆提到了垃圾收集器:


对象的堆存储由自动存储管理系统(称为垃圾收集器)回收;对象永远不会
显式释放。 Java虚拟机不假定特定类型的自动存储管理系统


(重点是我的; Java规范的第12.6节提到了BTW终结,并且内存模型是在Java规范的§17.4中)

所以(在Java中)您不必关心何时删除对象,并且可以将代码编码为(如果没有发生)(通过推理抽象出忽略的地方)那)。当然,您需要关心内存消耗和活动对象集,这是一个不同的问题。在几种简单的情况下(例如“ hello world”程序),您可以证明(或说服自己)分配的内存很小(例如,小于1 GB),然后您根本就不在乎删除单个对象。在更多情况下,您可以说服自己,活物(或可达的物,这是一个超集(易于推理)的活物)从未超过合理的限制(然后您确实依赖GC,但您没有关心垃圾收集的方式和时间)。了解有关空间复杂性的信息。

我猜想在运行像Java Hello World这样的短暂Java程序的几种JVM实现中,根本不会触发垃圾收集器,也不会发生删除。 AFAIU,这种行为符合众多Java规范。

大多数JVM实现都使用分代复制技术(至少对于大多数Java对象,那些不使用终结处理或弱引用的对象;终结处理不能保证在很短的时间内发生,并且可以推迟,只是一个有用的功能,您的代码不应太依赖它),其中删除单个对象的概念没有任何意义(因为一大块内存-包含许多对象的存储区-可能一次释放几兆字节)

如果JVM规范要求尽快准确地删除每个对象(或者只是对对象删除施加更多限制),那么将禁止高效的世代GC技术,因此, Java和JVM最好避免这种情况。

顺便说一句,从未删除对象且不释放内存的幼稚JVM可能符合规范(字母,不是精神),当然能够在实践中是一件非常好的事情(请注意,大多数小型且寿命短的Java程序分配的内存可能不超过几GB)。当然,这样的JVM不值得一提,只是一个玩意儿(就像C的malloc的实现一样)。有关更多信息,请参见Epsilon NoOp GC。现实生活中的JVM是非常复杂的软件,并且融合了多种垃圾回收技术。

另外,Java与JVM不同,您确实可以在没有JVM的情况下运行Java实现(例如,提前Java编译器,Android运行时)。在某些情况下(大多数是学术性的情况),您可能会想到(所谓的“编译时垃圾收集”技术)Java程序不会在运行时分配或删除(例如,由于优化的编译器足够聪明,只能使用调用堆栈和自动变量)。


为什么不再引用Java对象后不立即删除它们?


因为Java和JVM规范不需要。


阅读GC手册以了解更多信息(以及JVM规范)。请注意,对对象保持活动状态(或对将来的计算有用)是整个程序(非模块化)属性。

Objective-C支持在内存管理中使用引用计数方法。而且这也有一些陷阱(例如,Objective-C程序员必须通过显式弱引用来关心循环引用,但是JVM实际上在不需要Java程序员注意的情况下可以很好地处理循环引用。在编程和编程语言设计中不是“银弹”(要意识到“停顿问题”;要成为一个有用的活物通常是无法确定的)。

您还可以阅读SICP,《编程语言实用》,​​《龙书》 ,Lisp的小片段和操作系统:三个简单的片段。它们不是关于Java的,但它们会打开您的胸怀,并应帮助您了解JVM应该做什么以及它在计算机上实际上如何(与其他组件一起工作)。您还可能花费数月(或数年)来研究现有开放源代码JVM实现的复杂源代码(例如OpenJDK,它具有数百万个源代码行)。

评论


“可能永远不会删除对象并且不释放内存的幼稚JVM可能符合规范”。 Java 11实际上为非常短暂的程序添加了一个无操作垃圾收集器。

–迈克尔
18年5月8日在11:07



“您不必关心何时删除对象”。首先,您应该知道RAII不再是一种可行的模式,并且您不能依赖于finalize进行任何资源管理(文件句柄,数据库连接,gpu资源等)。

–亚历山大
18年5月8日在16:23

@Michael对于具有已用内存上限的批处理,这是非常有意义的。操作系统只能说“该程序使用的所有内存现在都消失了!”毕竟,这相当快。确实,许多用C编写的程序都是用这种方式编写的,尤其是在早期的Unix世界中。帕斯卡(Pascal)有一个非常可怕的“将堆栈/堆指针重置为预先保存的检查点”,尽管它非常不安全-标记,启动子任务,重置,但它使您可以做很多事情。

–罗安
18年5月9日在7:46

@Alexander通常在C ++(以及有意从中衍生的几种语言)之外,假设RAII仅基于终结器才能工作是一种反模式,应警告该模式,并用明确的资源控制块代替。毕竟,GC的全部意义在于生命周期和资源是分离的。

–卢申科
18年5月9日在9:13



@Leushenko我强烈反对“生命周期和资源分离”是GC的“重点”。您为GC的主要要点支付的负价是:简单,安全的内存管理。 “假设RAII仅能基于终结器工作是一种反模式”在Java中?也许。但是在CPython,Rust,Swift或Objective C中却没有。通过RAII管理资源的对象为您提供了传递范围内生命的句柄。资源尝试模块仅限于一个范围。

–亚历山大
18年5月9日在15:51

#4 楼


要使用Objective-C术语,所有Java引用本来都是“强”的。对象级别而不是语言关键字。


在Objective-C中,如果对象不再具有任何强引用,则立即删除该对象。


这也不一定是正确的-Objective C的某些版本确实使用了世代垃圾收集器。其他版本完全没有垃圾回收。

确实,较新版本的Objective C使用自动引用计数(ARC)而不是基于跟踪的GC,这(通常)导致对象被垃圾回收。当该引用计数达到零时“删除”。但是,请注意,JVM实现也可以兼容并以这种方式工作(哎呀,它可以兼容并且完全没有GC。)

那么为什么大多数JVM实现都不这样做,而是使用基于跟踪的GC算法?

简单地说,ARC不像它最初看起来的乌托邦式:


您必须在每个计数器上增加或减少一个计数器引用被复制,修改或超出范围的时间,这会带来明显的性能开销。
ARC无法轻松清除周期性引用,因为它们都相互引用,因此它们的引用数量永远不会归零。

当然,ARC确实具有优势-它易于实现且具有确定性。但是上述缺点以及其他缺点是大多数JVM实现将使用基于跟踪的分代GC的原因。

评论


有趣的是,苹果之所以选择ARC,恰恰是因为他们发现实际上,它大大优于其他GC(特别是几代GC)。公平地讲,这在内存受限平台(iPhone)上大多如此。但我要反驳您的说法“ ARC不像它最初看起来的乌托邦式”,而是说世代(以及其他非确定性)GC并不像它们最初看起来那样乌托邦式:确定性销毁可能是一个更好的选择。绝大多数情况。

–康拉德·鲁道夫(Konrad Rudolph)
18年5月9日在10:39



@KonradRudolph虽然我也很喜欢确定性破坏,但我认为“在大多数情况下更好的选择”不会成立。当延迟或内存比平均吞吐量更重要时,尤其是当逻辑相当简单时,这无疑是一个更好的选择。但这并不像没有那么多复杂的应用程序需要大量的循环引用等,并且需要快速的平均操作,但是却并不在乎延迟和有足够的可用内存。对于这些,怀疑ARC是否是一个好主意。

–leftaround关于
18年5月9日在12:53

@leftaroundabout在“大多数情况”下,吞吐量和内存压力都不是瓶颈,因此无论哪种方式都没有关系。您的示例是一种特定的情况。当然,这并不少见,但我不会声称它比其他更适合ARC的情况更普遍。此外,ARC可以很好地处理周期。程序员只需要一些简单的手动干预即可。这使其不那么理想,但几乎不会破坏交易。我认为确定性终结比您假装要重要得多。

–康拉德·鲁道夫(Konrad Rudolph)
18年5月9日在16:15



@KonradRudolph如果ARC需要程序员进行一些简单的手动干预,则它不会处理循环。如果您开始大量使用双向链接列表,则ARC会转换为手动内存分配。如果您有任意大的图形,ARC会强制您编写垃圾收集器。 GC的论点是,需要销毁的资源不是内存子系统的工作,为了跟踪相对较少的资源,应该通过程序员一些简单的手动干预将其确定性地确定下来。

– prosfilaes
18年5月10日在1:58

如果不手动处理@KonradRudolph ARC,则循环会从根本上导致内存泄漏。在足够复杂的系统中,如果发生以下情况,可能会发生重大泄漏:映射中存储的某些对象会存储对该映射的引用,程序员可以负责创建和销毁该映射的代码部分,从而可以进行更改。较大的任意图并不意味着内部指针不强,链接的项目消失也可以。我不会说,处理一些内存泄漏是否比必须手动关闭文件少了一个问题,但这是真实的。

– prosfilaes
18年5月11日在6:20



#5 楼

Java没有明确指定何时收集对象,因为这使实现者可以自由选择如何处理垃圾收集。对象几乎立即完全基于引用计数(我不知道有任何打破这种趋势的算法)。参考计数是一个功能强大的工具,但要付出维护参考计数的代价。在单线程代码中,无非是递增和递减,所以分配一个指针所花费的成本大约是非引用计数代码的3倍(如果编译器可以将所有内容分解到机器中)代码)。

在多线程代码中,成本更高。它要么要求原子的增/减,要么要求锁,这两者都可能很昂贵。在现代处理器上,原子操作的费用可能比简单寄存器操作的费用高20倍左右(显然,每个处理器的操作不同)。这会增加成本。

因此,我们可以考虑几种模型之间的权衡。


Objective-C专注于ARC-自动引用计数。他们的方法是对所有内容都使用引用计数。 (我不知道)没有周期检测,因此程序员应该防止周期的发生,这会花费开发时间。他们的理论是指针不会经常分配,它们的编译器可以识别递增/递减引用计数不会导致对象死亡的情况,并完全消除这些递增/递减。因此,它们最大程度地减少了引用计数的成本。
CPython使用混合机制。它们使用引用计数,但是它们还具有识别周期并释放周期的垃圾收集器。这以两种方法为代价提供了两个世界的利益。 CPython必须既要维护引用计数,又要做好记录以检测周期。 CPython通过两种方式避免了这种情况。首要的是CPython确实不是完全多线程的。它具有一个称为GIL的锁,用于限制多线程。这意味着CPython可以使用普通的增量/减量,而不是原子的增量/减量,这要快得多。还解释了CPython,这意味着像给变量赋值之类的操作已经需要少量指令,而不仅仅是1条指令。在C代码中快速完成增量/减量的额外成本已不再是问题,因为我们我已经付了这笔费用。
Java放弃了完全不保证引用计数系统的方法。的确,除了将有一个自动存储管理系统之外,该规范没有说明有关对象的管理方式。但是,该规范还强烈暗示这样一个假设,即将以处理周期的方式进行垃圾回收。通过不指定对象何时过期,java可以自由使用不浪费时间递增/递减的收集器。确实,诸如世代垃圾收集器之类的聪明算法甚至可以处理许多简单的情况,而无需查看正在回收的数据(它们只需要查看仍在引用的数据)。

可以看到这三个必须进行权衡。哪种折衷方案最好取决于很大程度上取决于该语言的使用方式。

#6 楼

尽管finalize搭载在Java的GC上,但垃圾回收的核心不是死对象,而是活动对象。在某些GC系统(可能包括Java的某些实现)上,唯一可以将代表对象的位与没有任何用途的存储区分开的唯一原因可能是对前者的引用。虽然带有终结符的对象被添加到特殊列表中,但其他对象在Universe中可能没有任何地方可以说其存储与对象相关联,但用户代码中保留的引用除外。当最后一个此类引用被覆盖时,内存中的位模式将立即停止被识别为对象,无论宇宙中是否有任何对象意识到这一点。

垃圾回收的目的是' t销毁不存在引用的对象,而是完成三件事:


使弱引用无效,这些弱引用标识那些没有任何与之关联的强引用的对象。 br />使用终结器搜索系统的对象列表,以查看其中是否没有与它们关联的任何高度可访问的引用。
确定并合并没有任何对象使用的存储区域。

请注意,GC的主要目标是#3,并且等待的时间越长,合并的机会就越大。在需要立即使用存储的情况下执行#3是有意义的,但是在其他情况下,推迟它会更有意义。

评论


实际上,gc只有一个目标:模拟无限内存。您命名为目标的所有内容要么是抽象上的缺陷,要么是实现细节。

–重复数据删除器
18年5月8日在19:46

@Deduplicator:弱引用提供了有用的语义,如果没有GC的帮助就无法实现。

–超级猫
18年5月8日在20:16

当然,弱引用具有有用的语义。但是,如果模拟效果更好,是否需要这些语义?

–重复数据删除器
18年5月8日在20:19

@Deduplicator:是的。考虑一个集合,该集合定义更新将如何与枚举交互。这样的集合可能需要保留对任何实时枚举器的弱引用。在不受限制的内存系统中,反复迭代的集合将使其感兴趣的枚举数列表无限制地增长。该列表所需的内存不会有问题,但是遍历该列表所需的时间会降低系统性能。添加GC可以表示O(N)和O(N ^ 2)算法之间的差异。

–超级猫
18年5月8日在20:32

为什么要通知枚举器,而不是附加到列表并让它们在使用时自行寻找?任何依赖于及时处理垃圾的程序,而不是依赖于内存压力的程序,如果运行的话,总会处于犯罪状态。

–重复数据删除器
18年5月8日在20:46



#7 楼

让我建议对您的问题进行重新措词和概括:请记住,快速浏览此处的答案。到目前为止,有七个(不算一个),还有很多注释线程。

这就是您的答案。有很多考虑因素,很多折衷方案,最终还有很多非常不同的方法。其中一些方法使在不需要对象时立即对它进行GC成为可能。其他人没有。通过保持契约松散,Java为实现者提供了更多选择。

当然,即使在该决定中也存在一个折衷:通过保持契约松散,Java大多数*消除了程序员依赖的能力。在破坏者身上。特别是C ++程序员经常会错过的一件事([需要引用];)),因此这并不是一个微不足道的折衷。我还没有看到有关该特定元决策的讨论,但是大概Java人士认为,拥有更多GC选项的好处胜过能够准确告诉程序员何时销毁对象的好处。 br />
*存在finalize方法,但是由于各种原因超出了此答案的范围,因此很难依靠它,也不是一个好主意。

#8 楼

在没有开发人员编写显式代码的情况下,有两种处理内存的策略:垃圾回收和引用计数。

垃圾收集的优点是“有效”,除非开发人员做一些愚蠢的事情。通过引用计数,您可以拥有参考周期,这意味着它可以“运行”,但是开发人员有时必须很聪明。因此,这是垃圾回收的一个优点。

通过引用计数,当引用计数降至零时,对象会立即消失。这是引用计数的优势。

如果您相信垃圾回收的支持者,那么从速度上看,垃圾回收会更快,而如果您相信引用计数的支持者,则引用计数会更快。

这是实现同一目标的两种不同方法,Java选择了一种方法,Objective-C选择了另一种方法(并添加了许多编译器支持,以将其从烦人的状态转换为某种状态对于开发人员而言,这几乎是没有用的)。

将Java从垃圾回收更改为引用计数将是一项主要工作,因为需要进行大量代码更改。

从理论上讲,Java可以实现垃圾回收和引用计数的混合:如果引用计数为0,则对象是不可访问的,但不一定是相反的。因此,您可以保留引用计数,并在它们的引用计数为零时删除对象(然后不时运行垃圾回收以在无法访问的引用周期内捕获对象)。我认为,人们认为将垃圾回收添加到引用计数是一个坏主意,而那些认为将垃圾回收添加到引用计数是一个坏主意的人将世界分为50/50。所以这不会发生。

因此,如果Java的引用计数变为零,则Java可以立即删除它们,然后在无法访问的周期内删除对象。但这是设计决定,而Java则反对。

评论


使用引用计数,最终确定是微不足道的,因为程序员需要处理周期。使用gc,周期是微不足道的,但是程序员在完成时必须小心。

–重复数据删除器
18年5月8日在16:33

@Deduplicator在Java中,还可以创建对最终确定对象的强大引用...在Objective-C和Swift中,一旦引用计数为零,该对象就会消失(除非您将无限循环放入dealloc / deist中)。

– gnasher729
18年5月8日在17:42

刚刚注意到愚蠢的拼写检查器将deinit替换为deist

– gnasher729
18年5月8日在19:07

大多数程序员有理由讨厌自动拼写更正... ;-)

–重复数据删除器
18年5月8日在19:44

大声笑...我认为认为是在垃圾收集中添加引用计数是一个坏主意的人与认为将垃圾收集添加到引用计数是一个坏主意的人与那些继续计数直到垃圾收集到达为止,因为那一吨已经再臭了...

–leftaround关于
18年5月9日在18:49

#9 楼

尽管我认为值得一提的另一个想法是至少有一个JVM(azul)考虑了这样的问题,但所有其他性能参数和有关在不再有对象引用时的理解困难的讨论都是正确的它实现的并行gc本质上具有一个vm线程,该线程不断检查引用以尝试删除它们,这些行为与您所谈论的行为并不会完全不同。基本上,它将不断地查看堆,并尝试回收未引用的任何内存。这确实招致了非常小的性能成本,但实际上导致了GC时间为零或非常短。 (这就是说,除非不断扩大的堆大小超过系统RAM,然后Azul感到困惑,然后出现巨龙)。就像其他任何工程上的折衷一样。

#10 楼

动态压力是使持续吞吐量最大化或gc延迟最小的原因,这可能是GC不会立即发生的最常见原因。在某些系统中,例如911紧急应用程序,未达到特定的延迟阈值可以开始触发站点故障转移过程。在其他地方,例如银行和/或套利网站,最大化吞吐量更为重要。

#11 楼

速度

为什么所有这些最终都归因于速度。如果处理器无限快,或者(实际上)接近它,例如每秒进行1,000,000,000,000,000,000,000,000,000,000,000,000,000次操作,则每个操作符之间可能会发生漫长而复杂的事情,例如确保删除了取消引用的对象。由于目前每秒的操作数还不正确,而且正如大多数其他答案所解释的那样,要弄清楚这一点实际上很复杂且需要大量资源,因此存在垃圾回收,以便程序可以专注于程序中他们实际试图实现的目标。快速的方式。

评论


好吧,我敢肯定,我们会找到更多有趣的方式来消耗额外的周期。

–重复数据删除器
18年5月13日在15:29