深入理解 JVM 中的 Concurrent Mode Failure:原因、影响与解决策略

0 阅读17分钟

在高并发 Java 系统中,CMS 垃圾收集器的 Concurrent Mode Failure 常常导致性能断崖式下降,引发长时间停顿,甚至引起系统雪崩。本文从技术本质出发,深入解析这一问题并提供系统化解决方案。

CMS 收集器背景与演进

CMS(Concurrent Mark Sweep)收集器在 JDK 1.5 引入,专为低延迟应用设计。重要的生命周期节点:

  • JDK 1.5:首次引入 CMS 收集器
  • JDK 9:被标记为废弃(deprecated)
  • JDK 14:被完全移除

尽管被废弃,大量生产环境的 JDK 8 系统仍在使用 CMS。

CMS 工作流程

CMS 工作流程.png

  • 红色阶段:STW(Stop-The-World),暂停所有用户线程
  • 蓝色阶段:与应用线程并发执行,几乎不影响应用运行

关键概念:浮动垃圾与三色标记

浮动垃圾(Floating Garbage)是 CMS 并发标记开始后产生的新垃圾,这些垃圾在本次 GC 中无法被清除,必须等到下次 GC。CMS 必须预留足够空间应对浮动垃圾,这也是 JDK 8 中 CMS 默认在老年代使用率达到 92%就启动回收的原因。

三色标记法是 CMS 并发标记的基础算法:

  • 白色:未被标记的对象,将被回收
  • 灰色:自身被标记,但引用对象未完全标记
  • 黑色:自身和引用对象都已标记完成,存活对象

为处理并发标记期间的引用变化,CMS 使用以下机制:

写屏障(Write Barrier): 当对象引用发生变化时执行的额外操作,捕获并记录引用变更,确保并发标记的正确性。

卡表(Card Table)与记忆集(Remembered Set)

  • 卡表是老年代的空间划分,每个卡表项对应一个老年代内存区域
  • 当老年代对象引用新生代对象时,通过写屏障标记对应的卡表项为"脏"
  • 新生代 GC 时只需扫描脏卡表,而非整个老年代,大幅提高效率
  • 记忆集是在 GC 收集器中维护的数据结构,记录从外部区域指向本区域的引用
  • 在 HotSpot 虚拟机中,卡表是记忆集的一种具体实现,专门用于解决老年代到新生代的引用问题

并发预清理(Concurrent Preclean)阶段

  • 作用是处理并发标记阶段中产生的新引用变化
  • 减少后续"重新标记"阶段的工作量,从而降低 STW 停顿时间
  • 遍历被修改的卡表,重新扫描引用关系,为最终标记做准备
  • 这是 CMS 为降低停顿时间所做的关键优化
// 写屏障伪代码示例
void oop_field_store(oop* field, oop new_value) {
    // 赋值操作
    *field = new_value;

    // 写后屏障
    if (new_value != null && is_in_young_gen(new_value) && is_in_old_gen(field)) {
        card_table->mark_card_as_dirty(field);
    }
}

Concurrent Mode Failure vs Promotion Failed

需要明确区分两种常见的 CMS 失败模式:

  1. Concurrent Mode Failure:CMS 并发收集过程中,老年代空间不足以容纳新晋升的对象,导致 CMS 被迫终止,切换到 Serial Old 收集器。

  2. Promotion Failed:新生代 Minor GC 时,老年代没有足够的连续空间容纳晋升对象,通常由内存碎片化导致。

比较这两种失败模式:

特征Concurrent Mode FailurePromotion Failed
触发阶段CMS 并发周期中Minor GC 期间
根本原因回收速度跟不上分配速度老年代碎片化
GC 日志关键词"concurrent mode failure""promotion failed"
解决思路提前触发 CMS、减少对象分配率内存整理、增加连续空间
紧急应对触发 Full GC触发 Full GC

Concurrent Mode Failure 的深层原因

1. 内存分配/晋升速率过快

