zl程序教程

您现在的位置是:首页 >  其他

当前栏目

GC Cause解析

2023-04-18 15:00:37 时间

通常,在基于Java生态体系中的应用程序抛出异常时,生产环境都会通过gc log[当然,也有2愣子直接去线上环境进行各种骚操作]去捕获各种可疑线索,以便快速、高效定位及解决问题。

本文主要基于 Hotspot VM 中“CMS”垃圾回收策略的一些实际场景进行汇总,[涉及的基础概念暂不在本章赘述]简要通过部分源码对引起GC现象的根本原因进行分析以及对排查方法进行总结。另外,本文专业术语较多,有一定的阅读门槛,如对JVM体系所涉及的内存分配及垃圾回收没有理论支撑以及实战经验,还请去官网查阅相关材料。

在我们测试环境或者预发布环境,通常通过如下命令查看某一特定Java应用程序的GC详细情况:

[administrator@JavaLangOutOfMemory luga % ]jstat -gccause pid xxxxx

以确认上次GC的原因和当前GC的原因。

GC Cause,顾名思义,就是引起发生垃圾回收的因素。只有了解是什么原因引起的 GC,以及每次的时间花费情况,才能有效去定位、分析问题所在。但是要具体分析 GC 的问题,首先要读懂 GC Cause,即 JVM在何种场景下选择进行 GC 操作,具体 GC Cause 的分类可参考Hotspot 源码:

src/share/vm/gc/shared/gcCause.hpp

src/share/vm/gc/shared/gcCause.cpp

const char* GCCause::to_string(GCCause::Cause cause) {
  switch (cause) {
    case _java_lang_system_gc:
      return "System.gc()";

    case _full_gc_alot:
      return "FullGCAlot";

    case _scavenge_alot:
      return "ScavengeAlot";

    case _allocation_profiler:
      return "Allocation Profiler";

    case _jvmti_force_gc:
      return "JvmtiEnv ForceGarbageCollection";

    case _gc_locker:
      return "GCLocker Initiated GC";

    case _heap_inspection:
      return "Heap Inspection Initiated GC";

    case _heap_dump:
      return "Heap Dump Initiated GC";

    case _wb_young_gc:
      return "WhiteBox Initiated Young GC";

    case _wb_conc_mark:
      return "WhiteBox Initiated Concurrent Mark";

    case _wb_full_gc:
      return "WhiteBox Initiated Full GC";

    case _no_gc:
      return "No GC";

    case _allocation_failure:
      return "Allocation Failure";

    case _tenured_generation_full:
      return "Tenured Generation Full";

    case _metadata_GC_threshold:
      return "Metadata GC Threshold";

    case _metadata_GC_clear_soft_refs:
      return "Metadata GC Clear Soft References";

    case _cms_generation_full:
      return "CMS Generation Full";

    case _cms_initial_mark:
      return "CMS Initial Mark";

    case _cms_final_remark:
      return "CMS Final Remark";

    case _cms_concurrent_mark:
      return "CMS Concurrent Mark";

    case _old_generation_expanded_on_last_scavenge:
      return "Old Generation Expanded On Last Scavenge";

    case _old_generation_too_full_to_scavenge:
      return "Old Generation Too Full To Scavenge";

    case _adaptive_size_policy:
      return "Ergonomics";

    case _g1_inc_collection_pause:
      return "G1 Evacuation Pause";

    case _g1_humongous_allocation:
      return "G1 Humongous Allocation";

    case _dcmd_gc_run:
      return "Diagnostic Command";

    case _last_gc_cause:
      return "ILLEGAL VALUE - last gc cause - ILLEGAL VALUE";

    default:
      return "unknown GCCause";
  }
  ShouldNotReachHere();
}

结合源码,我们可以看到,在实际的项目中,针对GC此处产生问题的分析重点需要关注的以下几个GC Cause:

1、System.gc():即,显性手动触发GC操作

2、CMS:CMS GC 在执行过程中的一些动作,重点需要关注 CMS Initial Mark 和 CMS Final Remark 两个 STW 阶段

3、Promotion Failure:Old 区没有足够的空间分配给 Young 区晋升的对象(即使总可用内存足够大)

4、Concurrent Mode Failure:CMS GC 运行期间,Old 区所预留的空间不足以分配给新创建的对象,此时收集器会发生退化,甚至严重影响 GC 性能

5、GCLocker Initiated GC:如果线程执行在 JNI 临界区操作时,刚好需要进行 GC操作,此时 GC Locker 将会阻止 GC 操作的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC

在一次实际的业务场景处理的过程中,如何判断是 GC 操作导致的故障,还是应用系统本身引发 GC 问题?这里主要结合相关数据信息(例如:监控数据、GC Log日志文件、资源使用情况以及可获得的HeapDump/ThreadDump及CoreDump等相关转储文件)进行合理分析。围绕“GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高“等核心要素,准确定位、判断到底哪个是罪魁祸首。

毕竟,不同的根因,后续的分析方法不尽相同。如果是 CPU 负载高,那可能需要用火焰图或者借助Nmon工具结合应用程序看下相关热点;如果是慢查询增多那可能需要观察下 DB 资源情况;如果是线程 Block 引起那可能需要判断是否存在锁竞争的情况;反之,如果各个核心要素证明都没有问题,那么罪魁祸首可能存在于GC这块,So,我们就需要以GC为切入点继续分析 GC 问题,直到将其Fix掉为止。

综上所述,只有通过对GC Cause的相关源码以及产生的相关因素进行剖析,在应用程序出现内存问题时才能游刃有余去处理。