在Java中,只要对象不再具有任何引用,就可以删除它,但是JVM决定何时实际删除该对象。要使用Objective-C术语,所有Java参考本质上都是“强”的。但是,在Objective-C中,如果对象不再具有任何强引用,则该对象将被立即删除。为什么在Java中不是这种情况?
在Java中,只要对象不再具有任何引用,就可以删除它,但是JVM决定何时实际删除该对象。要使用Objective-C术语,所有Java参考本质上都是“强”的。但是,在Objective-C中,如果对象不再具有任何强引用,则该对象将被立即删除。为什么在Java中不是这种情况?
Answers:
首先,Java具有弱引用,而另一个尽力而为类别称为软引用。弱引用与强引用是与引用计数与垃圾回收完全不同的问题。
其次,内存使用中的某些模式可以通过牺牲空间来提高垃圾回收的效率。例如,较新的对象比旧的对象更有可能被删除。因此,如果在两次扫描之间稍等片刻,则可以删除大多数新一代内存,同时将少数幸存者移至长期存储。长期存储的扫描频率要低得多。通过手动内存管理或引用计数立即删除更容易造成碎片。
这有点像每张工资单去杂货店购物和每天去买一天足够的食物之间的区别。一次大行程比一次小行程要花费更长的时间,但总的来说,您最终会节省时间,甚至可能节省金钱。
因为正确地知道不再引用某些东西并不容易。甚至都不容易。
如果您有两个对象互相引用该怎么办?他们会永远留下来吗?将这种思路扩展到解决任意数据结构,您将很快了解为什么JVM或其他垃圾收集器被迫采用更加复杂的方法来确定仍然需要什么以及可以做什么。
AFAIK,JVM规范(用英语编写)没有提到何时应删除确切的对象(或值),而是将其留给实现(对于R5RS也是一样)。它以某种方式要求或建议使用垃圾收集器,但将细节留给实现。Java规范也是如此。
请记住,编程语言是规范(语法,语义等),而不是软件实现。诸如Java(或其JVM)之类的语言具有许多实现。它的规范已发布,可下载(以便您进行研究)并以英语编写。第2.5.3节 JVM规范的堆提到了垃圾收集器:
自动存储管理系统(称为垃圾收集器)可以回收对象的堆存储;对象永远不会显式释放。Java虚拟机不假定特定类型的自动存储管理系统
(强调的是矿;顺便说一句定稿中提到§12.6 Java规范的,和存储器模型是在§17.4 Java规范的)
因此,(在Java中)您不必关心何时删除对象,并且可以将代码按原样(如果未发生)进行编码(通过推理抽象化而忽略该对象)。当然,您需要关心内存消耗和活动对象集,这是一个不同的问题。在几种简单的情况下(例如“ hello world”程序),您可以证明(或说服自己)分配的内存非常小(例如,小于1 GB),然后您根本就不在乎删除单个对象。在更多情况下,您可以使自己相信有生命的物体(或可达的,这是一个超集-易于推理-有生命的生命)永远不会超出合理的限制(然后您确实依赖于GC,但是您不关心垃圾收集的方式和时间)。了解有关空间复杂性的信息。
我猜想在运行像Java Hello World这样的短暂Java程序的几种JVM实现中,根本不会触发垃圾收集器,也不会发生删除。AFAIU,这种行为符合众多Java规范。
大多数JVM实现使用世代复制技术(至少对于大多数Java对象,那些不使用终结处理或弱引用的对象;终结处理不能保证在短时间内发生并且可以推迟,因此这是一个有用的功能,您的代码不应在很大程度上取决于),删除单个对象的概念没有任何意义(因为一大块内存-包含许多对象的存储区-可能一次释放几兆字节)。
如果JVM规范要求尽快准确地删除每个对象(或者只是对对象删除施加更多限制),那么将禁止高效的世代GC技术,因此Java和JVM的设计者应避免这种情况。
顺便说一句,一个永不删除对象并且不释放内存的幼稚JVM可能符合规范(字母,不是精神),并且确实能够在实践中运行一个hello world事物(请注意,大多数小型且寿命短的Java程序可能分配的内存不会超过几GB。当然,这样的JVM是不值得一提的是只是一个玩具的东西(比如是这样实现的malloc
对C)。有关更多信息,请参见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,它具有数百万个源代码行)。
finalize
任何资源管理(文件句柄,数据库连接,gpu资源等)。
要使用Objective-C术语,所有Java参考本质上都是“强”的。
这是不正确的-Java确实具有弱引用和软引用,尽管这些引用是在对象级别而不是作为语言关键字实现的。
在Objective-C中,如果对象不再具有任何强引用,则该对象将立即删除。
这也不一定是正确的-Objective C的某些版本确实使用了世代垃圾收集器。其他版本完全没有垃圾回收。
的确,较新版本的Objective C使用自动引用计数(ARC)而不是基于跟踪的GC,并且(通常)这导致对象的引用计数达到零时被“删除”。但是,请注意,JVM实现也可以兼容并完全按照这种方式工作(哎呀,它可以兼容并且完全没有GC。)
那么,为什么大多数JVM实现都不这样做,而是使用基于跟踪的GC算法?
简而言之,ARC并不像它最初看起来那样乌托邦式:
当然,ARC确实具有优势-它的易于实现和收集是确定性的。但是,上述缺点以及其他缺点是大多数JVM实现将使用基于跟踪的世代GC的原因。
Java没有精确指定何时收集对象,因为这使实现可以自由选择如何处理垃圾收集。
有许多不同的垃圾收集机制,但是那些保证您可以立即收集对象的垃圾收集机制几乎完全基于引用计数(我不知道有任何打破这种趋势的算法)。参考计数是一个功能强大的工具,但是要付出一定的代价才能保持参考计数。在单线程代码中,无非是递增和递减,所以分配指针所花费的成本大约是非引用计数代码的3倍(如果编译器可以将所有内容分解到机器中)码)。
在多线程代码中,成本更高。它要么要求原子的增/减,要么要求加锁,这两者都可能很昂贵。在现代处理器上,原子操作的费用可能比简单的寄存器操作高20倍左右(显然,每个处理器的操作不同)。这会增加成本。
因此,我们可以考虑几种模型之间的权衡。
Objective-C专注于ARC-自动引用计数。他们的方法是对所有内容都使用引用计数。(我不知道)没有周期检测,因此程序员应该防止周期的发生,这会花费开发时间。他们的理论是指针不会经常分配,它们的编译器可以识别递增/递减的引用计数不会导致对象死亡的情况,并完全消除那些递增/递减的情况。因此,它们最小化了参考计数的成本。
CPython使用混合机制。它们使用引用计数,但是它们还具有识别周期并释放周期的垃圾收集器。这以两种方法为代价提供了两个世界的利益。CPython必须同时维护引用计数和做簿记以检测周期。CPython通过两种方式避免了这种情况。首要的是CPython确实不是完全多线程的。它具有一个称为GIL的锁,该锁限制了多线程。这意味着CPython可以使用普通的增量/减量,而不是原子的增量/减量,这要快得多。还解释了CPython,这意味着像对变量进行赋值之类的操作已经需要少量指令,而不仅仅是1条指令。进行增量/减量的额外成本(在C代码中快速完成)已不再是问题,因为我们我已经支付了这笔费用。
Java采用了完全不保证引用计数系统的方法。事实上,规范并没有说任何有关对象如何进行管理以外,会有一个自动存储管理系统。但是,该规范还强烈暗示了以下假设:将以处理周期的方式将其垃圾收集。通过不指定对象何时过期,java可以自由使用收集器,而不会浪费时间增加/减少时间。确实,诸如代垃圾收集器之类的聪明算法甚至可以处理许多简单的情况,而无需查看正在回收的数据(它们只需要查看仍在引用的数据)。
因此,我们可以看到这三个必须进行权衡。哪种权衡是最好的,很大程度上取决于使用该语言的方式的性质。
尽管finalize
垃圾回收背负于Java的GC,但垃圾回收的核心不是死对象,而是活动对象。在某些GC系统(可能包括Java的某些实现)上,唯一可以将代表对象的位和不用于任何用途的存储区分开的唯一原因可能是对前者的引用。虽然带有终结符的对象被添加到特殊列表中,但其他对象在Universe中可能没有任何地方可以说其存储与对象相关联,但用户代码中保留的引用除外。当最后一个这样的引用被覆盖时,内存中的位模式将立即停止被识别为对象,无论宇宙中是否有任何对象意识到这一点。
垃圾回收的目的不是破坏没有引用的对象,而是完成三件事:
使弱引用无效,这些弱引用用于标识没有与之关联的任何强可达引用的对象。
使用终结器搜索系统的对象列表,以查看其中是否没有与它们关联的任何高度可达的引用。
识别并合并任何对象未使用的存储区域。
请注意,GC的主要目标是#3,而在等待之前等待的时间越长,合并一个人的机会就越多。在需要立即使用存储的情况下执行#3是有意义的,但是在其他情况下,推迟存储则更有意义。
让我建议对您的问题进行重新措词和概括:
Java为什么不对其GC流程做出有力保证?
考虑到这一点,快速浏览此处的答案。到目前为止,有七个(不算这一个),还有很多注释线程。
那就是你的答案。
GC很难。有很多考虑因素,很多折衷方案,最终还有很多非常不同的方法。其中一些方法使在不需要对象时立即对它进行GC成为可能。其他人没有。通过保持契约松散,Java为实现者提供了更多选择。
当然,即使在这个决定中也要权衡:通过保持契约松散,Java大多数*消除了程序员依赖析构函数的能力。特别是C ++程序员经常会错过的一件事([需要引用];)),因此这并不是一个微不足道的折衷。我还没有看到有关该特定元决策的讨论,但是大概Java人士认为,拥有更多GC选项的好处胜过能够准确告知程序员何时销毁对象的好处。
*有finalize
方法,但是由于各种原因超出了此答案的范围,因此很难依靠它也不是一个好主意。
在没有开发人员编写显式代码的情况下,有两种不同的处理内存的策略:垃圾回收和引用计数。
垃圾回收的优点是“有效”,除非开发人员做一些愚蠢的事情。通过引用计数,您可以拥有参考周期,这意味着它可以“运行”,但是开发人员有时必须很聪明。因此,这是垃圾收集的优点。
使用参考计数时,当参考计数降至零时,对象会立即消失。这是引用计数的优势。
从速度上看,如果您相信垃圾回收的支持者,那么垃圾回收会更快,而如果您相信引用计数的支持者,则引用计数会更快。
这是实现同一目标的两种不同方法,Java选择了一种方法,Objective-C选择了另一种方法(并添加了许多编译器支持,以将其从繁琐的工作变为对开发人员几乎没有用的工作)。
将Java从垃圾回收更改为引用计数将是一项艰巨的任务,因为需要进行大量代码更改。
从理论上讲,Java可以实现垃圾回收和引用计数的混合:如果引用计数为0,则对象是不可访问的,但不一定相反。因此,您可以保留引用计数,并在它们的引用计数为零时删除对象(然后不时运行垃圾回收以在无法访问的引用周期内捕获对象)。我认为,人们认为将垃圾回收添加到引用计数是一个坏主意,而那些认为将垃圾回收添加到引用计数是一个坏主意的人,这个世界是50/50。因此,这不会发生。
因此,如果Java 的引用计数变为零,则Java 可以立即删除它们,然后在无法访问的周期内删除对象。但这是设计决定,而Java则反对。
尽管我认为值得一提的另一个想法是,至少有一个JVM(azul)考虑这样的事情,但所有其他性能参数和有关在不再有对象引用时的理解困难的讨论都是正确的它实现的并行gc本质上具有一个vm线程,该线程不断检查引用以尝试删除它们,这些行为与您所谈论的行为并不会完全不同。基本上,它将不断地查看堆,并尝试回收未引用的任何内存。这确实招致了非常小的性能成本,但实际上导致了GC时间为零或非常短。(那是除非不断扩大的堆大小超过系统RAM,然后Azul感到困惑,然后出现巨龙)
TLDR JVM有点类似的东西,它只是一个特殊的jvm,它具有与其他任何工程折衷一样的缺点。
免责声明:我与Azul没有关系,我们只是在上一份工作中使用过它。
为何所有这些事情最终都归因于速度。如果处理器无限快,或者(实际上)接近处理器,例如每秒1,000,000,000,000,000,000,000,000,000,000,000,000,000次操作,那么每个操作符之间可能会发生漫长而复杂的事情,例如确保删除了被取消引用的对象。由于目前每秒的操作数还不正确,并且正如大多数其他答案所解释的那样,要弄清这一点实际上很复杂且需要大量资源,因此存在垃圾回收,以便程序可以专注于程序中他们实际试图实现的目标。快速的方式。