在本文中,我们将尝试解答围绕 System.gc() 调用的一些最常见问题。希望这能有所帮助。
什么是 System.gc()?
System.gc() 是 Java、Android、C# 和其他流行语言所提供的 API。调用时,其会尽最大努力从内存中清除积累的未引用对象(即:垃圾)。
谁会调用 System.gc()?
System.gc() 调用可由程序栈的多个部分进行:
- 可由程序开发人员显示调用 System.gc() 方法。
- 有时 System.gc() 可由第三方库、框架甚至是程序服务器触发。
- 可由外部工具(如 VisualVM)通过使用 JMX 触发
- 如果程序使用了 RMI,则 RMI 会周期性触发 System.gc()。
调用 System.gc() 的缺点是什么?
当程序调用“System.gc()”或“Runtime.getRuntime().gc()”的 API 时,将触发 stop-the-world Full GC 事件。在 stop-the-world Full GC 的过程中,整个 JVM 都会冻结(即:所有进行中的客户事务都将被暂停)。通常,这些 Full GC 需要很长时间才能完成。因此在不需要运行 GC 时,这可能会导致糟糕的用户体验和 SLA。
JVM 在后台运行着复杂的算法,计算着何时触发 GC。在调用 System.gc() 时,这些计算都会付诸东流。比如,在 JVM 触发了一次 GC 事件后的仅仅几毫秒内,您可能又会在程序中触发一次 System.gc()。因为仅从程序中您是无法知道 GC 会在何时运行的。
是否有足够好的理由来调用 System.gc() 呢?
我们还没遇到过很多从程序中调用 System.gc() 的好理由。不过这里有一个有关某大型航空公司的有趣例子。航空公司的程序需要用到 1TB 内存。程序的 Full GC 停顿需要大约 5 分钟才能完成。是的,不要震惊,就是 5 分钟(当然我们也见过 23 分钟的 GC 停顿时间案例)。为了避免停顿对客户体验造成影响,航空公司采用了一种十分聪明的解决方案。每晚,他们都会从负载均衡池中挑选一个 JVM 实例。然后通过该 JVM 上的 JMX 显式触发 System.gc()。GC 事件完成、垃圾从内存中清除后,再将 JVM 放回负载均衡池中。这确实是一个很聪明的方案,他们也得以最大限度地减少 5 分钟 GC 停顿时间对客户所造成的影响。
如何检测是否从您的程序中进行了 System.gc() 调用?
“谁会调用 System.gc()?”章节中提到过,System.gc() 会在多种源头进行调用,而不单单是通过程序的源代码调用。所以只是在程序代码中搜索“System.gc()”字符串并不足以判断程序是否进行了 System.gc() 调用。那么问题就来了:如何检测整个程序栈中是否触发了 System.gc() 调用呢?
这就是 GC 日志的实用之处了。在程序中启用 GC 日志吧。实际上我们建议在所有生产服务器中始终启用 GC 日志,因为这可以帮助您进行故障排除与程序性能优化工作。启用 GC 日志仅会增加几乎可忽略(如果有的话)的系统开销。您可将 GC 日志上传至垃圾回收日志分析工具(如:GCeasy、HP JMeter 等)中。这些工具可以生成内容丰富的垃圾回收分析报告。

图:由 GCeasy.io 工具提供的 GC 原因
上图是由 GCeasy 生成的报告中的“GC 原因”章节。您可以看到,“System.gc()”被调用了 304 次,占据了 52.42% 的 GC 停顿时间。
如何移除 System.gc() 调用?
您可通过以下方式移除显式的 System.gc() 调用:
a.搜索并替换
经典永不过时 :-)。在程序代码库中搜索“System.gc()”与“Runtime.getRuntime().gc()”。如果出现匹配,就将相应的内容移除。此方案适合“System.gc()”在您的程序源代码中触发的情形。如果“System.gc()”从第三方库、框架或外部源中调用,那么此方案将不起作用。在这种情况下,您可考虑使用 #b 中所列出的选项。
b. -XX:+DisableExplicitGC
您可以强制禁用 System.gc() 调用。启动程序时传递 JVM 参数“-XX:+DisableExplicitGC”即可。该选项将停用一切来自程序栈中任意位置的“System.gc()”调用。
c. -XX:+ExplicitGCInvokesConcurrent
您可传递 JVM 参数“-XX:+ExplicitGCInvokesConcurrent”。传递此参数时,GC 回收会与程序线程并发运行,进而缩短冗长的停顿时间。
d.RMI
如果您的程序使用了 RMI,则可控制其中进行“System.gc()”调用的频率。您可在程序启动时启用以下 JVM 参数来对频率进行配置:
-Dsun.rmi.dgc.server.gcInterval=n
-Dsun.rmi.dgc.client.gcInterval=n
这些属性的默认值为:
JDK 1.4.2 和 5.0 为 60000 毫秒(即 60 秒)
JDK 6 及更高版本为 3600000 毫秒(即 60 分钟)
您可将这些属性设置尽可能大的值以最大限度地减少影响。
Leave a Reply