为什么调用System.gc()是不好的做法?

回答问了一个关于如何用System.gc()Java中的强制对象(这个家伙正在清除一个1.5GB的HashMap)的问题后,我被告知手动调用System.gc()是不好的做法,但评论并不完全令人信服。此外,似乎没有人敢对我的回答投赞成票,也没有人敢投反对票。

我在那里被告知这是一种糟糕的做法,但后来我又被告知垃圾收集器的运行不再系统地停止整个世界,而且JVM也只能有效地将其用作提示,所以我有点不知所措。

我知道JVM通常比您更了解何时需要回收内存。我也明白,担心几千字节的数据是愚蠢的。我也明白,即使是兆字节的数据也不如几年前了。但还是1.5 gb ?你知道在内存中有1.5 GB的数据;这又不是瞎猜的。System.gc()在系统上是坏的,还是在某个点上它变得好了?

所以这个问题实际上是双重的

  • 为什么调用System.gc()是或不是不好的做法?它真的只是在某些实现下对JVM的一个提示,还是总是一个完整的收集周期?真的有垃圾回收器实现可以在不停机的情况下完成工作吗?请阐明人们在对我的回答的评论中所做的各种断言。
  • 门槛在哪里?调用System.gc()是否是从来没有的好主意,或者在某些情况下是可以接受的?如果有,是什么时间?
132002 次浏览

每个人都说要避免System.gc()的原因是,它是一个很好地说明了代码的根本问题。任何依赖于它的正确性的代码肯定是坏的;任何依赖于它的性能都很可能是坏的。

您不知道您正在哪种垃圾收集器下运行。当然有一些没有“停止世界”;,因为你断言,但一些jvm不是那么聪明或由于各种原因(也许它们在电话上?)没有这样做。你不知道它会做什么。

而且,它不能保证做任何事情。JVM可能会完全忽略您的请求。

“you don’t know it will do”的组合;“你甚至不知道它是否有用,”;而且“无论如何你都不需要叫它”;这就是为什么人们如此强烈地说,一般来说你不应该这么做。我认为这是一个“如果你需要问你是否应该使用这个,你不应该”的情况。


编辑来解决其他线程的一些问题:

看完你链接的帖子后,还有一些事情我想指出来。 首先,有人建议调用gc()可能会将内存返回给系统。这当然不一定是正确的——Java堆本身的增长独立于Java分配

例如,JVM将保留内存(几十兆字节),并根据需要增加堆。即使在释放Java对象时,它也不一定会将内存返回给系统;保留已分配的内存以供将来的Java分配使用是完全自由的。

要显示System.gc()可能什么都不做,请查看 JDK bug 6668279 特别是有一个-XX:DisableExplicitGC VM选项:

默认情况下,对System.gc()的调用是启用的(-XX:-DisableExplicitGC)。使用-XX:+DisableExplicitGC禁用对System.gc()的调用。请注意,JVM在必要时仍然执行垃圾收集。

是的,调用System.gc()并不能保证它会运行,它是对JVM的请求,可能会被忽略。从文档中可以看出:

调用gc方法表明Java虚拟机将精力用于回收未使用的对象

调用它几乎总是一个坏主意,因为自动内存管理通常比您更了解何时进行gc。当它的内部空闲内存池很低时,或者当操作系统要求归还一些内存时,它会这样做。

如果你知道它有帮助,调用System.gc()可能是可以接受的。我的意思是,你已经在部署平台上彻底测试和测量了这两种场景的行为,你可以证明它是有帮助的。但是要注意gc是不容易预测的-它可能在一次运行中有帮助,在另一次运行中有伤害。

前面已经解释过,调用system.gc() 五月什么都不做,任何“需要”垃圾收集器运行的代码都是坏的。

然而,调用System.gc()是不好的做法的实际原因是它的效率很低。在最坏的情况下,它是可怕的效率低下!让我解释一下。

典型的GC算法通过遍历堆中的所有非垃圾对象来识别垃圾,并推断任何未访问的对象都必须是垃圾。由此,我们可以对垃圾收集的总工作进行建模,其中一部分与活动数据量成正比,另一部分与垃圾量成正比;即work = (live * W1 + garbage * W2)

现在假设您在单线程应用程序中执行以下操作。

System.gc(); System.gc();

