监控 JVM Full GC 次数的正确实践:避免 CMS 垃圾收集器计数误判

本文详解为何 `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,而应:

  1. 识别真正的 Full GC 收集器:CMS 的 ConcurrentMarkSweep 是并发收集器,其 getCollectionCount() 包含并发与 foreground 混合计数;真正执行 Full GC 的是 ParNew(年轻代)+ CMS(老年代)组合,但更可靠的方式是监听 GarbageCollectionNotification。

  2. 使用 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 ); } } }
  1. 兼容性提醒(重要!)
    • 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 算法的实现细节陷阱。