当应用创建大对象或晋升速度超过 CMS 并发回收速度时,老年代空间会迅速耗尽。大对象(Humongous Allocation)对 CMS 的影响尤为严重,它们通常直接分配在老年代,迅速消耗老年代空间并加剧碎片化,是 Concurrent Mode Failure 的常见诱因之一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HighAllocationRateDemo {
    private static final Logger logger = LoggerFactory.getLogger(HighAllocationRateDemo.class);
    private static final int MB = 1024 * 1024;

    public static void main(String[] args) {
        try {
            // 模拟高速率内存分配
            for (int i = 0; i < 10000; i++) {
                byte[] buffer = new byte[2 * MB]; // 每次分配2MB内存
                // 执行操作确保buffer不会被立即回收
                process(buffer);
                Thread.sleep(1); // 控制分配速率
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            logger.error("线程被中断执行", e);
        } catch (OutOfMemoryError oome) {
            logger.error("内存分配失败,可能触发了Concurrent Mode Failure", oome);
            // 这里可以添加应急处理,如清理缓存或触发系统告警
        }
    }

    private static void process(byte[] buffer) {
        // 对buffer执行操作
        for (int i = 0; i < buffer.length; i += 1024) {
            buffer[i] = 1;
        }
    }
}

2. 老年代碎片化问题

CMS 使用标记-清除算法而非标记-整理算法,不会压缩内存,导致碎片化。即使总空间足够,也可能因找不到连续空间而失败。

判断内存碎片化的指标:

  • 老年代使用率不高(如 70%)却发生 OOM
  • jstat -gcutil显示老年代使用率波动但始终不低于某个值
  • GC 日志中出现"promotion failed"但老年代总空间充足

CMS 提供了以下参数处理碎片化:

-XX:+UseCMSCompactAtFullCollection  // Full GC时进行碎片整理 (JDK 9中废弃)
-XX:CMSFullGCsBeforeCompaction=n    // 每进行n次Full GC后进行一次碎片整理

这些参数在执行时会导致更长的停顿时间,因为需要额外的整理步骤,是典型的时间换空间的权衡。

3. CMS 触发时机优化

CMS 在 JDK 8 中默认当老年代使用率达到 92%时启动收集。早期版本(JDK 6 之前)中默认值为 68%,但在 JDK 8 中被调整为 92%。值得注意的是,这个默认值是 JVM 根据运行时动态计算的(自适应策略),除非同时使用了-XX:+UseCMSInitiatingOccupancyOnly参数,才会严格使用CMSInitiatingOccupancyFraction设定的值。

CMS 触发时机优化.png

实际案例与 GC 日志分析

在一个支持每秒 6000+交易的支付系统中,系统偶发性出现 3-5 秒响应延迟。以下是关键 GC 日志片段:

[001] [GC (CMS Initial Mark) [1 CMS-initial-mark: 2875342K(3145728K)] 2996886K(4194304K), 0.0084158 secs]
[002] [CMS-concurrent-mark-start]
[003] [CMS-concurrent-mark: 0.512/0.512 secs]
...省略部分日志...
[009] [CMS-concurrent-sweep-start]
[010] [CMS-concurrent-sweep: 0.354/0.354 secs]
[011] [CMS-concurrent-reset-start]
[012] [CMS-concurrent-reset: 0.059/0.059 secs]
[013] [GC (Allocation Failure) 875682K(1048576K)->875682K(1048576K), 0.3314230 secs]
[014] [Full GC (Allocation Failure) 875682K->875681K(1048576K), 3.8234520 secs]
...省略部分日志...
[023] [CMS-concurrent-sweep-start]
[024] [Full GC (Allocation Failure) 3895349K->2972651K(4194304K), 4.6706200 secs]

关键分析

  • 第 009-012 行显示 CMS 的并发清理正常进行
  • 第 013-014 行显示年轻代 GC 后紧接 Full GC,耗时 3.82 秒
  • 第 024 行再次发生 Full GC,耗时 4.67 秒,这是典型的 Concurrent Mode Failure

使用 JFR 和 JMC 分析 GC 事件

Java Flight Recorder(JFR)和 Java Mission Control(JMC)是分析 GC 问题的强大工具:

启用 JFR 记录

// JDK 8(需要解锁商业特性)
java -XX:+UnlockCommercialFeatures -XX:+FlightRecorder \
     -XX:StartFlightRecording=duration=300s,filename=cms_analysis.jfr \
     -XX:FlightRecorderOptions=stackdepth=128 \
     -jar your-application.jar

// JDK 11及以上版本(不需要解锁商业特性)
java -XX:+FlightRecorder \
     -XX:StartFlightRecording=duration=300s,filename=cms_analysis.jfr \
     -jar your-application.jar

JFR 分析步骤

  1. 使用 JMC 打开 jfr 文件:jmc -open cms_analysis.jfr
  2. 导航到"Garbage Collections"标签页
  3. 分析 GC 事件的持续时间和频率
  4. 查看"Old Collections"中的 Concurrent Mode Failure 事件
    • 这些事件在 JMC 中通常表现为较长的、红色的 Old GC 事件
    • 在事件详情中,"Cause"字段会显示"Concurrent Mode Failure"
    • 相关联的"Garbage Collection"图表会显示明显的停顿峰值
  5. 关联内存分配和对象晋升事件,特别注意观察 GC 前后老年代使用率的变化

多维度解决方案

1. JVM 参数优化

针对不同 JDK 版本的 CMS 优化参数:

// JDK 8 CMS优化参数
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:CMSInitiatingOccupancyFraction=70   // 降低触发阈值
-XX:+UseCMSInitiatingOccupancyOnly      // 只使用设定阈值
-XX:+CMSScavengeBeforeRemark            // 重标记前执行Young GC
-XX:+CMSParallelRemarkEnabled           // 并行重标记
-XX:ConcGCThreads=4                     // 并发GC线程数
-XX:+CMSClassUnloadingEnabled           // 允许类卸载
-XX:+UseCMSCompactAtFullCollection      // Full GC时压缩
-XX:CMSFullGCsBeforeCompaction=5        // 5次Full GC后压缩一次
-XX:+ExplicitGCInvokesConcurrent        // System.gc()触发并发GC

关键 CMS 参数详解

-XX:+CMSClassUnloadingEnabled

  • 默认在 CMS 中不启用类卸载
  • 启用后允许在 CMS 周期中卸载不再使用的类
  • 对于动态类加载频繁的应用(如使用反射、动态代理的系统)尤为重要
  • 可能略微增加 GC 暂停时间,但可以防止 Metaspace 区域 OOM

-XX:CMSInitiatingOccupancyFraction

  • JDK 8 默认值为 92%(较高)
  • 建议根据应用特点调整为 60%-80%
  • 数值过低会导致频繁 GC,影响吞吐量
  • 数值过高会增加 Concurrent Mode Failure 风险

2. 高级对象池与资源管理

import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 增强的线程安全对象池实现,带资源限制和监控
 * @param <T> 池化对象类型
 */
public class EnhancedObjectPool<T> implements AutoCloseable {
    private static final Logger logger = LoggerFactory.getLogger(EnhancedObjectPool.class);
    private final ConcurrentLinkedQueue<T> pool;
    private final Supplier<T> objectFactory;
    private final int maxSize;
    private final AtomicInteger activeCount = new AtomicInteger(0);
    private final AtomicInteger totalCreated = new AtomicInteger(0);
    private final int maxActive;
    private volatile boolean closed = false;

    public EnhancedObjectPool(Supplier<T> objectFactory, int initialSize, int maxSize, int maxActive) {
        if (initialSize < 0 || maxSize <= 0 || maxActive <= 0) {
            throw new IllegalArgumentException("池参数必须为正数");
        }

        this.objectFactory = objectFactory;
        this.maxSize = maxSize;
        this.maxActive = maxActive;
        this.pool = new ConcurrentLinkedQueue<>();

        try {
            // 预创建对象
            for (int i = 0; i < initialSize; i++) {
                T instance = objectFactory.get();
                totalCreated.incrementAndGet();
                pool.offer(instance);
            }
            logger.info("对象池初始化完成,初始大小: {}", initialSize);
        } catch (Exception e) {
            logger.error("初始化对象池失败", e);
            throw new RuntimeException("无法初始化对象池", e);
        }
    }

    public T borrow() {
        if (closed) {
            throw new IllegalStateException("对象池已关闭");
        }

        // 此处存在微小竞态条件,但在多数场景下可接受以换取更高性能
        if (activeCount.incrementAndGet() > maxActive) {
            activeCount.decrementAndGet();
            logger.warn("活跃对象数量超过限制: {}", maxActive);
            throw new IllegalStateException("活跃对象数量超过限制: " + maxActive);
        }

        T instance = pool.poll();
        if (instance == null) {
            try {
                instance = objectFactory.get();
                totalCreated.incrementAndGet();
                logger.debug("创建新对象实例,当前活跃数: {}, 总创建数: {}",
                            activeCount.get(), totalCreated.get());
            } catch (Exception e) {
                activeCount.decrementAndGet();
                logger.error("创建对象失败", e);
                throw new RuntimeException("无法创建对象", e);
            }
        }
        return instance;
    }

    public void release(T instance) {
        if (closed) {
            // 池已关闭,销毁对象
            destroyObject(instance);
            return;
        }

        try {
            if (instance != null && pool.size() < maxSize) {
                pool.offer(instance);
            } else if (instance != null) {
                destroyObject(instance);
            }
        } catch (Exception e) {
            logger.warn("归还对象到池失败", e);
        } finally {
            activeCount.decrementAndGet();
        }
    }

    private void destroyObject(T instance) {
        if (instance instanceof AutoCloseable) {
            try {
                ((AutoCloseable) instance).close();
            } catch (Exception e) {
                logger.warn("关闭对象失败", e);
            }
        }
    }

    public int getActiveCount() {
        return activeCount.get();
    }

    public int getPoolSize() {
        return pool.size();
    }

    public int getTotalCreated() {
        return totalCreated.get();
    }

    @Override
    public void close() {
        closed = true;

        // 清理池中所有对象
        T instance;
        while ((instance = pool.poll()) != null) {
            destroyObject(instance);
        }

        logger.info("对象池已关闭,释放所有资源");
    }
}

3. 优化的批量处理实现

改进批处理实现,避免 O(n²)复杂度:

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * 高效批量处理器,使用分区策略接口实现可扩展性
 */
public class BatchProcessor<T, R> {
    private final int batchSize;
    private final Function<List<T>, List<R>> batchProcessor;
    private final BatchPartitionStrategy<T> partitionStrategy;

    public BatchProcessor(int batchSize, Function<List<T>, List<R>> batchProcessor) {
        this(batchSize, batchProcessor, new DefaultBatchPartitionStrategy<>());
    }

    public BatchProcessor(int batchSize, Function<List<T>, List<R>> batchProcessor,
                         BatchPartitionStrategy<T> partitionStrategy) {
        this.batchSize = batchSize;
        this.batchProcessor = batchProcessor;
        this.partitionStrategy = partitionStrategy;
    }

    public List<R> processBatch(List<T> items) {
        if (items == null || items.isEmpty()) {
            return new ArrayList<>();
        }

        return partitionStrategy.partition(items, batchSize).stream()
            .flatMap(batch -> batchProcessor.apply(batch).stream())
            .collect(Collectors.toList());
    }

    /**
     * 批处理分区策略接口
     */
    public interface BatchPartitionStrategy<T> {
        List<List<T>> partition(List<T> items, int batchSize);
    }

    /**
     * 默认分区策略实现,O(n)时间复杂度
     */
    public static class DefaultBatchPartitionStrategy<T> implements BatchPartitionStrategy<T> {
        @Override
        public List<List<T>> partition(List<T> items, int batchSize) {
            int size = items.size();
            return IntStream.range(0, (size + batchSize - 1) / batchSize)
                .mapToObj(i -> {
                    int start = i * batchSize;
                    int end = Math.min(start + batchSize, size);
                    // 注意:subList返回的是一个视图,对它的修改会影响原始列表
                    return items.subList(start, end);
                })
                .collect(Collectors.toList());
        }
    }
}

使用示例:

// 处理订单批量
BatchProcessor<Order, OrderResult> processor =
    new BatchProcessor<>(1000, this::processOrderBatch);
List<OrderResult> results = processor.processBatch(orders);

// 使用自定义分区策略
BatchProcessor<Customer, CustomerDTO> customerProcessor =
    new BatchProcessor<>(500, this::processCustomers, new PriorityBatchStrategy<>());
List<CustomerDTO> customerResults = customerProcessor.processBatch(customers);

4. Web 应用中的 ThreadLocal 安全使用

在 Servlet 容器环境中,线程可能会被线程池复用,导致 ThreadLocal 值泄漏到其他请求:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Web应用中安全使用ThreadLocal的辅助类。
 * 注意:此实现适用于单个应用部署环境。在复杂的应用服务器环境中,
 * 如果多个应用共享此类,或应用需要热部署,可能会导致类加载器泄漏。
 * 在这种情况下,建议使用框架(如Spring)提供的请求作用域管理机制。
 */
public class ThreadLocalManager {
    private static final Logger logger = LoggerFactory.getLogger(ThreadLocalManager.class);

    // 注册所有ThreadLocal以便集中管理
    private static final List<ThreadLocal<?>> REGISTRY = new java.util.concurrent.CopyOnWriteArrayList<>();

    /**
     * 创建并注册ThreadLocal
     */
    public static <T> ThreadLocal<T> createThreadLocal(Supplier<T> initialValueSupplier) {
        ThreadLocal<T> threadLocal = ThreadLocal.withInitial(initialValueSupplier);
        register(threadLocal);
        return threadLocal;
    }

    /**
     * 注册现有ThreadLocal
     */
    public static synchronized <T> void register(ThreadLocal<T> threadLocal) {
        REGISTRY.add(threadLocal);
    }

    /**
     * 清理当前线程所有注册的ThreadLocal
     * 在请求结束时调用,如在Filter的finally块中
     */
    public static void cleanupThread() {
        REGISTRY.forEach(ThreadLocal::remove);
        logger.debug("已清理当前线程的所有ThreadLocal资源");
    }
}

// 使用示例
public class RequestLoggingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                        FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } finally {
            // 请求结束时清理
            ThreadLocalManager.cleanupThread();
        }
    }
}

