通常,在基于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的相关源码以及产生的相关因素进行剖析,在应用程序出现内存问题时才能游刃有余去处理。