在高并发 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 工作流程
- 红色阶段: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 失败模式:
-
Concurrent Mode Failure:CMS 并发收集过程中,老年代空间不足以容纳新晋升的对象,导致 CMS 被迫终止,切换到 Serial Old 收集器。
-
Promotion Failed:新生代 Minor GC 时,老年代没有足够的连续空间容纳晋升对象,通常由内存碎片化导致。
比较这两种失败模式:
特征 | Concurrent Mode Failure | Promotion 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
设定的值。
实际案例与 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 分析步骤:
- 使用 JMC 打开 jfr 文件:
jmc -open cms_analysis.jfr
- 导航到"Garbage Collections"标签页
- 分析 GC 事件的持续时间和频率
- 查看"Old Collections"中的 Concurrent Mode Failure 事件
- 这些事件在 JMC 中通常表现为较长的、红色的 Old GC 事件
- 在事件详情中,"Cause"字段会显示"Concurrent Mode Failure"
- 相关联的"Garbage Collection"图表会显示明显的停顿峰值
- 关联内存分配和对象晋升事件,特别注意观察 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 性能不如 CMS | JDK 11+ |
ZGC | 超低延迟、TB 级堆 | 停顿<1ms、可扩展 | 吞吐量略低 | JDK 15+ |
Shenandoah | 低延迟、跨平台 | 与 ZGC 相似、ARM 支持 | 红帽主导 | JDK 12+ |
CMS 到 G1 迁移步骤
-
准备阶段:
- 建立性能基线,记录关键指标
- 在测试环境验证 G1 参数配置
- 准备回滚方案
-
迁移参数:
# 移除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 成熟。
调优决策
为不同场景选择合适的优化策略:
术语
术语 | 解释 |
---|---|
CMS | Concurrent Mark Sweep,以低延迟为目标的老年代垃圾收集器 |
STW | Stop-The-World,垃圾收集器暂停所有用户线程的行为 |
Concurrent Mode Failure | CMS 并发收集过程中,老年代空间不足导致的收集失败 |
Promotion Failed | Minor GC 过程中,对象无法晋升到老年代的现象 |
浮动垃圾 | 并发标记过程中新产生的垃圾,本次 GC 无法清除 |
碎片化 | 内存空间被分割成多个不连续的小块,导致大对象分配失败 |
卡表(Card Table) | 记录老年代对象引用新生代对象的数据结构,用于减少 GC 扫描范围 |
写屏障(Write Barrier) | 在引用更新时执行的额外操作,用于维护 GC 正确性 |
三色标记 | 并发标记算法中对象的三种状态:白(未访问)、灰(部分访问)、黑(完全访问) |
JFR | Java Flight Recorder,JVM 内置的性能分析工具 |
MAT | Memory 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 状态 |
参考文献
- Oracle, "Java HotSpot VM Options", docs.oracle.com/javase/8/do…
- Poonam Parhar, "Understanding CMS GC Logs", Oracle Technical Article
- R. Tene, "Understanding Garbage Collection", www.jfokus.se/jfokus17/pr…
- 周志明, 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》, 机械工业出版社
- Alexey Ragozin, "Java GC explained", blog.ragozin.info/2011/06/und…