通常,在基于Java生态体系中的应用程序抛出异常时,生产环境都会通过gc log[当然,也有2愣子直接去线上环境进行各种骚操作]去捕获各种可疑线索,以便快速、高效定位及解决问题。
本文主要基于 Hotspot VM 中“CMS”垃圾回收策略的一些实际场景进行汇总,[涉及的基础概念暂不在本章赘述]简要通过部分源码对引起GC现象的根本原因进行分析以及对排查方法进行总结。另外,本文专业术语较多,有一定的阅读门槛,如对JVM体系所涉及的内存分配及垃圾回收没有理论支撑以及实战经验,还请去官网查阅相关材料。
在我们测试环境或者预发布环境,通常通过如下命令查看某一特定Java应用程序的GC详细情况:
代码语言:javascript复制[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
代码语言:javascript复制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的相关源码以及产生的相关因素进行剖析,在应用程序出现内存问题时才能游刃有余去处理。