5. 高级监控与预警系统

改进的 GC 指标收集器,实现 AutoCloseable 接口:

import java.lang.management.ManagementFactory;
import javax.management.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GCMetricsCollector implements GCMetricsMBean, NotificationListener, AutoCloseable {
    private static final Logger logger = LoggerFactory.getLogger(GCMetricsCollector.class);
    private volatile long fullGCCount = 0;
    private volatile long cmsFailureCount = 0;
    private volatile long lastGCDuration = 0;
    private final MBeanServer mbs;
    private final ObjectName name;
    private final NotificationEmitter emitter;

    public GCMetricsCollector() throws Exception {
        mbs = ManagementFactory.getPlatformMBeanServer();
        name = new ObjectName("com.example:type=GCMetrics");
        mbs.registerMBean(this, name);

        // 注册GC通知监听
        emitter = (NotificationEmitter)
            ManagementFactory.getGarbageCollectorMXBeans().stream()
            .filter(gc -> gc.getName().contains("CMS"))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("未找到CMS收集器"));

        emitter.addNotificationListener(this, null, null);
        logger.info("GC指标收集器已初始化并注册");
    }

    @Override
    public void handleNotification(Notification notification, Object handback) {
        if (notification.getType().equals("com.sun.management.gc.notification")) {
            CompositeData cd = (CompositeData) notification.getUserData();
            String gcAction = (String) cd.get("gcAction");
            String gcCause = (String) cd.get("gcCause");
            CompositeData gcInfo = (CompositeData) cd.get("gcInfo");
            long duration = (Long) gcInfo.get("duration");

            lastGCDuration = duration;

            if (gcAction.contains("end of major GC")) {
                fullGCCount++;
                if (gcCause.contains("concurrent mode failure")) {
                    cmsFailureCount++;
                    logger.warn("检测到Concurrent Mode Failure,当前计数: {}, 持续时间: {}ms",
                               cmsFailureCount, duration);
                }

                logger.info("Full GC完成,原因: {}, 持续时间: {}ms", gcCause, duration);
            }
        }
    }

    @Override
    public long getFullGCCount() {
        return fullGCCount;
    }

    @Override
    public long getCMSFailureCount() {
        return cmsFailureCount;
    }

    @Override
    public long getLastGCDuration() {
        return lastGCDuration;
    }

    @Override
    public void close() throws Exception {
        // 注销通知监听器
        emitter.removeNotificationListener(this);
        // 注销MBean
        mbs.unregisterMBean(name);
        logger.info("GC指标收集器已关闭");
    }
}