第一个调用将(我们预测)执行(live * W1 + garbage * W2)工作,并清除未处理的垃圾。

第二个调用将执行(live* W1 + 0 * W2)工作,并且不回收任何东西。换句话说,我们已经完成了(live * W1)的工作和一事无成

我们可以将收集器的效率建模为收集一个单位垃圾所需的工作量;即efficiency = (live * W1 + garbage * W2) / garbage。因此,为了使GC尽可能高效,我们需要在运行GC时最大化 garbage的值;也就是说,一直等到堆满。(并且,使堆尽可能大。但这是另一个话题。)

如果应用程序不干涉(通过调用System.gc()), GC将等待堆满才运行,从而有效地收集垃圾__abc1。但是,如果应用程序强制GC运行,则堆可能不会满,结果将是垃圾收集效率低下。应用程序强制GC的频率越高,GC的效率就越低。

注意:上面的解释掩盖了一个事实,即典型的现代GC将堆划分为“空间”,GC可能会动态扩展堆,应用程序的非垃圾对象的工作集可能会变化等等。即便如此,同样的基本原则适用于所有真正的垃圾收集器__abc0。强制GC运行效率很低。


1 -这就是“吞吐量”收集器的工作原理。并发收集器(如CMS和G1)使用不同的标准来决定何时启动垃圾收集器。

2 -我也排除了专门使用引用计数的内存管理器,但目前没有Java实现使用这种方法…理由很充分。

人们已经很好地解释了为什么不使用它,所以我将告诉你一些你应该使用它的情况:

(以下评论适用于在Linux上运行的带有CMS收集器的Hotspot,在这里我有信心地说System.gc()实际上总是调用完整的垃圾收集)。

  1. 在启动应用程序的初始工作之后,您的内存使用状态可能非常糟糕。有一半的终身教职都是垃圾,这意味着你离你的第一个CMS更近了。在重要的应用程序中,调用System.gc()将堆“重置”到活动数据的起始状态并不是一个坏主意。

  2. 与第1条相同,如果密切监视堆的使用情况,则希望准确地读取基线内存使用情况。如果应用程序正常运行时间的前2分钟都是初始化,那么您的数据将会混乱,除非您强制(咳咳…)"suggest")预先执行完整的gc。

  3. 您可能有一个应用程序,它被设计为在运行时永远不会将任何内容提升到终身代。但是,您可能需要预先初始化一些不太大的数据,以便自动移动到年老代中。除非您在一切设置完毕后调用System.gc(),否则您的数据可能会留在新一代中,直到它被提升为止。突然之间,您的超级低延迟、低gc的应用程序因为在正常操作期间提升这些对象而受到巨大(当然是相对而言的)延迟惩罚。

  4. 有时候拥有一个系统是有用的。Gc调用在生产应用程序中可用,用于验证内存泄漏的存在。如果您知道X时刻的活动数据集应该与Y时刻的活动数据集以一定比例存在,那么在X时刻和Y时刻调用System.gc()并比较内存使用情况可能会很有用。

也许我写的代码很糟糕,但我已经意识到在eclipse和netbeans ide上点击垃圾桶图标是一个“好的实践”。

GC效率依赖于许多启发式方法。例如,一个常见的启发是,对对象的写访问通常发生在不久前创建的对象上。另一个原因是许多对象的寿命非常短(有些对象会使用很长时间,但许多对象在创建后几微秒就会被丢弃)。

调用System.gc()就像踢掉GC。它的意思是:“所有那些精心调整的参数,那些聪明的组织,所有你投入到分配和管理对象上的努力,让事情顺利进行,好吧,放弃所有这些,从头开始”。它五月提高性能,但大多数时候它只是降解性能。

要可靠地使用System.gc()(*),你需要知道GC是如何操作的所有细节。如果使用其他供应商的JVM,或者使用同一供应商的下一个版本,或者使用相同JVM但命令行选项略有不同,那么这些细节可能会发生很大变化。因此,这很少是一个好主意,除非你想解决一个你控制所有这些参数的特定问题。因此就有了“坏做法”的概念:这并没有被禁止,方法是存在的,但它很少有回报。

我在这里谈论的是效率。System.gc()永远不会打破一个正确的Java程序。它既不会产生JVM无法获得的额外内存:在抛出OutOfMemoryError之前,JVM会完成System.gc()的工作,即使是作为最后的手段。

