System.gc()指南

2023/06/12

1. 概述

在本教程中,我们将研究位于java.lang包中的System.gc()方法。

显示调用System.gc()是一种不好的做法,让我们尝试了解调用此方法可能有用的原因以及是否存在任何用例。

2. 垃圾回收

当有指示时,Java虚拟机决定执行垃圾回收。这些指示因GC实现而异。它们基于不同的启发式。但是,有某些时刻肯定会执行GC:

  • 老年代(Tenured space)已满,触发major/full GC
  • 新生代(Eden + Survivor0 + Survivor1 spaces)已满,触发minor GC

唯一独立于GC实现的是对象是否有资格被垃圾回收

现在,我们来看看System.gc()方法本身。

3. System.gc()

方法的调用很简单:

System.gc()

Oracle官方文档指出:

调用gc方法表明Java虚拟机努力回收未使用的对象,以便使它们当前占用的内存可用于快速重用。

不能保证会触发实际的GC

System.gc()触发major GC。因此,根据你的垃圾回收器实现情况,存在在stop-the-world阶段花费一些时间的风险。结果,我们有一个不可靠的工具,可能会严重影响性能

对每个人来说,显式垃圾回收调用的存在应该是一个严重的危险信号。

我们可以通过使用-XX:DisableExplicitGC JVM标志来阻止System.gc()做任何工作。

3.1 性能调优

值得注意的是,就在抛出OutOfMemoryError之前,JVM将执行full GC。因此,显式调用System.gc()不会使我们免于失败

现在的垃圾回收器真的很聪明。他们了解内存使用情况和其他统计数据,以便能够做出正确的决定。因此,我们应该信任他们。

在内存问题的情况下,我们有一堆设置可以更改以调整我们的应用程序-从选择不同的垃圾回收器开始,通过设置所需的应用程序时间/GC时间比率,最后以设置内存段的固定大小结束。

还有一些方法可以减轻由显式调用引起的Full GC的影响。我们可以使用其中一个标志:

-XX:+ExplicitGCInvokesConcurrent

或者:

-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

如果我们真的希望我们的应用程序正常工作,我们应该解决真正的底层内存问题

在下一章中,我们将看到显式调用System.gc()似乎很有用的实际示例。

4. 使用示例

4.1 场景

让我们编写一个测试应用程序,我们想找到调用System.gc()可能有用的情况

minor垃圾回收比major垃圾回收更频繁。因此,我们或许应该关注后者。如果单个对象“幸存”了几次收集并且仍然可以从GC根访问,则它会被移动到永久空间。

假设我们有大量存在一段时间的对象。然后,在某个时候,我们正在清除对象集合。也许现在是运行System.gc()的好时机?

4.2 演示应用程序

我们将创建一个简单的控制台应用程序,使我们能够模拟该场景:

public class DemoApplication {

    private static final Map<String, String> cache = new HashMap<String, String>();

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNext()) {
            final String next = scanner.next();
            if ("fill".equals(next)) {
                for (int i = 0; i < 1000000; i++) {
                    cache.put(randomUUID().toString(), randomUUID().toString());
                }
            } else if ("invalidate".equals(next)) {
                cache.clear();
            } else if ("gc".equals(next)) {
                System.gc();
            } else if ("exit".equals(next)) {
                System.exit(0);
            } else {
                System.out.println("unknown");
            }
        }
    }
}

4.3 运行程序

让我们使用一些额外的标志来运行我们的应用程序:

-XX:+PrintGCDetails -Xloggc:gclog.log -Xms100M -Xmx500M -XX:+UseConcMarkSweepGC

需要前两个标志来记录GC信息。接下来的两个标志设置初始堆大小,然后设置最大堆大小。我们希望保持较小的堆大小以强制GC更加活跃。最后,我们决定使用CMS-并发标记和清除垃圾回收器。是时候运行我们的应用程序了!

首先,让我们尝试填充永久空间。键入fill。

我们可以检查我们的gclog.log文件以查看发生了什么。我们将看到大约15个集合。为单个集合记录的行如下所示:

197.057: [GC (Allocation Failure) 197.057: [ParNew: 67498K->40K(75840K), 0.0016945 secs] 
  168754K->101295K(244192K), 0.0017865 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] secs]

如我们所见,内存已满。

接下来,让我们通过键入gc强制执行System.gc()。我们可以看到内存使用情况没有显著变化:

238.810: [Full GC (System.gc()) 238.810: [CMS: 101255K->101231K(168352K); 0.2634318 secs] 
  120693K->101231K(244192K), [Metaspace: 32186K->32186K(1079296K)], 0.2635908 secs] 
  [Times: user=0.27 sys=0.00, real=0.26 secs]

再运行几次后,我们将看到内存大小保持在同一水平。

让我们通过输入invalidate来清除缓存。我们应该不会在gclog.log文件中看到更多的日志行。

我们可以尝试多次填充缓存,但没有GC发生。这是我们可以智取垃圾回收器的时刻。现在,在强制GC之后,我们将看到如下一行:

262.124: [Full GC (System.gc()) 262.124: [CMS: 101523K->14122K(169324K); 0.0975656 secs] 
  103369K->14122K(245612K), [Metaspace: 32203K->32203K(1079296K)], 0.0977279 secs]
  [Times: user=0.10 sys=0.00, real=0.10 secs]

我们释放了大量内存!但现在真的有必要吗?发生了什么?

根据这个例子,当我们释放大对象或使缓存失效时,调用System.gc()似乎很诱人。

5. 其他用途

显式调用System.gc()方法可能有用的原因很少。

一个可能的原因是在服务器启动后清理内存-我们正在启动一个服务器或应用程序,它会做很多准备工作。之后,还有很多对象需要最终确定。然而,这种准备后的清理不应该是我们的责任。

另一个是内存泄漏分析-它更像是一种调试实践,而不是我们希望保留在生产代码中的东西。调用System.gc()并看到堆空间仍然很高可能表明存在内存泄漏

6. 总结

在本文中,我们研究了System.gc()方法以及它何时可能有用。

当涉及到我们应用程序的正确性时,我们永远不应该依赖它。GC在大多数情况下比我们更聪明,如果出现任何内存问题,我们应该考虑调整虚拟机而不是进行这种显式调用。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章