interface GCMetricsMBean {
    long getFullGCCount();
    long getCMSFailureCount();
    long getLastGCDuration();
}

6. 带熔断机制的服务降级

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GCBasedDegradationService implements AutoCloseable {
    private static final Logger logger = LoggerFactory.getLogger(GCBasedDegradationService.class);
    private final AtomicLong fullGCCount = new AtomicLong(0);
    private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    // 熔断器状态
    private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
    private final int circuitBreakerThreshold;
    private volatile boolean degradationActive = false;
    private volatile boolean circuitOpen = false;

    public GCBasedDegradationService(int circuitBreakerThreshold) {
        this.circuitBreakerThreshold = circuitBreakerThreshold;
        // 监控GC状态
        scheduler.scheduleAtFixedRate(this::checkGCStatus, 10, 10, TimeUnit.SECONDS);
        logger.info("GC降级服务已启动,熔断阈值: {}", circuitBreakerThreshold);
    }

    private void checkGCStatus() {
        try {
            long currentFullGCs = getFullGCCount();
            long delta = currentFullGCs - fullGCCount.getAndSet(currentFullGCs);

            if (delta > 3) { // 10秒内超过3次Full GC
                if (!degradationActive) {
                    activateDegradation();
                }
            } else if (degradationActive && delta == 0) {
                deactivateDegradation();
            }
        } catch (Exception e) {
            logger.error("检查GC状态失败", e);
        }
    }

    private void activateDegradation() {
        degradationActive = true;
        logger.warn("检测到频繁Full GC,激活服务降级模式");
        // 实施降级策略,如减少队列长度、拒绝非核心请求等
    }

    private void deactivateDegradation() {
        degradationActive = false;
        logger.info("GC状态恢复正常,解除服务降级");
    }

    /**
     * 带熔断器的方法执行
     */
    public <T> T executeWithCircuitBreaker(Supplier<T> operation, T fallback) {
        if (circuitOpen) {
            logger.warn("熔断器开启,直接返回降级结果");
            return fallback;
        }

        try {
            T result = operation.get();
            consecutiveFailures.set(0); // 重置失败计数
            return result;
        } catch (Exception e) {
            if (consecutiveFailures.incrementAndGet() >= circuitBreakerThreshold) {
                circuitOpen = true;
                logger.error("连续失败次数达到阈值{},开启熔断器", circuitBreakerThreshold, e);
                // 安排熔断器自动关闭
                scheduleCircuitReset();
            }
            logger.warn("操作执行失败,返回降级结果", e);
            return fallback;
        }
    }

    private void scheduleCircuitReset() {
        scheduler.schedule(() -> {
            logger.info("熔断器冷却时间已到,重置为半开状态");
            circuitOpen = false;
            consecutiveFailures.set(0);
        }, 30, TimeUnit.SECONDS);
    }

    private long getFullGCCount() {
        // 从JMX获取Full GC计数
        // 注意:此处的filter仅适用于CMS,如果使用G1收集器,应寻找名为"G1 Old Generation"的MXBean
        return ManagementFactory.getGarbageCollectorMXBeans().stream()
            .filter(gc -> gc.getName().contains("MarkSweep"))
            .findFirst()
            .map(GarbageCollectorMXBean::getCollectionCount)
            .orElse(0L);
    }

    public boolean isDegradationActive() {
        return degradationActive;
    }

    public boolean isCircuitOpen() {
        return circuitOpen;
    }

    @Override
    public void close() {
        try {
            scheduler.shutdown();
            if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
            logger.info("GC降级服务已关闭");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            scheduler.shutdownNow();
            logger.warn("强制关闭GC降级服务调度器", e);
        }
    }
}