很多人似乎都告诉你不要这样做。我不同意。如果在加载关卡等大型加载过程后,你认为:

  1. 您有很多不可访问的对象,可能还没有被gc - ed。而且
  2. 您认为此时用户可以忍受轻微的减速

调用System.gc()没有害处。我把它看作c/c++ inline关键字。这只是对gc的一个提示,即您(开发人员)已经决定时间/性能不像通常那样重要,其中一些时间/性能可以用于回收内存。

建议不要依赖它做任何事情是正确的。不要依赖于它的工作,但给一个提示,现在是一个可以接受的时间收集是完全可以的。我宁愿把时间浪费在代码中无关紧要的地方(加载屏幕),也不愿浪费在用户与程序积极互动的时候(比如在游戏关卡中)。

有一次,当我将力< em > < / em >集合:当试图找出是一个特定的对象泄漏(本机代码或大型,复杂的回调交互。哦,还有任何UI组件,哪怕只是瞥一眼Matlab.) 在产品代码中不应该使用这种方法。

根据我的经验,使用System.gc()实际上是一种平台特定形式的优化(其中“平台”是硬件架构、OS、JVM版本和可能的更多运行时参数(如可用的RAM)的组合),因为它的行为虽然在特定平台上大致可预测,但在不同平台之间可能(也将)有很大差异。

是的,在情况下System.gc()将提高(感知的)性能。举个例子,如果延迟在你的应用的某些部分是可以容忍的,但在其他部分却不能(就像上文所提到的游戏例子,你希望GC发生在关卡开始时,而不是在关卡进行时)。

然而,它是帮助还是伤害(或什么都不做)是高度取决于平台(如上所定义)。

所以我认为这是针对特定平台的最后一种优化方法(即如果其他性能优化还不够的话)。但是,您绝不应该仅仅因为相信它可能有帮助(没有特定的基准)就调用它,因为它很可能没有帮助。

首先,规范和现实之间是有区别的。规范说System.gc()提示GC应该运行,VM可以忽略它。实际情况是,VM将从来没有忽略对System.gc()的调用。

调用GC会带来不小的开销,如果您在某个随机时间点执行此操作,那么您的努力很可能不会得到任何回报。另一方面,自然触发的回收很可能会收回调用的成本。如果您有信息表明应该运行GC,那么您可以调用System.gc(),您应该会看到好处。然而,根据我的经验,这种情况只在少数边缘情况下发生,因为您不太可能有足够的信息来理解是否以及何时应该调用System.gc()。

这里列出了一个例子,在IDE中敲击垃圾桶。如果你要去开会,为什么不去呢?开销不会影响您,当您返回时,可能会清理堆。在生产系统中执行此操作,频繁调用收集将使其彻底停止!即使是RMI偶尔发出的调用也会对性能造成破坏。

有时候(不是经常!)你确实比运行时更了解过去、当前和未来的内存使用情况。这种情况并不经常发生,而且我敢说,在web应用程序中,当提供正常页面时,这种情况绝不会发生。

很多年前,我在一个报告生成器上工作

  • 只有一根线
  • 从队列中读取“报告请求”
  • 从数据库加载报告所需的数据
  • 生成报告并通过电子邮件发送出去。
  • 没完没了地重复,没有特别的要求就睡去。
  • 它没有在报告之间重复使用任何数据,也没有进行任何兑现。

首先,因为它不是实时的,而且用户希望等待报告,GC运行时的延迟不是问题,但是我们需要以比请求更快的速度生成报告。

看了上面的过程大纲,很明显。

  • 我们知道,在报告通过电子邮件发送出去之后,活动对象会非常少,因为下一个请求还没有开始处理。
  • 众所周知,运行垃圾收集周期的成本取决于活动物体的数量,,垃圾的数量对GC运行的成本几乎没有影响。
  • 当队列为空时,没有什么更好的事情可做,然后运行GC。

因此,当请求队列为空时执行GC运行显然是非常值得的;这并没有什么坏处。

在每个报告通过电子邮件发送之后执行GC运行可能是值得的,因为我们知道这是GC运行的好时机。但是,如果计算机有足够的ram,则可以通过延迟GC运行来获得更好的结果。

这种行为是在每个安装基础上配置的,对于一些客户,在每个报告极大的推动生成报告后启用强制GC。(我认为这是由于他们服务器上的内存较低,并且运行了许多其他进程,因此强制GC减少了分页。)

