jstat显示的full GC次数与CMS周期的关系

本文详细解析了在Oracle/Sun JDK中使用CMS垃圾回收器时,如何理解与计算full GC的次数。通过深入分析HotSpot虚拟机内部实现,揭示了CMS GC过程中暂停次数与full GC计数器之间的关系。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用Oracle/Sun JDK来运行Java程序的时候,大家或许有用过jstat工具来观察GC的统计数据,例如上一篇日志里的
$ jstat -gcutil `pgrep -u admin java`
S0 S1 E O P YGC YGCT FGC FGCT GCT
37.21 0.00 99.81 12.87 76.82 1767 196.843 3085 2998.088 3194.931

以前写过一帖说明jstat工具显示的读数与jvmstat计数器之前的关系:[url=http://rednaxelafx.iteye.com/blog/790864]用Java获取full GC的次数?(2)[/url]
可以知道,FGC列表示的是full GC次数,对应的jvmstat计数器是[b]sun.gc.collector.1.invocations[/b]:
column {
header "^FGC^" /* Full Collections */
data sun.gc.collector.1.invocations
align right
width 5
scale raw
format "0"
}


但是这个计数器在CMS GC里到底是指什么的次数呢?

在JDK 6的HotSpot VM中,Oracle/Sun有官方支持的GC只有CMS比较特殊([color=red]Garbage-First在JDK6里还没正式支持,不算在内[/color]):其它几种GC的每个周期都是完全stop-the-world的;而CMS的每个并发GC周期则有两个stop-the-world阶段——[b]initial mark[/b]与[b]final re-mark[/b],其它阶段是与应用程序一起并发执行的。
[quote="Memory Management in the Java HotSpot™ Virtual Machine"][img]http://dl.iteye.com/upload/attachment/506740/620accea-7008-3f3e-b2a6-990746be821b.png[/img][/quote]
如果CMS并发GC过程中出现了concurrent mode failure的话那么接下来就会做一次mark-sweep-compact的full GC,这个是完全stop-the-world的。

正是这个特征,使得CMS的每个并发GC周期总共会更新full GC计数器两次,initial mark与final re-mark各一次;如果出现concurrent mode failure,则接下来的full GC自己算一次。
如果说大家关心“GC次数”主要关心的其实是应用暂停次数的话,这么做倒也合理。但要注意的是在CMS里“暂停次数”并不等同于“GC次数”,CMS并发GC的一个周期叫“一次GC”但暂停了两次。

只不过有些人在从其它GC改为用CMS的时候会对“full GC次数”的显著增加感到不满,觉得是不是应该想办法调优来让“full GC次数”降下来。这里有几点:

1、CMS GC的设计初衷就是以降低GC latency为目标。如果一个应用产生垃圾的速度非常高的话,原本清除那些垃圾需要的时间并不会消失,CMS只是把它从一个大暂停分散到了多个阶段上,其中部分是暂停的,部分是并发的。所以暂停的次数本来就应该会增加,而每次停顿的时间则应该比较短——这是设计取舍的倾向性导致的。

2、为了更有效的实现并发,CMS GC进行的过程中必须保证堆里还有足够剩余空间来留给应用去分配对象,所以比起ParallelScavenge等别的实现CMS必须要提早一些触发并发GC的启动。如果从ParallelScavange迁移到CMS的时候不同事增大GC堆的大小,那么可以看到同样的应用在GC堆的占用率更低的时候就会触发GC了,所以GC次数增加了。

3、CMS GC中,“full GC次数”的计数器在每个并发GC周期里是增加2而不是增加1的。这也就是这篇日志最想说明的点:这个计数器说明了GC造成的应用暂停的次数,但并不代表CMS的并发GC周期的个数。由于full GC的计数器也会在完全stop-the-world的full GC中增加1,所以这个计数器也不准确代表并发GC周期个数的正好两倍。

4、一个CMS并发GC周期的触发原因只有一个;其中的两次暂停都是同一个原因引致的,例如说最初CMS old gen或者perm gen的使用率已经超过了某个阈值之类。

======================================================================

实现细节感兴趣的同学们,看代码~

CMSCollector里有_gc_counters用于记录jvmstat(或者说PerfData)需要的统计数据。这是个CollectorCounters类型的对象,里面有_invocations成员是用来记录GC次数的。
CollectorCounters::CollectorCounters(const char* name, int ordinal) {

if (UsePerfData) {
EXCEPTION_MARK;
ResourceMark rm;

const char* cns = PerfDataManager::name_space("collector", ordinal);

_name_space = NEW_C_HEAP_ARRAY(char, strlen(cns)+1);
strcpy(_name_space, cns);

// ...

cname = PerfDataManager::counter_name(_name_space, "invocations");
_invocations = PerfDataManager::create_counter(SUN_GC, cname,
PerfData::U_Events, CHECK);

// ...
}
}

TraceCollectorStats用于辅助记录GC的次数。它在构造器里会将传入的CollectorCounters的invocation_counter()计数器自增1(自增1的具体逻辑在其基类的PerfTraceTimedEvent的构造器里)。
class TraceCollectorStats: public PerfTraceTimedEvent {

protected:
CollectorCounters* _c;

public:
inline TraceCollectorStats(CollectorCounters* c) :
PerfTraceTimedEvent(c->time_counter(), c->invocation_counter()),
_c(c) {

if (UsePerfData) {
_c->last_entry_counter()->set_value(os::elapsed_counter());
}
}

inline ~TraceCollectorStats() {
if (UsePerfData) _c->last_exit_counter()->set_value(os::elapsed_counter());
}
};

class PerfTraceTimedEvent : public PerfTraceTime {

protected:
PerfLongCounter* _eventp;

public:
inline PerfTraceTimedEvent(PerfLongCounter* timerp, PerfLongCounter* eventp): PerfTraceTime(timerp), _eventp(eventp) {
if (!UsePerfData) return;
_eventp->inc();
}

inline PerfTraceTimedEvent(PerfLongCounter* timerp, PerfLongCounter* eventp, int* recursion_counter): PerfTraceTime(timerp, recursion_counter), _eventp(eventp) {
if (!UsePerfData) return;
_eventp->inc();
}
};

计数器增加1就是这里的[color=brown]_eventp->inc();[/color]

HotSpot里每个stop-the-world行为都用一个VM_Operation包装起来。与CMS相关的两个VM_Operation就是[b]VM_CMS_Initial_Mark[/b]与[b]VM_CMS_Final_Mark[/b]。
// The VM_CMS_Operation is slightly different from
// a VM_GC_Operation -- and would not have subclassed easily
// to VM_GC_Operation without several changes to VM_GC_Operation.
// To minimize the changes, we have replicated some of the VM_GC_Operation
// functionality here. We will consolidate that back by doing subclassing
// as appropriate in Dolphin.
//
// VM_Operation
// VM_CMS_Operation
// - implements the common portion of work done in support
// of CMS' stop-world phases (initial mark and remark).
//
// VM_CMS_Initial_Mark
// VM_CMS_Final_Mark
//

这两个VM_Operation的核心部分都调用了下面这个函数:
void CMSCollector::do_CMS_operation(CMS_op_type op) {
gclog_or_tty->date_stamp(PrintGC && PrintGCDateStamps);
TraceCPUTime tcpu(PrintGCDetails, true, gclog_or_tty);
TraceTime t("GC", PrintGC, !PrintGCDetails, gclog_or_tty);
TraceCollectorStats tcs(counters());

switch (op) {
case CMS_op_checkpointRootsInitial: {
SvcGCMarker sgcm(SvcGCMarker::OTHER);
checkpointRootsInitial(true); // asynch
if (PrintGC) {
_cmsGen->printOccupancy("initial-mark");
}
break;
}
case CMS_op_checkpointRootsFinal: {
SvcGCMarker sgcm(SvcGCMarker::OTHER);
checkpointRootsFinal(true, // asynch
false, // !clear_all_soft_refs
false); // !init_mark_was_synchronous
if (PrintGC) {
_cmsGen->printOccupancy("remark");
}
break;
}
default:
fatal("No such CMS_op");
}
}

留意到其中的[color=brown]TraceCollectorStats tcs(counters());[/color]了么?这就让full GC的计数器增加了1。
也就是说CMS GC的两个暂停阶段各自会让full GC计数器增加1,于是整个CMS并发GC周期里该计数器就会增加2了。

======================================================================

追加:有人提醒我有这么一篇文章:
[url=https://community.dynatrace.com/community/display/PUB/JDK6+Update+23+changes+CMS+Collection+counters]JDK6 Update 23 changes CMS Collection counters[/url]
所以顺便一提,我这篇blog用的是JDK 6 update 25对应的HotSpot 20来讲的。
如果大家关注JMX读出来CMS collections次数,请留意一下上面链接的文章。
<think>我们正在讨论jstat命令的两个常用选项:-gc和-gcutil。用户希望了解这两个命令输出参数的具体含义。根据引用[1]和引用[4],我们可以找到一些基础信息:-jstat-gc:监控GC的相关统计信息(如各代内存使用情况)。-jstat-gcutil:监控GC使用情况百分比(如Eden、Old、Meta的使用率)。但是,用户需要的是这两个命令输出的每个参数的具体含义。首先,我们需要明确两个命令的输出列分别代表什么。一、jstat-gc的输出列(根据引用[3]和引用[4]以及标准文档):它输出的是各个内存区域的实际容量(单位KB),以及GC发生的次数和时间。通常,jstat-gc的输出包含以下列(注意:不同的JVM版本可能略有不同,但核心列是相似的):S0C:当前Survivor0区容量(kB)S1C:当前Survivor1区容量(kB)S0U:Survivor0区已使用(kB)S1U:Survivor1区已使用(kB)EC:当前Eden区容量(kB)EU:Eden区已使用(kB)OC:当前Old区容量(kB)OU:Old区已使用(kB)MC:Metaspace(元空间)容量(kB)MU:Metaspace已使用(kB)CCSC:压缩类空间容量(kB)CCSU:压缩类空间已使用(kB)YGC:年轻代GC次数YGCT:年轻代GC总耗时(秒)FGC:FullGC次数FGCT:FullGC总耗时(秒)CGC:并发GC次数(有些版本可能没有)CGCT:并发GC总耗时(秒)GCT:所有GC总耗时(YGCT+FGCT+CGCT)二、jstat-gcutil的输出列:它输出的是各个内存区域的使用率百分比。通常包含以下列:S0:Survivor0区使用率(百分比)S1:Survivor1区使用率(百分比)E:Eden区使用率(百分比)O:Old区使用率(百分比)M:Metaspace使用率(百分比)CCS:压缩类空间使用率(百分比)YGC:年轻代GC次数YGCT:年轻代GC总耗时(秒)FGC:FullGC次数FGCT:FullGC总耗时(秒)CGC:并发GC次数(有些版本可能没有)CGCT:并发GC总耗时(秒)GCT:所有GC总耗时(秒)注意:在gcutil中,每个内存区域的使用率计算公式为:(已使用容量/当前容量)*100%另外,引用[2]提到,在故障分析中,gcutil是一个常用的工具,可以快速查看各个区域的使用率,从而判断内存使用是否健康。下面我们用一个具体的例子来说明(假设输出一行,数字为示例):jstat-gcutil1234510001输出:S0S1EOMCCSYGCYGCTFGCFGCTCGCCGCTGCT0.0025.0080.0065.0097.5090.001002.50050.80030.2003.500解释:S0:0.00%表示Survivor0区没有使用S1:25.00%表示Survivor1区使用了25%E:80.00%表示Eden区使用了80%O:65.00%表示老年代使用了65%M:97.50%表示元空间使用了97.5%,需要关注是否可能溢出CCS:90.00%表示压缩类空间使用了90%YGC:100次年轻代GCYGCT:年轻代GC总耗时2.5秒FGC:5次FullGCFGCT:FullGC总耗时0.8秒CGC:3次并发GC(如CMS的并发阶段)CGCT:并发GC总耗时0.2秒GCT:总GC时间3.5秒而同样的进程,用jstat-gc可能输出(单位为KB):S0CS1CS0US1UECEUOCOUMCMUCCSCCCSUYGCYGCTFGCFGCTCGCCGCTGCT1024.01024.00.0256.08192.06553.610240.06656.04480.04368.0384.0345.61002.50050.80030.2003.500这里每个容量和使用量都是具体的数值(KB)。因此,总结:-jstat-gc提供的是绝对值(容量和使用量),单位是KB。-jstat-gcutil提供的是相对值(使用百分比)。在实际问题排查中,gcutil可以快速看出哪些区域使用率过高,而gc则可以具体看出内存分配的大小和实际使用量,两者结合可以更全面地分析内存使用情况。注意:不同的垃圾收集器(如G1)和不同的JDK版本,输出可能会有所不同。例如,在G1收集器中,可能没有S0C、S1C等,而是替换为G1相关的区域。以上内容参考了引用[1]、[2]、[3]、[4]。</think>### jstat -gcjstat -gcutil 参数详解 #### 一、`jstat -gc` 输出参数(显示各内存区域实际容量) 输出单位为 KB,提供内存区域的**绝对数值**: ``` S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT ``` - **新生代相关** - `S0C`:Survivor0 区当前容量 - `S1C`:Survivor1 区当前容量 - `S0U`:Survivor0 区已使用量 - `S1U`:Survivor1 区已使用量 - `EC`:Eden 区当前容量 - `EU`:Eden 区已使用量 - **老年代相关** - `OC`:Old 区当前容量 - `OU`:Old 区已使用量 - **元空间相关** - `MC`:Metaspace 容量 - `MU`:Metaspace 已使用量 - `CCSC`:压缩类空间容量 - `CCSU`:压缩类空间已使用量 - **GC 统计** - `YGC`:Young GC 发生次数 - `YGCT`:Young GC 总耗时(秒) - `FGC`:Full GC 发生次数 - `FGCT`:Full GC 总耗时(秒) - `CGC`:并发 GC 次数(如 CMS/G1) - `CGCT`:并发 GC 总耗时(秒) - `GCT`:所有 GC 总耗时(YGCT + FGCT + CGCT)[^1][^3] #### 二、`jstat -gcutil` 输出参数(显示使用百分比) 输出为**百分比**,聚焦内存使用率: ``` S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT ``` - **内存使用率** - `S0`:Survivor0 区使用率 - `S1`:Survivor1 区使用率 - `E`:Eden 区使用率 - `O`:Old 区使用率 - `M`:Metaspace 使用率 - `CCS`:压缩类空间使用率 - **GC 统计**( `-gc` 相同) - `YGC`/`YGCT`:Young GC 次数耗时 - `FGC`/`FGCT`:Full GC 次数耗时 - `GCT`:GC 总耗时[^2][^4] #### 三、核心区别 | **特性** | `jstat -gc` | `jstat -gcutil` | |----------------|----------------------------|-------------------------| | **数据维度** | 绝对容量(KB) | 使用率百分比(%) | | **适用场景** | 分析内存分配大小 | 快速识别内存瓶颈区域 | | **关键指标** | `EU`(Eden 使用量) | `O`(Old 区使用率) | | **故障排查** | 结合容量判断是否需扩容 | 识别 >95% 的溢出风险区 | #### 四、使用示例 1. **监控内存趋势** ```bash jstat -gcutil <pid> 1000 5 # 每秒采样1次,共5次 ``` 输出示例: ``` S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 25.00 80.00 65.00 97.50 90.00 100 2.500 5 0.800 3.500 ``` **解读**: - 元空间(`M=97.5%`)接近溢出,需检查类加载 - 老年代(`O=65%`)使用正常 - 发生 5 次 Full GC(`FGC=5`)[^2][^4] 2. **分析内存分配** ```bash jstat -gc <pid> ``` 输出示例: ``` EC EU OC OU MC 8192.0 6553.6 10240.0 6656.0 4480.0 ``` **解读**: - Eden 总容量 8MB(`EC=8192KB`),已用 6.4MB(`EU=6553.6KB`) - Old 区总容量 10MB(`OC=10240KB`)[^3][^5] ### 关键应用场景 - **`-gcutil`**:快速定位内存瓶颈(如 Old 区持续 >90% 可能需优化对象生命周期)[^2] - **`-gc`**:调整 JVM 参数前验证各代容量(如 `OC` 是否过小导致频繁 Full GC)[^5]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值