GC overhead limit exceeded
# GC overhead limit exceeded
# 报错信息
java.lang.OutOfMemoryError: GC overhead limit exceeded
- 这个是JDK6新加的错误类型,一般都是堆太小导致的。 Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。 本质是一个预判性的异常 ,抛出该异常时系统没有真正的内存溢出
# 案例模拟
示例代码1
public static void test1() {
int i = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(UUID.randomUUID().toString().intern());
i++;
}
} catch (Throwable e) {
System.out.println("************i: " + i);
e.printStackTrace();
throw e;
}
}
示例代码1JVM参数配置:
-Xms10M
-Xmx10M
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/dumpExceeded.hprof
-XX:+PrintGCDateStamps
-Xloggc:log/gc-oomExceeded.log
示例代码1运行结果:
java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at java.util.UUID.toString(UUID.java:380)
......
示例代码2
public static void test2() {
String str = "";
Integer i = 1;
try {
while (true) {
i++;
str += UUID.randomUUID();
}
} catch (Throwable e) {
System.out.println("************i: " + i);
e.printStackTrace();
throw e;
}
}
示例代码2JVM参数配置:
-Xms10M
-Xmx10M
-XX:+PrintGCDetails
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=heap/dumpHeap1.hprof
-XX:+PrintGCDateStamps
-Xloggc:log/gc-oomHeap1.log
示例代码2运行结果:
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at java.lang.StringBuilder.append(StringBuilder.java:131)
······
# 代码解析
代码1:运行期间将内容放入常量池的典型案例。
intern()方法 :
- 如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用;
- 如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用
代码2:不停的追加字符串str 。
你可能会疑惑,看似demo也没有差太多,为什么第二个没有报GC overhead limit exceeded
呢?以上两个demo的区别在于:
Java heap space
的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出- 而
GC overhead limit exceeded
的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。
# 分析与解决
示例1日志解析:
2022-03-16T21:21:51.670-0800: 1.534: [Full GC (Ergonomics) [PSYoungGen: 2048K->2043K(2560K)] [ParOldGen: 7139K->7139K(7168K)] 9187K->9183K(9728K), [Metaspace: 3672K->3672K(1056768K)], 0.0227713 secs] [Times: user=0.11 sys=0.01, real=0.02 secs]
2022-03-16T21:21:51.693-0800: 1.557: [Full GC (Ergonomics) [PSYoungGen: 2048K->2044K(2560K)] [ParOldGen: 7139K->7139K(7168K)] 9187K->9183K(9728K), [Metaspace: 3672K->3672K(1056768K)], 0.0191276 secs] [Times: user=0.12 sys=0.00, real=0.02 secs]
2022-03-16T21:21:51.713-0800: 1.576: [Full GC (Ergonomics) [PSYoungGen: 2048K->0K(2560K)] [ParOldGen: 7139K->527K(7168K)] 9187K->527K(9728K), [Metaspace: 3676K->3676K(1056768K)], 0.0061429 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
通过查看GC日志可以发现,系统在频繁性的做FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆dump文件来具体分析。
VisualVM分析dump,定位代码块:
MAT分析dump,定位代码块:
解决方法:
- 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
- 添加参数
-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。 - dump内存,检查是否存在内存泄漏,如果没有,加大内存。
上次更新: 5/28/2023, 10:57:53 PM