
本文详解为何 `managementfactory.getgarbagecollectormxbeans()` 在 cms 垃圾收集器下会将一次 `jmap -histo:live` 触发的 full gc 误报为多次,揭示 cms 的 foreground 模式中 initial mark、remark 和 sweep 阶段均独立增加 `collectioncount` 的机制,并提供健壮、跨 gc 算法的监控方案。
在使用 ManagementFactory.getGarbageCollectorMXBeans() 监控 Full GC 次数时,你观察到:执行一次 jmap -histo:live(触发 Heap Inspection Initiated GC)后,日志显示仅发生一次 CMS Full GC,但程序却报告 add FullGC count:2 —— 这并非代码逻辑错误,而是 CMS 垃圾收集器在 foreground 模式下的固有行为。
? 根本原因:CMS 的 “伪 Full GC” 阶段拆分
当 jmap -histo:live 触发 GC 时,JVM 会强制进入 CMS 的 foreground 模式(即暂停所有应用线程的同步回收),该模式并非单次原子操作,而是由多个可独立计数的子阶段组成:
- Initial Mark(初始标记)→ collectionCount++
- Remark(重新标记)→ collectionCount++
- Sweep(清除)→ collectionCount++
查看你的 GC 日志可验证这一点:
# 第一次 jmap:包含 Remark + Sweep(共 2 次计数增量) [Full GC (Heap Inspection Initiated GC) ... [CMS: ...] ... [weak refs processing] ... [class unloading] ... [scrub symbol/string table] ... ] → 实际触发了 Remark(含 class unloading/scrub)和 Sweep 两个独立阶段 # 第二次 jmap:仅 Sweep(1 次增量) [Full GC (Heap Inspection Initiated GC) ... [CMS: 83931K->85173K(...) ...]
因此,bean.getCollectionCount() 返回的是 CMS 子阶段总执行次数,而非用户语义上的“一次完整的 Full GC”。这就是你看到 sum of fullgc:1, add FullGC count:2 的真实原因。
✅ 正确做法:区分 GC 类型 + 聚合统计
不应依赖单一 GarbageCollectorMXBean 的 getCollectionCount() 判断 Full GC,而应:
识别真正的 Full GC 收集器:CMS 的 ConcurrentMarkSweep 是并发收集器,其 getCollectionCount() 包含并发与 foreground 混合计数;真正执行 Full GC 的是 ParNew(年轻代)+ CMS(老年代)组合,但更可靠的方式是监听 GarbageCollectionNotification。
使用 JMX 通知机制(推荐):
它能精确捕获每次 GC 的类型(endOfMajorGC / endOfMinorGC)、持续时间与内存变化,且不受 GC 算法内部阶段拆分影响:
import com.sun.management.GarbageCollectionNotificationInfo;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.util.List;
public class GCMonitor {
private static long fullGCCount = 0;
public static void startMonitoring() throws Exception {
List beans = ManagementFactory.getGarbageCollectorMXBeans();
for (GarbageCollectorMXBean bean : beans) {
ObjectName objName = ManagementFactory.newPlatformMXBeanObjectName(
ManagementFactory.GARBAGE_COLLECTOR_MXBEAN_DOMAIN_TYPE + "," +
"name=" + ObjectName.quote(bean.getName())
);
ManagementFactory.getPlatformMBeanServer().addNotificationListener(
objName,
(notification, handback) -> {
if (notification.getType().equals(GarbageCollectionNotificationInfo.GARBAGE_COLLECTION_NOTIFICATION)) {
GarbageCollectionNotificationInfo info =
GarbageCollectionNotificationInfo.from((CompositeData) notification.getUserData());
// 关键判断:Major GC(即 Full GC)通常作用于老年代(如 CMS Old Gen、G1 Old Generation)
if (info.getGcCause().contains("System.gc") ||
info.getGcCause().contains("Heap Inspection") ||
info.getGcName().toLowerCase().contains("old")) {
fullGCCount++;
System.out.printf("✅ Detected Full GC #%d: %s (%s) → %dms%n",
fullGCCount, info.getGcName(), info.getGcCause(), info.getGcInfo().getDuration());
}
}
},
null, null
);
}
}
} -
兼容性提醒(重要!)
- CMS 已在 JDK 9 中被标记为 deprecated,JDK 14 起彻底移除。现代应用应迁移到 G1 或 ZGC。
- G1/ZGC 的 getCollectionCount() 行为更符合直觉:一次 jmap -histo:live 仅触发 1 次 collectionCount 增量(对应一次 Mixed GC 或 Full GC)。
- 若必须支持旧版 CMS,请始终以 GarbageCollectionNotification 为准,放弃轮询 getCollectionCount()。
? 总结
- ❌ 错误认知:getCollectionCount() = Full GC 次数(尤其在 CMS foreground 模式下不成立)
- ✅ 正确认知:它是 GC 子阶段执行总次数,CMS 下一次 jmap 可能触发多次计数
- ✅ 最佳实践:使用 GarbageCollectionNotification 监听,结合 gcCause(如 "Heap Inspection")和 gcName(如 "CMS Old Gen")精准识别 Full GC
- ⚠️ 长期建议:升级至 G1/ZGC,简化监控逻辑并获得更好性能与可观测性
通过以上改进,你的 Full GC 监控将真正反映 JVM 行为本质,而非 GC 算法的实现细节陷阱。










