zl程序教程

您现在的位置是:首页 >  Java

当前栏目

Java虚拟机System.gc()解析

2023-04-18 15:01:26 时间

对于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分钟)

同时,因业务场景差异化,我们可能需要将这些属性设置为非常高的值,以便可以将影响最小化。