是的,但这在现实生活中不太可能发生。
要让JVM抛出java.lang.OutOfMemoryError: GC overhead limit exceeded,必须满足两个条件:
- GC 循环回收的堆空间少于
GCHeapFreeLimit (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。
注意:重现问题需要仔细选择参数(堆大小、延迟、配额)。其他机器和其他环境的参数会有所不同。我的观点是 - 这个问题理论上是可能的,但在实践中可能永远不会发生,因为需要匹配的因素太多了。