一、案例模拟

示例代码1

  1. package com.atguigu.oom;
  2. import org.junit.Test;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import java.util.UUID;
  6. /**
  7. * 案例3:测试 GC overhead limit exceeded
  8. * @create 16:57
  9. */
  10. public class OOMTest {
  11. public static void main(String[] args) {
  12. test1();
  13. // test2();
  14. }
  15. public static void test1() {
  16. int i = 0;
  17. List<String> list = new ArrayList<>();
  18. try {
  19. while (true) {
  20. list.add(UUID.randomUUID().toString().intern());
  21. i++;
  22. }
  23. } catch (Throwable e) {
  24. System.out.println("************i: " + i);
  25. e.printStackTrace();
  26. throw e;
  27. }
  28. }
  29. public static void test2() {
  30. String str = "";
  31. Integer i = 1;
  32. try {
  33. while (true) {
  34. i++;
  35. str += UUID.randomUUID();
  36. }
  37. } catch (Throwable e) {
  38. System.out.println("************i: " + i);
  39. e.printStackTrace();
  40. throw e;
  41. }
  42. }
  43. }

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

  1. public static void test2() {
  2. String str = "";
  3. Integer i = 1;
  4. try {
  5. while (true) {
  6. i++;
  7. str += UUID.randomUUID();
  8. }
  9. } catch (Throwable e) {
  10. System.out.println("************i: " + i);
  11. e.printStackTrace();
  12. throw e;
  13. }
  14. }

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分析

image.png
image.png
这里就定位到了具体的线程中具体出现问题的代码的位置,进而进行优化即可。

MAT分析

image.png
image.png
image.png
通过线程分析如下图所示,可以定位到发生OOM的代码块
image.png
image.png

第2步:分析dump文件直方图

看到发生OOM是因为进行了死循环,不停的往 ArrayList 存放字符串常量,JDK1.8以后,字符串常量池移到了堆中存储,所以最终导致内存不足发生了OOM。

打开Histogram,可以看到,String类型的字符串占用了大概8M的空间,几乎把堆占满,但是还没有占满,所以这也符合Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常,本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出。
image.png
image.png

第3步:代码修改

根据业务来修改是否需要死循环。

原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出

解决方法:

1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
2. 添加参数 -XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现 java.lang.OutOfMemoryError: Java heap space。
3. dump内存,检查是否存在内存泄漏,如果没有,加大内存。