2.1 GC overhead limit exceeded 概述
Java 运行时环境包含一个内建的垃圾收集线程. 在很多其他编程语言中, 开发者需要手动分配和释放内存区域, 以保证释放的内存可以被复用.
但是 Java 应用只需要分配内存. 只要一个特定的内存空间不再使用, 一个单独的叫做 垃圾收集 的线程会清理内存. GC 如何检测特定内存不再使用的内容超出本文范围, 但是你可以信任 GC 可以把这个活做的很好.
java.lang.OutOfMemoryError: GC overhead limit exceeded
(GC 开销超过限制)错误意味着 GC 尝试释放内存但是却无法完成任何一件事情. 默认它发生在: JVM 在 GC 中花费超过 98% 的时间,GC 之后, 只有不到 2% 的堆被释放.
java.lang.OutOfMemoryError: GC overhead limit exceeded
错误在以下场景会出现: 你的应用消耗了相当多的可用内存, GC 反复尝试清理, 但均以失败告终.
2.2 原因
java.lang.OutOfMemoryError: GC overhead limit exceeded
错误就是 JVM 在发信号告诉你: 应用消耗了太多时间在做垃圾收集, 而且还收效甚微. 默认情况的配置是 JVM 会在以下情况下抛出该错误: JVM 在 GC 中花费超过 98% 的时间,GC 之后, 只有不到 2% 的堆被释放.
如果这个 GC 开销限制不存在,会发生什么情况? 需要注意的是 java.lang.OutOfMemoryError: GC overhead limit exceeded
错误只有当在几次 GC 周期之后, 只有 2% 的内存被释放的情况下才会抛出. 这意味着只有很少量的内存 GC 能够清理, 而且还会再次被迅速填满, 导致 GC 再次开始清理线程. 这形成了一个恶性循环,CPU 100% 在忙于 GC,没有实际工作可做. 应用最终用户面临着极端响应慢的情况 – 本来几毫秒就能完成的操作需要花费几分钟来完成.
所以, java.lang.OutOfMemoryError: GC overhead limit exceeded
消息是一个实现 快速失败 (fail-fast) 原则的相当好的案例.
2.3 案例
在下列案例中, 我们通过初始化一个 Map 并且在一个无限循环里增加一个 key-value 对到该 Map 来创建一个 GC overhead limit exceeded
错误:
class Wrapper { public static void main(String args[]) throws Exception { Map map = System.getProperties(); Random r = new Random(); while (true) { map.put(r.nextInt(), "value"); } } } JAVA |
正如你可能猜到的那样,这是不可能结束的. 事实上, 当我们使用以下语句运行:
java -Xmx100m -XX:+UseParallelGC Wrapper
我们不久就会看到 java.lang.OutOfMemoryError: GC overhead limit exceeded
信息. 但是上边的例子很棘手. 当选择不同的 Java heap size 或者不同的 GC 策略, 在 Mac OS X 10.9.2, Hotspot 1.7.0_45 上死法各不相同. 例如, 当我用更小的 Java heap size 运行:
java -Xmx10m -XX:+UseParallelGC Wrapper
该应用会抛出一个更常见的 java.lang.OutOfMemoryError: Java heap space
消息. 当我使用不同的 GC 策略运行它, 比如-XX:+UseConcMarkSweepGC
或 -XX:+UseG1GC
, 错误被默认的异常 handler 捕获, 并且在这种情况下,因为堆耗尽, 堆栈跟踪甚至不能在异常创建中被填充。
这些变化确实是很好的例子,说明在资源受限的情况下你不能预测你的应用程序将会以怎样的方式死亡,所以不要以你的期望为基础寄托在要完成的具体操作序列上。
2.4 解决办法
作为一个半开玩笑的解决方案, 如果你希望避免 java.lang.OutOfMemoryError: GC overhead limit exceeded
消息, 可以在启动脚本里增加下列信息:
-XX:-UseGCOverheadLimit
我 强烈建议不要使用这个参数 – 这只是饮鸩止渴: 最终应用会耗尽内存, 需要修复. 指定这个参数只是用一个更常见的消息java.lang.OutOfMemoryError: Java heap space
掩盖了 java.lang.OutOfMemoryError: GC overhead limit exceeded
错误.
另一种方式(临时的), 是给 JVM 进程更多的内存. 再次说明, 加这个很简单,但是最终仍会导致错误:
java.lang.OutOfMemoryError: Java heap space
在上边例子中, 给 Java 进程 1GB 的 heap. 增加这个值会解决 GC 开销限制问题如果你的应用先是出现内存不足. 但是如果你希望确保你已经解决了潜在的问题而不是掩盖 java.lang.OutOfMemoryError: GC overhead limit exceeded
症状, 你不应该仅止于此. 对于这种情况, 联系我就是最好的方式(@ ̄ー ̄@). 当然你手头上也有不同的工具可供选择, 如: profilers 和内存 dump 分析工具. 但要准备好投入大量的时间, 而且要意识到这一点: 工具会对你的 Java 运行时造成了巨大的开销,因此它们不适合生产环境使用。
对于这种问题, 其实 Dynatrace 也会进行告警. 我这有一篇分析的案例供参考.