对于Java语言来说是不用刻意手动去释放内存,同时,也尽可能不需要手动去干预Java虚拟机的GC行为。在本篇文章中,我们试图从多个方面去解析有关System.gc()API调用的最常见问题。希望对需要了解这块技术的朋友有所帮助。
System.gc()是干什么的?
System.gc()是Java,Android,Go、C#以及其他流行语言所提供的API。当调用时,它将尽最大努力从内存中清除累积的未引用对象(即我们常说的垃圾回收)。Hotspot为我们开放了Java语言级别的GC手动触发入口System.gc(),我们可以看下其源码,首先,我们了解下 java.lang.System#gc()方法,具体如下所示:
/** * Runs the garbage collector. * <p> * Calling the <code>gc</code> method suggests that the Java Virtual * Machine expend effort toward recycling unused objects in order to * make the memory they currently occupy available for quick reuse. * When control returns from the method call, the Java Virtual * Machine has made a best effort to reclaim space from all discarded * objects. * <p> * The call <code>System.gc()</code> is effectively equivalent to the * call: * <blockquote><pre> * Runtime.getRuntime().gc() * </pre></blockquote> * * @see java.lang.Runtime#gc() */ public static void gc() { Runtime.getRuntime().gc(); }
根据上述源码,我们追溯到java.lang.Runtime#gc()方法,具体如下所示:
/** * Runs the garbage collector. * Calling this method suggests that the Java virtual machine expend * effort toward recycling unused objects in order to make the memory * they currently occupy available for quick reuse. When control * returns from the method call, the virtual machine has made * its best effort to recycle all discarded objects. * <p> * The name <code>gc</code> stands for "garbage * collector". The virtual machine performs this recycling * process automatically as needed, in a separate thread, even if the * <code>gc</code> method is not invoked explicitly. * <p> * The method {@link System#gc()} is the conventional and convenient * means of invoking this method. */ public native void gc();
有关Runtime.c # Java_java_lang_Runtime_gc()方法调用了native方法gc(),对应的方法
在Hotspot源码:src/java.base/share/native/libjava/Runtime.c,具体如下所示:
JNIEXPORT void JNICALL Java_java_lang_Runtime_gc(JNIEnv *env, jobject this) { JVM_GC(); }
有关jvm.cpp方法实现在src/hotspot/share/prims/jvm.cpp,具体如下所示:
JVM_ENTRY_NO_ENV(void, JVM_GC(void)) JVMWrapper("JVM_GC"); if (!DisableExplicitGC) { // 调用具体堆实现的collect方法 Universe::heap()->collect(GCCause::_java_lang_system_gc); } JVM_END
通过Universe调用具体堆实现的collect方法,取决于使用当前实例使用的GC模式,在JVM中目前堆实现主要有:
- 串行回收堆实现 - src/hotspot/share/gc/serial/defNewGeneration.cpp(年轻代) - src/hotspot/share/gc/serial/tenuredGeneration.cpp(年老代) - 并行回收堆实现 - src/hotspot/share/gc/parallel/parallelScavengeHeap.cpp - CMS并发回收堆实现 - src/hotspot/share/gc/shared/genCollectedHeap.cpp - G1并发回收堆实现 - src/hotspot/share/gc/g1/g1CollectedHeap.cpp
我们可以通过分析应用程序堆栈信息,探索调用System.gc()的相关可疑线索,通常主要有以下几种场景:
1、应用程序能正在显式调用System.gc()方法。
2、由应用程序触发的第三方库,框架甚至其他底层组件调用System.gc()方法。
3、使用JMX从外部工具(如VisualVM)触发。
4、若应用程序或其调用的框架正在使用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的场景下尽可能不要去触发此种不必要的操作,毕竟,触发System.gc()有可能导致业务中断以及不良的用户体验,遭到客户的投诉。
Java虚拟机本身具有复杂的算法,该算法跟随者我们的应用逻辑处理默默在后台运行,进行着所有计算以及有关何时触发GC的计算。当我们程序调用System.gc()时,所有这些计算都将投入使用。如果JVM仅在一毫秒后触发了GC事件,然后又从应用程序中再次调用System.gc(),该怎么办?因为从我们的应用程序压根就不清楚GC何时运行。
什么场景下调用System.gc()?
目前,无论是基于早期的JDK 1.4版本还是现在的JDK 15,不建议、也不推荐应用程序调用System.gc()方法。但是,可能在某种特定的场景下,也可尝试调用此方法达到应用服务性能最大化。
以下为某一家大型航空公司实际案例:该应用程序使用1 TB的内存。此应用程序的完整GC暂停时间大约需要5分钟才能完成。是的,它是5分钟(但我们也看到了GC暂停时间为23分钟的情况)。为了避免由于此暂停时间而对客户造成的影响,该航空公司已实施了明智的解决方案。每天晚上,他们一次从负载均衡器池中取出一个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日志通过垃圾收集日志分析器工具进行分析,从而可生成丰富的垃圾收集分析报告,如下图:
如何禁用System.gc()调用?
我们可以通过以下解决方案去除显式的System.gc()调用:
1、搜索和替换
这可能是一种传统方法,但是可以实现一部分场景。在应用程序代码库中搜索“ System.gc()”和“ Runtime.getRuntime().gc()”。如果看到匹配项,则将其移除。如果从我们的应用程序源代码中调用“ System.gc()”,则此解决方案将起作用。如果要从我们的第三方库,框架或通过外部源调用“ System.gc()”,则此解决方案将无法使用。在这种情况下,我们可以考虑使用#2中概述的选项。
2、-XX: + DisableExplicitGC参数
启动应用程序时,可以通过配置JVM参数“ -XX:+ DisableExplicitGC”来强制禁用System.gc()调用。此选项将使在应用程序堆栈中任何位置调用的所有“ System.gc()”调用静音。
3、-XX: + ExplicitGCInvokesConcurrent参数
我们可以配置“ -XX:+ ExplicitGCInvokesConcurrent” JVM参数。传递此参数后,GC集合将与应用程序线程同时运行,以减少冗长的暂停时间。
4、 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分钟)
同时,因业务场景差异化,我们可能需要将这些属性设置为非常高的值,以便可以将影响最小化。