7. 使用 MAT 分析内存问题的详细步骤

Memory Analyzer Tool (MAT)是分析堆转储文件的最佳工具:

1. 生成堆转储文件

// 自动在OOM时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps/

// 手动触发堆转储
jmap -dump:format=b,file=heap.hprof <pid>

2. 使用 MAT 加载分析

  • 下载并安装 Eclipse MAT
  • 加载堆转储文件:File -> Open Heap Dump
  • 运行泄漏检测:Leak Suspects Report

3. 查找大对象

  • 使用 Histogram 视图查看对象分布
  • 按大小排序:右键 -> Sort -> Descending by Retained Size
  • 分析对象持有关系:右键对象 -> List objects -> with outgoing references

4. 查找内存泄漏

  • 检查 Dominator Tree 视图中的大对象
  • 分析 GC Roots 到对象的最短引用路径
  • 识别异常的引用模式,如长时间缓存、未关闭的资源等

5. 对比多个堆转储

  • 在不同时间点创建多个堆转储
  • 使用比较功能:Window -> Compare Basket
  • 分析对象数量和内存占用的变化趋势

从 CMS 迁移到现代 GC 收集器

不同 GC 收集器对比

垃圾收集器适用场景优点缺点推荐 JDK 版本
CMS低延迟要求、有经验调优团队低停顿时间内存碎片、浮动垃圾JDK 8
G1大内存、可接受短暂停顿可预测停顿、区域化收集JDK 8 性能不如 CMSJDK 11+
ZGC超低延迟、TB 级堆停顿<1ms、可扩展吞吐量略低JDK 15+
Shenandoah低延迟、跨平台与 ZGC 相似、ARM 支持红帽主导JDK 12+

