【问题标题】:Can an external process force the JVM to throw a "java.lang.OutOfMemoryError: GC overhead limit exceeded"外部进程能否强制 JVM 抛出“java.lang.OutOfMemoryError:GC 开销限制超出”
【发布时间】:2020-10-01 11:31:22
【问题描述】:

在相同的操作系统和硬件上运行的另一个进程(java oder not)是否有可能触发一个

java.lang.OutOfMemoryError: GC overhead limit exceeded

通过消耗 RAM 和/或大量 CPU 负载 - 还是通过其他方式?


来自Java 8 documentation

详细信息“GC 开销限制超出”表示垃圾收集器一直在运行,Java 程序进展缓慢。垃圾回收后,如果 Java 进程花费大约 98% 以上的时间进行垃圾回收,并且回收的堆空间不到 2%...

这个somewhat older thread 我知道这是时间敏感的。然而,这 98% 所指的内容似乎缺乏适当的规范。

编辑 20201008: 添加了Link to the Garbage Collector Ergonomics

【问题讨论】:

标签: java jvm out-of-memory


【解决方案1】:

是的,但这在现实生活中不太可能发生。

要让JVM抛出java.lang.OutOfMemoryError: GC overhead limit exceeded,必须满足两个条件:

  1. GC 循环回收的堆空间少于GCHeapFreeLimit (2%);
  2. JVM 花费超过 GCTimeLimit (98%) 的时间进行 GC。

外部进程几乎不会影响第一个条件,除非它直接与目标应用程序交互。这意味着,JVM 应该已经处于“几乎内存不足”状态才能发生错误。

另一个过程可能会影响时间。如果此进程大量使用共享 CPU 资源,它会通过与 JVM 竞争 CPU 时间而使 GC 运行速度变慢。较慢的 GC 意味着更长的 GC 周期,因此花费在 GC 上的时间百分比更高。

当另一个进程让 JVM 抛出 GC overhead limit exceeded 时,我能够创建一个人工示例,但这真的很棘手。

考虑以下 Java 程序。

import java.util.ArrayList;

public class GCOverheadLimit {
    static ArrayList<Object> garbage = new ArrayList<>();
    static byte[] reserve = new byte[100_000];

    static void fillHeap() {
        try {
            while (true) {
                garbage.add(new byte[10_000]);
            }
        } catch (OutOfMemoryError e) {
            reserve = null;
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("Filling heap");
        fillHeap();

        System.out.println("Starting GC loop");
        while (true) {
            garbage.add(new byte[10_000]);
            garbage.remove(garbage.size() - 1);
            Thread.sleep(20);
        }
    }
}

首先,它用不可回收的对象填充整个堆,留下一小部分可用内存。然后在反复分配可回收垃圾使GC一次又一次地发生。迭代之间有一个小的延迟,以保持总 GC 开销低于 98%。

实验使用 1GB 堆和 Parallel GC:

java -Xmx1g -Xms1g -XX:+UseParallelGC GCOverheadLimit

我在一个有 CPU 配额的 cgroup 中运行这个程序。我的机器有 4 个内核,但我让 JVM 每 100 毫秒周期只使用 200 毫秒 CPU 时间。

mkdir /sys/fs/cgroup/cpu/test
echo 200000 > /sys/fs/cgroup/cpu/test/cpu.cfs_quota_us
echo $JAVA_PID > /sys/fs/cgroup/cpu/test/cgroup.procs

到目前为止,该程序运行良好。现在我在同一个 cgroup 中运行一两个 CPU 烧录进程:

sha1sum /dev/zero &
echo $! > /sys/fs/cgroup/cpu/test/cgroup.procs

由于超出配额,操作系统开始限制进程。 GC次数增加,JVM最终抛出java.lang.OutOfMemoryError: GC overhead limit exceeded

注意:重现问题需要仔细选择参数(堆大小、延迟、配额)。其他机器和其他环境的参数会有所不同。我的观点是 - 这个问题理论上是可能的,但在实践中可能永远不会发生,因为需要匹配的因素太多了。

【讨论】:

  • 天哪!我尝试了同样的事情(非常接近这个)但没有取得太大的成功,这非常,非常很好。但这是否意味着 另一个 进程触发了错误?这肯定看起来像另一个进程影响 JVM。我不确定是我自己还是这里可以互换。
  • @Eugene 对我来说,“强制 JVM 抛出错误”和“触发错误”是可以互换的。
  • 确实,一个很好的例子。所以基本上这归结为机器上的一些沉重的CPU(过载)负载,同时,GC处于压力之下。在更大的机器上,即 30+ HT 内核和 300+ GB 的 RAM,并行 GC 本身可能会施加相当多的 CPU 负载。竞争进程需要多少额外的 CPU 负载才能“倾斜”?这仅仅是因为 Parallel GC(可能)基于经过的实时/墙上时间而不是 CPU 时间来计算时间吗?
  • @Andreas 对,GC 开销策略计算暂停时间,而不是 CPU 时间。我不能说 30 多个内核,但我的观点是——即使理论上另一个进程可能导致 JVM 抛出“GC 开销限制”错误,但在实践中并不重要:当可用内存如此之低时,还是有问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-21
  • 1970-01-01
  • 2011-02-21
相关资源
最近更新 更多