每次工作队列为空时,我们从未检测到一个安装没有从强制GC运行中获益。

但是,需要明确的是,上述情况并不常见。

现在,我更倾向于在单独的进程中运行每个报告,让操作系统清理内存,而不是使用垃圾收集器,并让自定义队列管理器服务在大型服务器上使用多个工作进程。

这是一个非常麻烦的问题,我觉得这是许多人反对Java的原因,尽管它是一种多么有用的语言。

你不能相信"系统"gc”来做任何事情都令人难以置信地令人生畏,并且很容易调用“恐惧,不确定,怀疑”的语言感觉。

在许多情况下,在重要事件发生之前处理您故意引起的内存峰值是很好的,这将导致用户认为您的程序设计很糟糕/反应迟钝。

拥有控制垃圾收集的能力将是一个非常好的教育工具,进而提高人们对垃圾收集如何工作以及如何使程序利用其默认行为和受控行为的理解。

让我回顾一下这篇文章的论点。

  1. 效率低下:

通常情况下,程序可能什么都不做,而您知道它什么都不做是因为它的设计方式。例如,它可能正在使用一个大的等待消息框进行某种长时间的等待,最后它可能会添加一个调用来收集垃圾,因为运行它的时间只占长等待时间的一小部分,但可以避免gc在更重要的操作中间发生故障。

  1. 这是一种不好的做法,表明代码有问题。

我不同意,不管你有什么垃圾收集器。它的工作是追踪垃圾并清理垃圾。

通过在使用不那么关键的时候调用gc,当您的生命依赖于正在运行的特定代码,但它却决定收集垃圾时,您可以减少gc运行的几率。

当然,它的行为可能不是您想要或期望的方式,但当您确实想要调用它时,您知道什么都没有发生,并且用户愿意容忍缓慢/停机。如果系统。Gc工作,太棒了!如果没有,至少你试过了。没有任何缺点,除非垃圾收集器具有固有的副作用,会对手动调用垃圾收集器的行为产生可怕的意想不到的影响,而这本身就会引起不信任。

  1. 这不是一个常见的用例:

这是一个不能可靠地实现的用例,但如果系统以这种方式设计,则可以实现。这就像做一个交通灯,让它的一些/所有的交通灯的按钮不做任何事情,这让你质疑为什么按钮在那里开始,javascript没有垃圾收集功能,所以我们没有仔细检查它。

  1. 规范说System.gc()提示GC应该运行,VM可以忽略它。

什么是“暗示”?什么是“忽略”?计算机不能简单地接受暗示或忽略某些东西,它所采取的严格行为路径可能是动态的,由系统的意图指导。一个正确的答案应该包括垃圾收集器在实现级别上实际做了什么,导致它在您请求它时不执行收集。这个功能只是一个nop吗?有什么条件是必须满足的吗?这些条件是什么?

就目前的情况而言,Java的GC通常看起来像一个不值得信任的怪物。你不知道它什么时候来,什么时候走,你不知道它会做什么,它会怎么做。我可以想象一些专家对他们的垃圾收集如何在每条指令的基础上工作有更好的想法,但绝大多数人只是希望它“只是工作”,不得不相信一个看起来不透明的算法为你工作是令人沮丧的。

阅读一些东西或学习一些东西,与实际看到它的实现,不同系统之间的差异,以及能够在不查看源代码的情况下使用它之间有很大的差距。这会创造自信和掌控/理解/控制的感觉。

总而言之,“这个功能可能不会做任何事情,我不会详细说明它什么时候会做什么事情,什么时候不会做,为什么不会或会做,这通常意味着尝试这样做是违反哲学的,即使背后的意图是合理的”,这是一个固有的问题。

