JVM源码剖析之Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded异常

写在前面:

版本信息:

jdk版本:jdk8u40
垃圾回收器:ParallelScavenge new/old

最近在群里看到有一位老哥拿着异常信息到处问,而发生的就是java.lang.OutOfMemoryError: GC overhead limit exceeded异常,恰好看到经常有人询问关于这个异常的问题,如何发生的,要如何解决呢?所以促使我写下这篇文章,此文章分为3大块,出现的原因,如何解决,源码论证。

异常出现的原因

full gc回收时间大于98%(这里是一个算法,可以忽略,只需要明白最近一直花大量时间在GC),并且回收后可用空间小于总空间的2%。 这样的情况下,达到5次就会抛出java.lang.OutOfMemoryError: GC overhead limit exceeded异常。

总而言之:我们首先要明白,GC的过程是需要STW(STOP-THE-WORD) 也即业务线程是需要停止工作,而GC过程中消耗大量时间回收空间,而回收后的可使用空间仅仅只有总空间的2%,往往下次new对象的时候又去GC了,周而复始,给用户的体验是当前系统已经完全卡死了~

所以在种种因素下JVM认为你已经没必要去GC了,GC也是毫无意义的事情了,完全卡死的情况下,还不如我给你抛出java.lang.OutOfMemoryError: GC overhead limit exceeded异常,开发者好好去排查一下问题~

如何解决

仅供参考,还是需要分析自身系统环境做出不同的策略。

  1. 堆空间是否设置的太少?可以在启动时添加-Xms -Xmx参数设置堆大小
  2. 分析是否存在内存泄露?
  3. 如果项目庞大,是否需要提升硬件?
  4. 启动时添加-XX:+HeapDump0n0ut0fMemoryError -XX:HeapDumpPath= "路径" 参数下次发生OOM时便可分析
  5. 分析项目中经常使用的大对象,是否可以优化一下空间?
  6. 是否可以把项目中非重要的缓存数据设置成软、弱引用对象
  7. 以上的分析可以使用阿里开源的 "Arthas" 工具
  8. 还有很多笔者暂时没有考虑到的.......

源码论证

由于大部分的公司还是停留在Java8,并且垃圾回收器也是默认的ParallelScavenge new/old,所以直接给出ParallelScavenge new/old垃圾回收器的部分核心源码

src/share/vm/gc_interface/collectedHeap.inline.cpp 文件中, common_mem_allocate_noinit方法,此方法尝试开辟对象,如果开辟不成功,就会根据当前不同OOM异常种类Dump和抛出对应的异常

cpp 复制代码
HeapWord* CollectedHeap::common_mem_allocate_noinit(KlassHandle klass, size_t size, TRAPS) {

  bool gc_overhead_limit_was_exceeded = false;

  // 尝试在堆空间开辟对象
  result = Universe::heap()->mem_allocate(size,
                                          &gc_overhead_limit_was_exceeded);

  // 根据gc_overhead_limit_was_exceeded参数区分是那种OOM异常。
  if (!gc_overhead_limit_was_exceeded) {
    
    // -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support
    // 英文注释特别明显了,如果设置了dump参数,就导出
    report_java_out_of_memory("Java heap space");

    // 抛出OOM:Java heap space
    THROW_OOP_0(Universe::out_of_memory_error_java_heap());
  } else {      
    // -XX:+HeapDumpOnOutOfMemoryError and -XX:OnOutOfMemoryError support
    // 英文注释特别明显了,如果设置了dump参数,就导出
    report_java_out_of_memory("GC overhead limit exceeded");

    // 抛出OOM:GC overhead limit exceeded
    THROW_OOP_0(Universe::out_of_memory_error_gc_overhead_limit());
  }
}

而我们关心的是GC overhead limit exceeded异常,而这里是根据gc_overhead_limit_was_exceeded变量来做区分,而gc_overhead_limit_was_exceeded变量传入mem_allocate方法,所以我们接着看mem_allocate方法src/share/vm/gc_implementation/parallelScavenge/parallelScavengeHeap.cpp 文件中mem_allocate方法,此方法也是开辟对象的过程。

cpp 复制代码
HeapWord* ParallelScavengeHeap::mem_allocate(
                                     size_t size,
                                     bool* gc_overhead_limit_was_exceeded) {

  // 尝试在young_gen空间创建对象
  HeapWord* result = young_gen()->allocate(size);
  
  // 在年轻代没有创建出对象。
  while (result == NULL) {
    {
      // 因为存在锁的原因,所以下面又在年轻代尝试了一次。
      MutexLocker ml(Heap_lock);
      gc_count = Universe::heap()->total_collections();
      result = young_gen()->allocate(size);
      if (result != NULL) {
        return result;
      }

      // 年轻代开辟不了,老年代尝试一下。
      result = mem_allocate_old_gen(size);
      if (result != NULL) {
        return result;
      }

      // 超过允许尝试的次数,直接返回
      if (gclocker_stalled_count > GCLockerRetryAllocationCount) {
        return NULL;
      }
    }

    // 需要触发GC,回收空间后再尝试开辟对象
    if (result == NULL) {
      // 触发GC回收空间
      VM_ParallelGCFailedAllocation op(size, gc_count);
      VMThread::execute(&op);

      if (op.prologue_succeeded()) {

        // 是否达到次数,是否清空软引用了
        const bool limit_exceeded = size_policy()->gc_overhead_limit_exceeded();
        const bool softrefs_clear = collector_policy()->all_soft_refs_clear();

        if (limit_exceeded && softrefs_clear) {
          // 设置为true,代表抛出GC overhead limit exceeded异常
          *gc_overhead_limit_was_exceeded = true;   
          size_policy()->set_gc_overhead_limit_exceeded(false);
          return NULL;
        }

        return op.result();
      }
    }
  }

  return result;
}

