一、案例模拟
示例代码1
package com.atguigu.oom;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 案例3:测试 GC overhead limit exceeded
* @create 16:57
*/
public class OOMTest {
public static void main(String[] args) {
test1();
// test2();
}
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;
}
}
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;
}
}
}
JVM配置
-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/dumpExceeded.hprof
-XX:+PrintGCDateStamps -Xms10M -Xmx10M -Xloggc:log/gc-oomExceeded.log
报错信息
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7110K->7095K(7168K)] 9158K->9143K(9728K), [Metaspace: 3177K->3177K(1056768K)], 0.0479640 secs] [Times: user=0.23 sys=0.01, real=0.05 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7114K->7096K(7168K)] 9162K->9144K(9728K), [Metaspace: 3198K->3198K(1056768K)], 0.0408506 secs] [Times: user=0.22 sys=0.01, real=0.04 secs]
通过查看GC日志可以发现,系统在频繁性的做FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆dump文件来具体分析。
示例代码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;
}
}
JVM配置
-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/dumpHeap1.hprof
-XX:+PrintGCDateStamps -Xms10M -Xmx10M -Xloggc:log/gc-oomHeap1.log
二、代码解析
第一段代码:运行期间将内容放入常量池的典型案例
intern()方法
如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用;
如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用
第二段代码:不停的追加字符串str
你可能会疑惑,看似demo也没有差太多,为什么第二个没有报GC overhead limit exceeded呢?以上两个demo的区别在于:
Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出
GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。
报错信息:
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7110K->7095K(7168K)] 9158K->9143K(9728K), [Metaspace: 3177K->3177K(1056768K)], 0.0479640 secs] [Times: user=0.23 sys=0.01, real=0.05 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7114K->7096K(7168K)] 9162K->9144K(9728K), [Metaspace: 3198K->3198K(1056768K)], 0.0408506 secs] [Times: user=0.22 sys=0.01, real=0.04 secs]
通过查看GC日志可以发现,系统在频繁性的做FULL GC,但是却没有回收掉多少空间,那么引起的原因可能是因为内存不足,也可能是存在内存泄漏的情况,接下来我们要根据堆DUMP文件来具体分析。
三、分析及解决
第1步:定位问题代码块
jvisualvm分析
这里就定位到了具体的线程中具体出现问题的代码的位置,进而进行优化即可。
MAT分析
第2步:分析dump文件直方图
看到发生OOM是因为进行了死循环,不停的往 ArrayList 存放字符串常量,JDK1.8以后,字符串常量池移到了堆中存储,所以最终导致内存不足发生了OOM。
打开Histogram,可以看到,String类型的字符串占用了大概8M的空间,几乎把堆占满,但是还没有占满,所以这也符合Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常,本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出。
第3步:代码修改
根据业务来修改是否需要死循环。
原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出
解决方法:
1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2. 添加参数 -XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
3. dump内存,检查是否存在内存泄漏,如果没有,加大内存。