Java GC的行为方式可能是可以的,也可能不是,但要理解它,很难真正遵循哪个方向来全面了解可以信任GC做什么和不做什么,所以很容易不信任语言,因为语言的目的是在哲学的程度上控制行为(对程序员来说很容易,特别是新手因为某些系统/语言行为而陷入生存危机),你有能力容忍(如果你不能,你只是不会使用语言,直到你不得不使用),更多你无法控制的事情,而不知道为什么你无法控制它们,这本质上是有害的。

    因为对象是使用new操作符动态分配的,所以
    您可能想知道这样的对象是如何被销毁的以及它们的

    在某些语言中,如c++,动态分配对象必须 使用删除操作符手动释放。

  1. Java采用不同的方法;它为您处理重新分配 李自动。< / > 实现此功能的技术称为垃圾收集。 它的工作原理是这样的:当不存在对对象的引用时,假定该对象不再需要,并且可以回收该对象占用的内存。不需要像c++那样显式地销毁对象
  2. 垃圾收集仅零星发生(如果有)
  3. 它不会仅仅因为存在一个或多个对象而发生 此外,不同的Java运行时实现将采用 不同的垃圾收集方法,但在大多数情况下,您 不应该在写程序的时候考虑它。

我将要写的一些内容只是对其他答案中已经写过的内容的总结,还有一些是新的。

问题“为什么调用System.gc()是不好的做法?”不计算。它假定这是不好的做法,但事实并非如此。这在很大程度上取决于你想要完成什么。

绝大多数程序员不需要System.gc(),在绝大多数用例中,它永远不会对他们做任何有用的事情。因此,对于大多数人来说,调用它是一种糟糕的做法,因为它不会做他们认为它会做的任何事情,它只会增加开销。

然而,在极少数情况下,调用System.gc()实际上是有益的:

  1. 当你绝对确定你有一些CPU时间可以空闲现在,并且你想提高稍后运行的代码的吞吐量。例如,一个web服务器发现目前没有待发的web请求,可以立即启动垃圾收集,以减少在稍后处理大量web请求期间需要垃圾收集的机会。(当然,如果在收集过程中有web请求到达,这可能会造成伤害,但web服务器可能会很聪明,如果有请求进入,就会放弃收集。)桌面gui是另一个例子:在空闲事件(或者更广泛地说,在一段时间不活动之后)上,您可以给JVM一个提示,如果它有任何垃圾收集要做,现在比以后好。

  2. 当你想检测内存泄漏。这通常与仅调试模式的终结器一起完成,或者从Java 9开始与java.lang.ref.Cleaner类一起完成。其思想是,通过强制垃圾收集现在,从而发现内存泄漏现在,而不是在未来的某个随机时间点,您可以在内存泄漏发生后尽快检测到内存泄漏,因此可以更好地准确地判断哪段代码泄漏了内存以及原因。(顺便说一句,这也是终结器或Cleaner的合法用例之一,或者可能是唯一的合法用例。使用终结来回收非托管资源的做法是有缺陷的,尽管这种做法非常普遍,甚至被官方推荐,因为它是不确定的。有关此主题的更多信息,请阅读:https://blog.michael.gr/2021/01/object-lifetime-awareness.html)

  3. 当您度量代码的性能时,(基准测试)是为了减少/最小化基准测试期间发生垃圾收集的机会,或者是为了保证基准测试期间由于垃圾收集而遭受的任何开销都是由于基准测试下的代码产生的垃圾,而不是不相关的代码。一个好的基准测试总是从尽可能彻底的垃圾收集开始。

  4. 当你测量代码的内存消耗,以确定一段代码产生了多少垃圾。其思想是执行一次完整的垃圾收集,以便在干净状态下开始,在度量下运行代码,获得堆大小,然后再执行一次完整的垃圾收集,再次获得堆大小,并计算差值。(顺便说一句,在运行度量下的代码时临时抑制垃圾收集的能力在这里是有用的,唉,JVM不支持这一点。这是可悲的。)

请注意,在上面的用例中,只有一个是在生产场景中;其余的在测试/诊断场景中。

这意味着System.gc()在某些情况下可能非常有用,这反过来意味着它“只是一个提示”;是有问题的。

(只要JVM没有提供一些确定性和有保证的方法来控制垃圾收集,JVM就会在这方面被破坏。)

下面是如何将System.gc()变成一个更少的提示:

private static void runGarbageCollection()
{
for( WeakReference<Object> ref = new WeakReference<>( new Object() ); ; )
{
System.gc(); //optional
Runtime.getRuntime().runFinalization(); //optional
if( ref.get() == null )
break;
Thread.yield();
}
}

这仍然不能保证您将得到一个完整的GC,但它已经很接近了。具体来说,即使使用了-XX:DisableExplicitGC VM选项,它也会给你一些垃圾收集。(因此,它真正使用System.gc()作为提示;它并不依赖于它。)