我们看到后续GC回收后,判断limit_exceeded 和 softrefs_clear,如果都为true就把gc_overhead_limit_was_exceeded设置为true。

而softrefs_clear变量是清空软引用,我们知道,在JVM中,内存实在不足的时候会清空软引用

而我们看到limit_exceeded变量的设置即可。看何时把他设置为true即可。

src/share/vm/gc_implementation/share/adaptiveSizePolicy.cpp 文件中check_gc_overhead_limit方法

cpp 复制代码
void AdaptiveSizePolicy::check_gc_overhead_limit(
                                          size_t young_live,
                                          size_t eden_live,
                                          size_t max_old_gen_size,
                                          size_t max_eden_size,
                                          bool   is_full_gc,
                                          GCCause::Cause gc_cause,
                                          CollectorPolicy* collector_policy) {


  // 当前eden空闲大小
  const size_t free_in_eden = max_eden_size > live_in_eden ?
    max_eden_size - live_in_eden : 0;
  // 当前老年代空闲大小
  const size_t free_in_old_gen = (size_t)(max_old_gen_size - avg_old_live()->average());
  // 当前堆空间空闲大小
  const size_t total_free_limit = free_in_old_gen + free_in_eden;
  // 堆空间的总大小
  const size_t total_mem = max_old_gen_size + max_eden_size;

  // GCHeapFreeLimit是2
  // 所以这里是算出比例,2%
  const double mem_free_limit = total_mem * (GCHeapFreeLimit/100.0);
  const double mem_free_old_limit = max_old_gen_size * (GCHeapFreeLimit/100.0);
  const double mem_free_eden_limit = max_eden_size * (GCHeapFreeLimit/100.0);

  // GCTimeLimit是98
  // 所以这里是算出比例,98%
  const double gc_cost_limit = GCTimeLimit/100.0;

  // 如果是fullgc
  if (is_full_gc) {
    // 如果GC时长超过98%
    // 并且回收后可用空间小于总空间的2%
    if (gc_cost() > gc_cost_limit &&
      free_in_old_gen < (size_t) mem_free_old_limit &&
      free_in_eden < (size_t) mem_free_eden_limit) {

      inc_gc_overhead_limit_count();    // 自增一次

      // 如果开启了次数限制
      if (UseGCOverheadLimit) {
        // 如果次数大于等于5次。
        if (gc_overhead_limit_count() >=
            AdaptiveSizePolicyGCTimeLimitThreshold){
          // 设置为true,到时候就会抛出GC overhead limit exceeded异常
          // 并且清空次数
          set_gc_overhead_limit_exceeded(true);   
          reset_gc_overhead_limit_count();
        } 
      }

    }
  }

  // 如果设置了UseGCOverheadLimit的情况下, 不会响应此异常
  if (UseGCOverheadLimit && PrintGCDetails && Verbose) {
    if (gc_overhead_limit_exceeded()) {
      reset_gc_overhead_limit_count();
    } 
  }
}

这里算出了堆总空间的百分之2,gc回收时间的百分之98。然后算出了GC后空闲空间的占比,GC的回收时间。最后通过比较,如果GC回收时间大于98%,并且回收后可用空间小于总空间的2% 情况下计数器+1,如果计数器达到5次就通过set_gc_overhead_limit_exceeded方法设置为true,最终抛出java.lang.OutOfMemoryError: GC overhead limit exceeded异常。

所以看了源码后,解决GC overhead limit exceeded异常,还可以通过设置

-XX:-UseGCOverheadLimit 或者 -XX:AdaptiveSizePolicyGCTimeLimitThreshold = "设置很大的数值" 都能让JVM不抛出 GC overhead limit exceeded异常。但是没任何意义,因为会一直触发GC,一直STW暂停,用户一直是卡死的状态~

相关推荐
loyd314 分钟前
【数据分析】5 设计不同业务分析框架
java·网络·数据分析
m0_7482451720 分钟前
Spring Boot项目开发常见问题及解决方案(上)
java·spring boot·后端
今天的接口写完了吗?21 分钟前
Spring Boot操作MaxComputer(保姆级教程)
java·spring boot·后端
金州小铁匠34 分钟前
基于EasyExcel封装的Excel工具类,支持高效导出和读取操作
java·spring·excel
IIIIIIlllii37 分钟前
java练习(43)
java·开发语言
xxxxxmy1 小时前
Spring MVC 程序开发(1)
java·spring·mvc
不平衡的叉叉树1 小时前
使用优化版的编辑距离算法替代ES默认的评分算法
java·算法
没什么技术1 小时前
Spock框架:让单元测试更优雅的高效武器
java·spock
码代码的小仙女1 小时前
学习笔记-07生产者-消费者模型4种实现方式
java·学习
m0_748240441 小时前
Rust 错误处理(下)
java·算法·rust