CMS 到 G1 迁移步骤

  1. 准备阶段

    • 建立性能基线,记录关键指标
    • 在测试环境验证 G1 参数配置
    • 准备回滚方案
  2. 迁移参数

# 移除CMS相关参数
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:CMSInitiatingOccupancyFraction=70
...

# 添加G1参数
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1NewSizePercent=20
-XX:G1MaxNewSizePercent=60
-XX:G1HeapWastePercent=10

3. 监控与调优: * 关注 G1 的 evacuation failure 和 humongous allocation * 调整 MaxGCPauseMillis 和区域大小平衡延迟与吞吐量 * 使用 JFR 验证内存分配模式变化

值得注意的是,G1 在 JDK 8 后期版本(如 u191+)中也得到了显著改进。对于无法升级到 JDK 11+但希望尝试新 GC 的用户,G1 也是一个可以考虑的选项,尽管其性能不如 JDK 11+中的 G1 成熟。

调优决策

为不同场景选择合适的优化策略:

调优决策.png

术语

术语解释
CMSConcurrent Mark Sweep,以低延迟为目标的老年代垃圾收集器
STWStop-The-World,垃圾收集器暂停所有用户线程的行为
Concurrent Mode FailureCMS 并发收集过程中,老年代空间不足导致的收集失败
Promotion FailedMinor GC 过程中,对象无法晋升到老年代的现象
浮动垃圾并发标记过程中新产生的垃圾,本次 GC 无法清除
碎片化内存空间被分割成多个不连续的小块,导致大对象分配失败
卡表(Card Table)记录老年代对象引用新生代对象的数据结构,用于减少 GC 扫描范围
写屏障(Write Barrier)在引用更新时执行的额外操作,用于维护 GC 正确性
三色标记并发标记算法中对象的三种状态:白(未访问)、灰(部分访问)、黑(完全访问)
JFRJava Flight Recorder,JVM 内置的性能分析工具
MATMemory Analyzer Tool,分析堆转储文件的工具
记忆集(Remembered Set)用于记录从外部指向本区域的引用,辅助垃圾回收
熔断器(Circuit Breaker)一种故障容错模式,防止系统级联失败
大对象(Humongous Allocation)大小超过标准区域一半的对象,在 G1 中有特殊处理机制

总结

方面说明
Concurrent Mode Failure 本质CMS 并发收集速度跟不上内存分配/晋升速度
主要原因1. 内存分配/晋升速率过高
2. 老年代碎片化
3. CMS 触发时机过晚
诊断工具1. GC 日志分析(GCViewer)
2. JFR/JMC 性能分析
3. MAT 堆分析
4. JMX 监控指标
解决方案1. JVM 参数优化
2. 对象生命周期管理
3. 实现池化和批处理
4. 考虑迁移到 G1/ZGC
预防措施1. 建立性能基线和告警
2. 定期性能测试
3. 代码审核关注内存使用
4. 实时监控 GC 状态

参考文献

  1. Oracle, "Java HotSpot VM Options", docs.oracle.com/javase/8/do…
  2. Poonam Parhar, "Understanding CMS GC Logs", Oracle Technical Article
  3. R. Tene, "Understanding Garbage Collection", www.jfokus.se/jfokus17/pr…
  4. 周志明, 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》, 机械工业出版社
  5. Alexey Ragozin, "Java GC explained", blog.ragozin.info/2011/06/und…