在 answering 提出一个关于如何使用 System.gc()
force-free objects in Java(这个人正在清除 1.5GB HashMap)的问题之后,有人告诉我手动调用 System.gc()
是不好的做法,但评论并不完全令人信服。此外,似乎没有人敢赞成,也不敢反对我的回答。
有人告诉我这是不好的做法,但后来我也被告知垃圾收集器运行不会再系统地停止世界,它也可以被 JVM 有效地用作提示,所以我有点不知所措。
我确实理解 JVM 在需要回收内存时通常比您更清楚。我也明白担心几千字节的数据是愚蠢的。我也明白,即使是兆字节的数据也不是几年前的样子。但是,1.5 GB?您知道内存中大约有 1.5 GB 的数据;这不像是在黑暗中拍摄。 System.gc()
系统性地不好,还是在某个时候它变得好起来了?
所以问题实际上是双重的:
为什么调用 System.gc() 是或不是不好的做法?它真的只是在某些实现下对 JVM 的提示,还是它总是一个完整的收集周期?真的有垃圾收集器实现可以在不停止世界的情况下完成它们的工作吗?请阐明人们在对我的回答的评论中所做的各种断言。
门槛在哪里?调用 System.gc() 从来都不是一个好主意,还是有时可以接受?如果是这样,那是什么时候?
每个人都说要避免使用 System.gc()
的原因是它很好地指示了根本性的代码损坏。任何依赖它来保证正确性的代码肯定会被破坏;任何依赖它来获得性能的东西都很可能被破坏。
您不知道您正在运行哪种垃圾收集器。当然,有些 JVM 不会像您断言的那样“停止世界”,但有些 JVM 并不那么聪明或出于各种原因(也许它们在电话上?)不这样做。你不知道它会做什么。
此外,它不能保证做任何事情。 JVM 可能会完全忽略您的请求。
“你不知道它会做什么”、“你不知道它是否会有所帮助”和“无论如何你都不应该调用它”的组合是为什么人们通常如此有力地说你不应该打电话给它。我认为这是“如果您需要询问是否应该使用它,则不应该”的情况
编辑以解决另一个线程的一些问题:
在阅读了您链接的主题后,我还想指出一些事情。首先,有人建议调用 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()
可能 什么都不做,并且任何“需要”垃圾收集器运行的代码都会被破坏。
但是,调用 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 将等到堆满再运行,从而高效收集垃圾1。但是如果应用程序强制 GC 运行,很有可能堆不会满,结果是垃圾收集效率低下。而且应用程序强制 GC 的频率越高,GC 的效率就越低。
注意:上面的解释掩盖了一个典型的现代 GC 将堆划分为“空间”,GC 可能会动态扩展堆,应用程序的非垃圾对象的工作集可能会变化等事实。即便如此,同样的基本原则也适用于所有真正的垃圾收集器2。强制 GC 运行是低效的。
1 - 这就是“吞吐量”收集器的工作方式。 CMS 和 G1 等并发收集器使用不同的标准来决定何时启动垃圾收集器。
2 - 我也排除了专门使用引用计数的内存管理器,但当前的 Java 实现没有使用这种方法......这是有充分理由的。
很多人似乎在告诉你不要这样做。我不同意。如果在加载关卡等大型加载过程之后,您认为:
您有很多无法访问且可能尚未被 gc'ed 的对象。并且您认为用户此时可以忍受小幅减速
调用 System.gc() 没有害处。我把它看成 c/c++ inline
关键字。这只是对 gc 的一个提示,您,开发人员,已经决定时间/性能并不像通常那样重要,并且其中一些可以用于回收内存。
不依赖它做任何事情的建议是正确的。不要依赖它的工作,但暗示现在是一个可以接受的收集时间是完全可以的。我宁愿在代码中无关紧要的地方(加载屏幕)浪费时间,也不愿在用户积极与程序交互时(比如在游戏关卡中)。
有一次我会force收集:当试图找出某个特定对象泄漏时(本机代码或大型、复杂的回调交互。哦,还有任何 UI 组件,只要瞥一眼 Matlab。)这绝不应该在生产代码中使用。
stop the world
方法,如果发生这种情况,这是真正的害处
人们一直在很好地解释为什么不使用它,所以我会告诉你几个你应该使用它的情况:
(以下评论适用于在 Linux 上使用 CMS 收集器运行的 Hotspot,我有信心说 System.gc()
实际上总是调用完整的垃圾收集)。
在启动您的应用程序的初始工作之后,您的内存使用情况可能很糟糕。你有一半的老一代可能充满了垃圾,这意味着你离你的第一个 CMS 更近了。在重要的应用程序中,调用 System.gc() 将堆“重置”为实时数据的起始状态并不是一个坏主意。与 #1 相同,如果您密切监视堆使用情况,您希望准确了解基线内存使用情况。如果您的应用程序正常运行时间的前 2 分钟都是初始化,那么您的数据将会被弄乱,除非您强制(咳咳……“建议”)预先执行完整的 gc。您可能有一个应用程序,它被设计为在运行时永远不会向终身代提升任何东西。但也许您需要预先初始化一些数据,这些数据不是那么大,以至于自动转移到终身代。除非您在所有内容都设置好后调用 System.gc(),否则您的数据可能会保留在新一代中,直到需要升级为止。突然之间,您的超级低延迟、低 GC 应用程序会因在正常操作期间提升这些对象而遭受巨大的(当然是相对而言)延迟损失。有时在生产应用程序中调用 System.gc 来验证是否存在内存泄漏很有用。如果您知道时间 X 的实时数据集与时间 Y 的实时数据集应以一定比例存在,那么在时间 X 和时间 Y 调用 System.gc() 并比较内存使用情况可能会很有用.
System.gc()
一次来强制提升对象不会给您带来任何好处。而且您肯定不想连续调用 System.gc()
八次并祈祷现在提升已经完成,并且以后提升所节省的成本证明了多次完整 GC 的成本是合理的。根据 GC 算法,提升大量对象甚至可能不承担实际成本,因为它只会将内存重新分配给老年代或并发复制......
这是一个非常令人烦恼的问题,尽管 Java 是一种非常有用的语言,但我觉得它促成了许多反对 Java 的人。
您不能信任“System.gc”做任何事情的事实令人难以置信,并且可以轻松地唤起该语言的“恐惧,不确定性,怀疑”感觉。
在许多情况下,最好在重要事件发生之前处理您故意造成的内存峰值,这会导致用户认为您的程序设计不良/反应迟钝。
拥有控制垃圾收集的能力将是一个很好的教育工具,进而提高人们对垃圾收集如何工作以及如何使程序利用它的默认行为和受控行为的理解。
让我回顾一下这个线程的论点。
这是低效的:
通常,该程序可能没有做任何事情,并且您知道它没有做任何事情,因为它的设计方式。例如,它可能正在使用一个大的等待消息框进行某种长时间的等待,最后它还可以添加一个收集垃圾的调用,因为运行它的时间将只占用运行时间的一小部分。漫长的等待,但会避免 gc 在更重要的操作中发挥作用。
这始终是一种不好的做法,并表明代码损坏。
我不同意,你有什么垃圾收集器并不重要。它的工作是跟踪和清理垃圾。
通过在使用不那么重要的时候调用 gc,当你的生活依赖于正在运行的特定代码但它决定收集垃圾时,你可以降低它运行的几率。
当然,它可能不会按照您想要或期望的方式运行,但是当您确实想调用它时,您知道什么都没有发生,并且用户愿意容忍缓慢/停机时间。如果 System.gc 有效,那就太好了!如果没有,至少你试过了。除非垃圾收集器具有固有的副作用,如果手动调用垃圾收集器的行为方式会产生可怕的意外,并且这本身会引起不信任,否则根本没有不利的一面。
这不是一个常见的用例:
这是一个无法可靠实现的用例,但如果系统是以这种方式设计的,则可能是这样。这就像制作一个红绿灯并使其部分/所有红绿灯的按钮不做任何事情,这让你质疑为什么按钮在那里开始,javascript没有垃圾收集功能所以我们没有不要为此仔细检查它。
规范说 System.gc() 是 GC 应该运行的提示,VM 可以随意忽略它。
什么是“提示”?什么是“忽略”?计算机不能简单地接受提示或忽略某事,它采用严格的行为路径,这些路径可能是动态的,由系统的意图引导。一个正确的答案将包括垃圾收集器在实现级别实际上在做什么,这会导致它在您请求它时不执行收集。该功能只是一个nop吗?我必须满足某种条件吗?这些条件是什么?
就目前而言,Java 的 GC 通常看起来像是一个您不信任的怪物。你不知道它什么时候来或去,你不知道它会做什么,它会怎么做。我可以想象一些专家对他们的垃圾收集如何在每条指令的基础上工作有更好的了解,但绝大多数人只是希望它“正常工作”,而不得不相信一个看似不透明的算法来为你工作是令人沮丧的。
在阅读某事或被教导某事与实际看到它的实现、系统之间的差异以及无需查看源代码即可使用它之间存在很大差距。这会产生自信和掌握/理解/控制的感觉。
总而言之,答案存在一个固有的问题“这个功能可能不会做任何事情,我不会详细说明如何判断它什么时候做某事,什么时候不做,以及为什么它不会或会做某事,经常暗示尝试这样做完全违反哲学,即使其背后的意图是合理的”。
Java GC 的行为方式可能是可以的,也可能不是,但要理解它,很难真正遵循哪个方向去全面了解您可以信任 GC 做什么和不要这样做,所以简单地不信任语言太容易了,因为语言的目的是在哲学范围内控制行为(程序员,尤其是新手很容易因某些系统/语言行为而陷入生存危机)你有能力容忍(如果你不能,你就不会使用这种语言,直到你不得不这样做),而更多你无法控制的事情,因为你无法控制它们,本质上是有害的。
有时(不经常!)您确实比运行时更了解过去、当前和未来的内存使用情况。这种情况不会经常发生,我会声称在提供普通页面时永远不会在 Web 应用程序中。
许多年前,我在一个报告生成器上工作,那个
有一个线程
从队列中读取“报告请求”
从数据库中加载报表所需的数据
生成报告并通过电子邮件发送出去。
永远重复,在没有未完成的请求时休眠。
它没有在报告之间重复使用任何数据,也没有进行任何兑现。
首先,由于它不是实时的,并且用户希望等待报告,因此 GC 运行时的延迟不是问题,但我们需要以比要求更快的速度生成报告。
看上面的流程大纲,就很清楚了。
我们知道,在通过电子邮件发送报告后,活动对象将非常少,因为下一个请求尚未开始处理。
众所周知,运行一次垃圾回收周期的成本取决于存活对象的数量,垃圾量对 GC 运行的成本影响不大。
当队列为空时,没有什么比运行 GC 更好的办法了。
因此很明显,当请求队列为空时执行 GC 运行是非常值得的;这没有缺点。
在通过电子邮件发送每个报告后运行 GC 可能是值得的,因为我们知道这是运行 GC 的好时机。但是,如果计算机有足够的内存,则通过延迟 GC 运行可以获得更好的结果。
此行为是在每个安装基础上配置的,对于某些客户而言,在每次报告后启用强制 GC 大大加快了报告的生成速度。 (我预计这是由于他们的服务器上的内存不足,并且它运行了许多其他进程,因此很好的时间强制 GC 减少了分页。)
每次工作队列为空时,我们从未检测到没有从强制 GC 运行中受益的安装。
但是,请明确一点,上述情况并不常见。
这些天来,我更倾向于在单独的进程中运行每个报告,让操作系统清理内存而不是垃圾收集器,并让自定义队列管理器服务在大型服务器上使用多个工作进程。
GC 效率依赖于许多启发式方法。例如,一个常见的启发式方法是对对象的写访问通常发生在不久前创建的对象上。另一个是许多对象的寿命很短(有些对象会使用很长时间,但许多对象会在创建后几微秒内被丢弃)。
调用 System.gc()
就像踢 GC。它的意思是:“所有那些精心调整的参数,那些聪明的组织,你为分配和管理对象所做的所有努力,以使事情顺利进行,好吧,放下所有的一切,从头开始”。它可能提高性能,但大多数时候它只是降低性能。
要可靠地使用 System.gc()
(*),您需要了解 GC 的所有细节是如何运行的。如果您使用来自其他供应商的 JVM,或者使用来自同一供应商的下一个版本,或者使用相同的 JVM 但命令行选项略有不同,这些细节往往会发生很大变化。因此,除非您想解决控制所有这些参数的特定问题,否则这很少是一个好主意。因此出现了“不良做法”的概念:这不是被禁止的,这种方法是存在的,但它很少有回报。
(*) 我在这里谈论的是效率。 System.gc()
永远不会破坏正确的 Java 程序。它不会产生 JVM 否则无法获得的额外内存:在抛出 OutOfMemoryError
之前,JVM 会完成 System.gc()
的工作,即使是最后的手段。
也许我写了蹩脚的代码,但我开始意识到单击 eclipse 和 netbeans IDE 上的垃圾桶图标是一种“好习惯”。
System.gc()
,您可能会发现这种行为很烦人。
是的,调用 System.gc() 并不能保证它会运行,它是对 JVM 的请求,可能会被忽略。从文档:
调用 gc 方法表明 Java 虚拟机花费精力来回收未使用的对象
调用它几乎总是一个坏主意,因为自动内存管理通常比你更清楚何时使用 gc。当它的内部可用内存池不足时,或者如果操作系统请求返还一些内存,它就会这样做。
如果您知道它有帮助,调用 System.gc() 可能是可以接受的。我的意思是你已经在部署平台上彻底测试和测量了这两种场景的行为,你可以证明它有帮助。请注意,虽然 gc 不容易预测 - 它可能有助于一次运行并伤害另一次。
首先,规格和现实之间存在差异。规范说 System.gc() 是 GC 应该运行的提示,VM 可以随意忽略它。现实情况是,VM 永远不会忽略对 System.gc() 的调用。
调用 GC 会带来不小的开销,如果您在某个随机时间点执行此操作,您的努力可能不会得到任何回报。另一方面,自然触发的收款很有可能收回通话成本。如果您有指示应该运行 GC 的信息,那么您可以调用 System.gc() 并且您应该会看到好处。但是,根据我的经验,这仅在少数极端情况下发生,因为您不太可能有足够的信息来了解是否以及何时应该调用 System.gc()。
此处列出的一个示例是在您的 IDE 中使用垃圾桶。如果你要去开会,为什么不去开会。开销不会影响您,并且当您回来时可能会清理堆。在生产系统中执行此操作,频繁调用 collect 会使其停止运行!即使是偶尔的调用,例如 RMI 发出的调用,也会对性能造成破坏。
我将要写的一些内容只是对其他答案中已经写过的内容的总结,还有一些是新的。
问题“为什么调用 System.gc() 是不好的做法?”不计算。它假定这是不好的做法,但事实并非如此。这在很大程度上取决于您要完成的工作。
绝大多数程序员不需要 System.gc()
,并且在绝大多数用例中它永远不会对他们做任何有用的事情。因此,对于大多数人来说,调用它是不好的做法,因为它不会做他们认为会做的任何事情,它只会增加开销。
但是,在少数情况下调用 System.gc()
实际上是有益的:
当您绝对确定您现在有一些 CPU 时间可用,并且您希望提高稍后运行的代码的吞吐量时。例如,Web 服务器发现此时没有待处理的 Web 请求,现在可以启动垃圾收集,以减少在以后处理一连串 Web 请求时需要进行垃圾收集的机会。 (当然,如果 Web 请求在收集期间到达,这可能会造成伤害,但 Web 服务器可能对此很聪明,如果请求进入,则放弃收集。)桌面 GUI 是另一个示例:在空闲事件(或更广泛地说,在一段时间不活动,)你可以给 JVM 一个提示,如果它有任何垃圾收集要做,现在总比以后好。当您想检测内存泄漏时。这通常与仅调试模式的终结器或 Java 9 及更高版本的 java.lang.ref.Cleaner 类结合使用。这个想法是,通过现在强制垃圾收集,从而发现现在的内存泄漏而不是将来的某个随机时间点,您可以在内存泄漏发生后尽快检测到,因此处于更好的位置准确判断哪段代码泄漏了内存以及原因。 (顺便说一句,这也是终结器或清洁器的合法用例之一,或者可能是唯一的合法用例。使用终结器来回收非托管资源的做法是有缺陷的,尽管它非常普遍甚至官方推荐,因为它不是确定性。有关此主题的更多信息,请阅读:https://blog.michael.gr/2021/01/object-lifetime-awareness.html)当您测量代码的性能时,(基准测试)以减少/最小化在基准测试期间发生垃圾收集的机会,或者为了保证在基准测试期间由于垃圾收集而遭受的任何开销都是由于基准测试代码生成的垃圾,而不是无关代码。一个好的基准总是从尽可能彻底的垃圾收集开始。当你在测量代码的内存消耗时,为了确定一段代码产生了多少垃圾。想法是执行一次完整的垃圾回收,以从干净的状态开始,运行被测量的代码,获取堆大小,然后再进行一次完整的垃圾回收,再次获取堆大小,并取差值。 (顺便说一句,在运行被测代码时暂时抑制垃圾收集的能力在这里很有用,可惜 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()
作为提示;它不依赖它。)
根据我的经验,使用 System.gc() 实际上是一种特定于平台的优化形式(其中“平台”是硬件架构、操作系统、JVM 版本和可能的更多运行时参数(例如可用 RAM)的组合),因为它的行为,虽然在特定平台上大致可预测,但可以(并且将会)在平台之间有很大差异。
是的,在某些情况下 System.gc() 会提高(感知)性能。例如,如果延迟在您的应用程序的某些部分是可以容忍的,但在其他部分是不能容忍的(上面引用的游戏示例,您希望 GC 在关卡开始时发生,而不是在关卡期间发生)。
然而,它是否会帮助或伤害(或什么都不做)在很大程度上取决于平台(如上所述)。
所以我认为它作为最后的平台特定优化是有效的(即如果其他性能优化还不够的话)。但是你永远不应该仅仅因为你相信它可能会有所帮助(没有特定的基准)就调用它,因为它很可能不会。
由于对象是使用 new 运算符动态分配的,您可能想知道如何销毁这些对象以及如何释放它们的内存以供以后重新分配。在某些语言中,例如 C++,动态分配的对象必须通过使用删除操作符手动释放。 Java 采用了不同的方法。它会自动为您处理释放。实现这一点的技术称为垃圾收集。它的工作原理是这样的:当不存在对某个对象的引用时,假定不再需要该对象,并且可以回收该对象占用的内存。没有像在 C++ 中那样明确需要销毁对象。垃圾收集仅在程序执行期间偶尔发生(如果有的话)。它不会仅仅因为存在一个或多个不再使用的对象而发生。此外,不同的 Java 运行时实现将采用不同的垃圾收集方法,但在大多数情况下,您不必在编写程序时考虑它。
System.gc()
很有用,甚至可能是必要的。例如,在 Windows 上的 UI 应用程序中,当您在最小化 Window 之前调用 System.gc() (尤其是当它保持最小化一段时间并且该过程的一部分被交换到磁盘)。WeakReference
用于您想要保留的对象的代码从一开始就是不正确的,垃圾收集与否。您在使用std::weak_ptr
的 C++ 中会遇到同样的问题(尽管您可能会注意到 C++ 版本中的问题早于 Java 版本中的问题,因为对象销毁不会像通常的终结那样被推迟)。System.gc()
修复它的事实是一种解决方法,而不是良好的编码习惯。