
1. GC优化不是调几个参数就完事而是对程序生命周期的深度理解“GC优化”这四个字在Java、C#、Node.js甚至PythonCPython的引用计数循环检测工程师的日常里出现频率高得有点刺眼。但绝大多数人一听到它第一反应是翻出JVM启动参数列表把-XX:UseG1GC改成-XX:UseZGC再调大-Xmx然后点个重启——仿佛GC优化就是一场参数填空游戏。我干了十多年后端和中间件开发从单机Tomcat到百万QPS的微服务集群踩过太多坑才明白GC不是性能瓶颈的“替罪羊”而是系统健康状态最诚实的仪表盘。它不撒谎每次Full GC的停顿时间、每次Young GC后老年代的缓慢爬升、每次Promotion Failure的报错日志都在告诉你你的对象生命周期设计错了、缓存策略失灵了、线程池配置失当了甚至数据库查询没加索引——GC只是把所有上游问题用最直观的方式STW时间、内存溢出、吞吐量骤降打在你脸上。真正有效的GC优化从来不是孤立地调垃圾回收器而是以GC行为为线索反向解构整个应用的内存使用模式。比如你看到G1 GC频繁触发Mixed GC别急着调-XX:MaxGCPauseMillis先问自己为什么这么多对象能活过多次Young GC进入老年代是缓存没设淘汰策略还是某个定时任务每分钟new出几百个大对象塞进静态Map又比如ZGC明明标称毫秒级停顿但实际监控发现ZUncommit阶段耗时飙升那大概率是堆外内存泄漏或DirectByteBuffer没及时clean跟ZGC本身关系不大。所以这篇内容的核心不是教你怎么抄参数而是带你建立一套“GC行为→内存模式→代码缺陷→架构调整”的闭环诊断思维。它适合三类人刚接手老项目的Java工程师面对一堆OOM日志手足无措、做高并发中间件的开发者需要稳定亚毫秒级响应、以及准备技术面试的候选人面试官问“你们怎么做的GC优化”答“用了ZGC”绝对拿不到offer。接下来我会拆解真实生产环境里最常遇到的5种GC异常模式给出可落地的根因定位路径、验证方法以及比调参更本质的代码与架构层面的解决方案。2. GC优化的本质从“回收算法”到“对象生命周期管理”的范式转移2.1 为什么90%的GC调优最终都失败了我见过太多团队投入大量时间做GC优化结果却收效甚微甚至适得其反。根本原因在于他们把GC优化当成一个“黑盒调参”问题而忽略了它背后真正的本质对象生命周期管理。JVM的垃圾回收器G1、ZGC、Shenandoah等本质上是一套高度自动化的内存管家它的核心职责是识别哪些对象已经“死亡”并安全地回收其占用的内存空间。但这个“死亡”的判定标准完全取决于你的代码如何创建、持有、传递和释放对象。如果代码中充斥着长生命周期的缓存、未关闭的资源句柄、静态集合的无序堆积那么无论你换多先进的GC算法都只是在给一个不断漏水的水池拼命换水泵。举个最典型的例子某电商订单服务上线后每小时发生一次Full GC团队花了两周时间尝试各种G1参数组合——调整Region大小、修改Mixed GC触发阈值、降低目标停顿时间……最终发现问题根源是一个被遗忘的static MapString, OrderDetail缓存它没有设置LRU淘汰策略也没有过期时间随着促销活动开展缓存条目从几千暴涨到百万级直接撑爆老年代。解决方法不是换ZGC而是用Caffeine替换掉那个静态Map并配置maximumSize(10000)和expireAfterWrite(10, TimeUnit.MINUTES)。GC压力瞬间下降80%Full GC消失。这个案例说明GC优化的第一步永远是审视代码中的对象创建与持有逻辑而不是打开jstat看一眼YGC次数就去改JVM参数。2.2 四大GC回收器的真实适用场景与硬伤市面上主流的JVM GC回收器常被简单粗暴地贴上“新”“快”“好”的标签但实际选型必须结合业务特征。我根据五年内参与的17个线上项目涵盖金融支付、实时风控、IoT设备管理、内容推荐总结出它们最真实的战场表现Parallel GC吞吐量优先这是JDK8的默认GC也是绝大多数传统企业应用的起点。它的优势在于简单、稳定、CPU利用率高特别适合后台批处理任务如日终结算、报表生成。但它的致命伤是Stop-The-World时间不可控一次Full GC可能长达数秒。如果你的系统SLA要求P99响应时间200msParallel GC基本可以排除。CMSConcurrent Mark-Sweep已废弃曾经的低延迟明星但因其并发模式下的“浮动垃圾”问题和复杂的碎片整理机制在JDK14中被正式移除。现在还看到CMS配置基本意味着系统长期未升级存在严重安全隐患。我的建议是立刻升级JDK并迁移到G1或ZGC。G1 GC平衡之选目前生产环境的主力。它通过将堆划分为多个Region实现了可预测的停顿时间模型。但G1的“平衡”是有代价的它需要额外的Remembered SetRSet来跟踪跨Region引用这会带来约10%-15%的内存开销和CPU消耗。在内存极其敏感的场景如容器化部署内存配额严格G1的RSet开销可能成为瓶颈。我们曾在一个K8s集群中将G1的-XX:G1HeapRegionSize从默认的1MB调小到512KB结果RSet内存占用反而增加因为Region数量翻倍导致RSet元数据膨胀。最终解决方案是保持1MB Region Size并通过代码减少跨Region引用如避免大对象跨Region分配。ZGC/Shenandoah超低延迟ZGC的目标是停顿时间10ms且与堆大小无关。但它对操作系统有强依赖Linux kernel 4.14且在JDK11-15版本中存在不少稳定性问题。我们一个实时风控系统在JDK15上启用ZGC后发现ZRelocate阶段偶尔卡顿达300ms排查后是JDK15.0.1的一个已知bug升级到15.0.2才修复。Shenandoah则对CPU更友好但内存占用略高。选择它们的前提是你的业务真的需要亚毫秒级的GC停顿且愿意承担新GC带来的运维复杂度。提示不要迷信“最新就是最好”。我们一个日均10亿次调用的API网关经过压测对比G1在24GB堆下平均GC停顿12msZGC在同样配置下平均8ms但ZGC的CPU使用率高出18%。考虑到网关是CPU密集型服务最终选择了G1并通过优化对象复用如ThreadLocal缓存StringBuilder将GC频率降低了40%。这说明GC选型必须放在整个系统资源约束下权衡。2.3 GC日志读懂JVM写给你的“诊断书”GC日志是GC优化的唯一真相来源但很多人只会看[GC (Allocation Failure)]这种基础信息。要真正读懂它你需要掌握三个关键维度时间维度关注[Times: user0.12 sys0.02, real0.14 secs]中的real墙钟时间这才是用户感知的停顿。usersys是CPU时间用于判断GC是否CPU受限。空间维度重点看[Eden: 1024.0M(1024.0M)-0.0M(1024.0M) Survivors: 128.0M-128.0M Heap: 2048.0M(4096.0M)-1024.0M(4096.0M)]。这里能看出Eden区是否被清空-0.0M表示成功回收Survivor区是否溢出Survivors: X-Y若YX说明有对象晋升堆总使用量变化Heap: A-B若B持续接近初始值说明内存泄漏事件维度区分GCYoung GC、Full GC、G1 Evacuation PauseG1的Mixed GC、ZGC Garbage Collection等。不同事件代表不同回收范围和成本。我习惯用-Xlog:gc*,gcheapdebug,gcagetrace:filegc.log:time,tags,level开启详细日志JDK10然后用gcviewer或gceasy.io进行可视化分析。但最关键的一步是把GC日志和业务日志按时间戳对齐。比如当你看到一次Full GC发生在14:23:15.234立刻去查同一秒的业务日志往往能找到线索可能是某个定时任务开始执行或是某个大促活动流量洪峰到来。有一次我们发现每天凌晨3点准时发生Full GC对齐日志后发现是Log4j2的RollingFileAppender在滚动日志时创建了大量临时ByteBuffer对象这些对象存活时间刚好超过Survivor区的年龄阈值被晋升到老年代。解决方案是调整RollingFileAppender的缓冲区大小并启用AsyncLogger。3. 实操五种高频GC异常模式的根因定位与代码级修复3.1 模式一Young GC频率过高每秒多次现象jstat -gc pid显示YGCTYoung GC总耗时和YGCYoung GC次数数值飞涨例如YGC1200YGCT45.2即平均每秒1.2次GC每次耗时37ms。根因分析Young GC频繁本质是Eden区“入不敷出”新对象申请速度远超GC回收速度。常见原因有短生命周期对象爆炸式创建如JSON序列化/反序列化、字符串拼接、正则匹配等操作每调用一次就产生大量临时对象。Eden区过小堆总大小合理但年轻代占比太低-XX:NewRatio设置过大。对象直接分配到老年代大对象-XX:PretenureSizeThreshold或TLABThread Local Allocation Buffer耗尽时对象直接在老年代分配加剧老年代压力间接导致Young GC更频繁因为老年代满会触发Full GC而Full GC前通常会强制一次Young GC。实操定位开启对象分配追踪-XX:PrintGCDetails -XX:PrintGCTimeStamps -XX:PrintAdaptiveSizePolicy使用jmap -histo pid查看实时对象分布重点关注char[]、java.lang.String、byte[]、java.util.HashMap$Node等高频对象。结合async-profiler进行火焰图分析./profiler.sh -e alloc -d 30 -f alloc.html pid找出分配热点方法。代码级修复案例 我们一个API网关的YGC高达每秒5次jmap -histo显示char[]占堆的65%。火焰图指向Jackson的ObjectMapper.readValue()。原代码// 每次请求都创建新ObjectMapper String json request.getBody(); ObjectMapper mapper new ObjectMapper(); // 错误重量级对象不应每次创建 User user mapper.readValue(json, User.class);修复后// 全局单例线程安全 private static final ObjectMapper MAPPER new ObjectMapper(); // 复用避免重复创建解析器 String json request.getBody(); User user MAPPER.readValue(json, User.class);同时为避免ObjectMapper的JsonParser内部缓存失效添加配置MAPPER.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false); MAPPER.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);效果YGC从5次/秒降至0.3次/秒CPU使用率下降12%。注意ObjectMapper是线程安全的但ObjectReader/ObjectWriter不是。如果需要定制配置如日期格式应使用ObjectMapper.readerFor(...)或writerFor(...)而非每次都new ObjectMapper()。3.2 模式二老年代持续增长最终OOM现象jstat显示OGCMN老年代初始容量、OGCMX老年代最大容量、OGC当前老年代容量三者中OGC持续缓慢上升逼近OGCMX最终触发java.lang.OutOfMemoryError: Java heap space。根因分析对象“活得太久”不断从年轻代晋升到老年代且老年代回收不力。核心原因通常是内存泄漏Memory Leak对象被意外强引用无法被GC回收。缓存滥用static集合、ConcurrentHashMap无淘汰策略、ThreadLocal未清理。大对象直接分配如byte[]数组、ArrayList扩容后的底层数组直接进入老年代。实操定位jstat -gc pid确认老年代增长趋势。jmap -dump:formatb,fileheap.hprof pid生成堆转储。用Eclipse MATMemory Analyzer Tool分析打开Leak Suspects ReportMAT会自动标记疑似泄漏点。查看Dominator Tree按Retained Heap排序找占用最大的对象。对关键对象右键Path to GC Roots选择exclude weak/soft references查看强引用链。代码级修复案例 一个消息队列消费者服务运行一周后OOM。MAT分析显示org.apache.kafka.clients.consumer.internals.Fetcher对象的completedFetches队列占用了85%的堆。Path to GC Roots显示其被KafkaConsumer的fetcher字段强引用而KafkaConsumer又被一个static的ConcurrentHashMap持有。代码片段// 错误静态Map存储Consumer实例且从未清理 private static final MapString, KafkaConsumer CONSUMER_MAP new ConcurrentHashMap(); public static KafkaConsumer getConsumer(String groupId) { return CONSUMER_MAP.computeIfAbsent(groupId, id - new KafkaConsumer(props)); // 每个groupId一个Consumer }问题在于KafkaConsumer内部维护了大量网络连接、缓冲区和元数据且completedFetches队列会随消费进度不断增长。修复方案根本解决取消静态Consumer缓存改为每个业务线程创建独立ConsumerKafka官方推荐。快速止损为CONSUMER_MAP添加LRU淘汰或增加close()调用钩子// 使用WeakReference避免强引用 private static final MapString, WeakReferenceKafkaConsumer CONSUMER_MAP new ConcurrentHashMap(); public static KafkaConsumer getConsumer(String groupId) { WeakReferenceKafkaConsumer ref CONSUMER_MAP.get(groupId); KafkaConsumer consumer ref ! null ? ref.get() : null; if (consumer null) { consumer new KafkaConsumer(props); CONSUMER_MAP.put(groupId, new WeakReference(consumer)); } return consumer; }3.3 模式三Full GC频繁发生每分钟多次现象jstat中FGCFull GC次数和FGCTFull GC总耗时数值激增例如FGC120FGCT320.5即平均每分钟2次Full GC每次平均2.7秒。根因分析Full GC是JVM的“急救手术”通常由以下原因触发老年代空间不足Young GC后晋升对象过多老年代无法容纳。Metaspace空间不足JDK8动态生成类如Spring CGLIB代理、Groovy脚本过多。System.gc()显式调用某些框架或SDK会主动触发如旧版logback在日志滚动时。实操定位jstat -gc pid观察MCMetaspace容量、MUMetaspace使用量是否接近MC。jinfo -flag PrintGCDetails pid确认是否有-XX:DisableExplicitGC禁用System.gc()。jstack pid搜索GC task thread线程栈看是否有System.gc()调用痕迹。代码级修复案例 一个微服务在发布新版本后Full GC从每天1次飙升到每小时5次。jstat显示MUMetaspace使用量从200MB涨到900MBMC1024MB。jmap -clstats pid显示加载的类数量从15000增至42000。进一步用jcmd pid VM.native_memory summary发现class区域内存占用异常。排查代码发现一个自定义的BeanFactoryPostProcessor在postProcessBeanFactory中为每个ServiceBean动态生成了一个CGLIB代理类// 错误为每个Bean都生成新代理类元数据永不卸载 for (String beanName : beanFactory.getBeanDefinitionNames()) { Class? clazz beanFactory.getType(beanName); if (clazz.isAnnotationPresent(Service.class)) { Enhancer enhancer new Enhancer(); enhancer.setSuperclass(clazz); enhancer.setCallback(new MyInterceptor()); Object proxy enhancer.create(); // 每次create都生成新类 } }修复方案绝不为每个Bean动态生成代理类。改为使用Spring AOP的标准方式或在启动时预生成有限的代理类模板。最终我们将代理逻辑移到Aspect切面中由Spring容器统一管理类加载数量回归正常Full GC消失。实操心得Metaspace OOM的错误日志非常明确java.lang.OutOfMemoryError: Metaspace。但很多工程师会忽略它以为是堆内存问题。记住只要看到这个错误第一步就是检查jstat -gc中的MU/MC第二步是jmap -clstats看类加载数。3.4 模式四GC停顿时间过长单次500ms现象jstat或GC日志显示单次GC尤其是Full GC或G1 Mixed GC的real时间超过500ms严重影响用户体验。根因分析停顿时间长核心是GC线程需要扫描、标记、移动大量对象。常见原因堆过大且碎片化特别是使用CMS时老年代碎片会导致Concurrent Mode Failure触发Serial Old GC。对象图过于复杂对象间引用关系网庞大标记阶段耗时。GC线程数不足-XX:ParallelGCThreads设置过小无法充分利用多核CPU。实操定位jstat -gc pid观察GCTGC总耗时与GC次数的比值若比值100ms说明单次耗时长。对于G1关注-XX:PrintGCDetails日志中的[G1Ergonomics (Mixed GCs)]部分看target number of mixed GCs是否被频繁调整。使用jcmd pid VM.native_memory detail检查internal区域是否异常可能表示GC内部结构开销大。代码级修复案例 一个实时推荐引擎使用G1 GC堆大小32GB但单次Mixed GC停顿常达1.2秒。日志显示[G1Ergonomics (Mixed GCs) do not add more regions to the collection set because the old gen is not filling up fast enough]。这说明G1认为老年代“不够满”但Mixed GC却在持续进行矛盾点在于老年代确实满了但G1的预测模型失效了。深入分析发现该服务大量使用java.util.concurrent.ConcurrentHashMap其内部的Node数组在扩容时会产生大量无法被G1高效处理的“大对象”。解决方案降低ConcurrentHashMap的初始容量避免早期就分配大数组。改用LongAdder替代AtomicLong减少CAS竞争和对象创建。最关键将推荐计算中的一次性大对象如double[]特征向量改为复用ThreadLocaldouble[]避免频繁分配。效果Mixed GC平均停顿从1200ms降至85msP99延迟下降40%。3.5 模式五GC后内存无法释放堆使用率居高不下现象一次Full GC后jstat显示OU老年代使用量只下降了很小一部分例如从3800M降到3750M释放仅50MB而堆总大小为4096MB。根因分析这通常意味着存在不可达但未被回收的对象最常见的是Finalizer队列阻塞对象重写了finalize()方法但Finalizer线程处理不过来导致对象一直卡在finalizer队列中。JNI全局引用泄漏Native代码中创建了JNIEnv-NewGlobalRef()但忘记调用DeleteGlobalRef()。ClassLoader泄漏Web应用热部署时旧的ClassLoader被新ClassLoader引用导致其加载的所有类和对象都无法卸载。实操定位jstat -gc pid确认OU在Full GC后无明显下降。jmap -finalizerinfo pid查看Number of objects pending for finalization若数值巨大1000基本锁定finalize()问题。jcmd pid VM.native_memory summary scaleMB对比internal和other区域若other异常高怀疑JNI泄漏。代码级修复案例 一个遗留的ERP系统每次Full GC后堆使用率只降1%jmap -finalizerinfo显示12482 objects pending for finalization。代码审计发现一个核心的DatabaseConnection类重写了finalize()// 错误finalize中执行耗时IO操作且未加锁 protected void finalize() throws Throwable { close(); // 调用数据库连接关闭可能耗时数秒 super.finalize(); }Finalizer线程是单线程的一个慢close()会阻塞整个队列。修复方案立即删除finalize()方法。所有资源清理工作必须通过try-with-resources或显式close()完成。对于必须异步清理的场景使用CleanerJDK9private static final Cleaner cleaner Cleaner.create(); private final Cleaner.Cleanable cleanable; public DatabaseConnection() { this.cleanable cleaner.register(this, new CleanupAction()); } private static class CleanupAction implements Runnable { public void run() { // 安全的清理逻辑 closeQuietly(); } }效果finalizerinfo数量归零Full GC后OU下降95%系统稳定性大幅提升。4. GC优化的终极武器从代码、JVM到架构的三级防御体系4.1 第一级防御代码层——让对象“生得少死得快”GC优化的起点永远是代码。再好的GC算法也救不了一个每毫秒都在new对象的程序。我总结了三条铁律铁律一杜绝无意义的对象创建字符串操作用StringBuilder代替尤其在循环中。String.format()比StringBuilder.append()慢3-5倍因为它内部会创建Formatter对象。集合初始化new ArrayList(16)比new ArrayList()好避免扩容时的数组复制。包装类型用Integer.valueOf(127)代替new Integer(127)利用缓存。铁律二善用对象池但警惕过度设计对象池如Apache Commons Pool、Netty Recycler对ByteBuffer、ByteBuf等重量级对象效果显著。但对String、Integer等轻量对象池化开销远大于创建开销。我们的经验是只有满足“创建成本高生命周期短可复用”三个条件的对象才值得池化。铁律三精准控制对象生命周期ThreadLocal务必在finally块中remove()否则在Web容器中会导致内存泄漏。WeakReference/SoftReference用于实现缓存WeakReference在下次GC时必然回收SoftReference则在内存不足时回收。AutoCloseable所有资源文件、网络连接、数据库连接必须实现AutoCloseable并在try-with-resources中使用。4.2 第二级防御JVM层——为GC创造最优环境代码优化后JVM参数是最后的“微调”。我的原则是最小化配置最大化监控。堆大小-Xms和-Xmx必须相等避免堆动态扩容带来的GC波动。大小设定基于jstat观测的OU峰值20%余量。年轻代比例-XX:NewRatio2年轻代:老年代1:2是通用起点。若YGC频繁可尝试-XX:NewRatio1若Promotion Rate高则调小年轻代。GC日志-Xlog:gc*:filegc.log:time,uptime,level,tagsJDK10这是你唯一的“行车记录仪”。禁用显式GC-XX:DisableExplicitGC防止第三方库调用System.gc()。实操心得不要迷信“一键优化脚本”。我们曾用一个网上下载的“JVM终极优化参数”部署服务结果因为-XX:G1NewSizePercent设置过高导致Eden区过小YGC频率暴增。后来全部回滚只保留-Xms/-Xmx相等和GC日志再根据日志数据逐步微调。记住JVM参数是结果不是原因。4.3 第三级防御架构层——让GC问题“无处可生”最高明的优化是让问题根本不发生。这需要架构层面的思考异步化与背压将同步阻塞操作如IO、RPC异步化用CompletableFuture或Reactor模式避免线程长时间等待减少ThreadLocal对象堆积。分治与隔离将内存敏感型模块如图像处理与业务逻辑模块物理隔离用独立进程或服务部署避免互相影响。数据流设计采用流式处理如Flink、Spark Streaming避免将海量数据一次性加载到内存。一个千万级用户画像计算任务从“全量加载内存计算”改为“分片流式计算”内存峰值从16GB降至2GB。5. 常见问题与排查技巧实录那些年我们一起踩过的坑5.1 “用了ZGC为什么停顿还是100ms”问题描述团队升级到JDK17启用ZGC-XX:UseZGC但APM监控显示GC停顿时间仍高达100ms远超宣传的10ms。排查过程确认ZGC是否真正生效jstat -gc pid若看到ZGC字样说明启用成功。检查-Xlog:gc*日志发现大量ZRelocate事件耗时异常。进一步jcmd pid VM.native_memory summary scaleMB发现internal区域占用高达1.2GB。根因ZGC的ZRelocate阶段需要移动对象这依赖于mmap系统调用。在容器化环境中/proc/sys/vm/max_map_count默认值65530过小导致ZGC无法分配足够的内存映射区域被迫退化为更慢的路径。解决方案# 在宿主机上执行 echo 262144 /proc/sys/vm/max_map_count # 或在Docker启动时 docker run --sysctl vm.max_map_count262144 ...效果ZRelocate耗时从100ms降至3ms以内。注意max_map_count是Linux内核参数不是JVM参数。很多团队只改JVM忘了改OS导致ZGC“名不副实”。5.2 “G1 GC的Mixed GC为什么总在‘混’却不‘收’”问题描述G1 GC日志中G1 Evacuation Pause (Mixed)频繁出现但老年代使用率OU却持续缓慢上升Mixed GC似乎“无效”。排查过程jstat -gc pid确认OGC确实在涨。jstat -gc -h10 pid 1000每秒打印10行观察ECEden容量和OC老年代容量变化。发现EC每次GC后都清空但OC只降一点点。根因G1的Mixed GC只回收部分老年代Region由-XX:G1MixedGCCountTarget控制如果老年代Region中存活对象比例过高65%G1会跳过这些Region只回收“垃圾多”的Region。这导致“顽固”的老年代Region一直得不到清理。解决方案降低-XX:G1MixedGCCountTarget默认8让G1在更多轮Mixed GC中尝试回收。提高-XX:G1OldCSetRegionThresholdPercent默认10允许G1回收存活率更高的Region。终极方案代码层减少老年代对象晋升如优化缓存策略、减少大对象分配。5.3 “为什么jmap -histo显示byte[]最多但jstack里找不到源头”问题描述jmap -histo pid显示byte[]占堆70%但jstack看不出哪个线程在大量分配。排查过程jmap -histo:live pid确认是活跃对象。jcmd pid VM.native_memory summary scaleMB发现internal区域异常高。使用async-profiler的alloc事件./profiler.sh -e alloc -d 60 -f alloc.html pid。根因byte[]本身是原始数组不包含业务逻辑它的分配者往往是底层库。最常见的“隐形杀手”是Netty的PooledByteBufAllocator如果配置不当会创建大量未释放的ByteBuf。HTTP客户端如OkHttp的响应体缓存Response.body().string()会将整个响应读入byte[]。日志框架的异步缓冲区Log4j2的AsyncLogger内部使用RingBuffer其byte[]数组会被统计为byte[]。解决方案对于Netty确保PooledByteBufAllocator.DEFAULT被正确使用并在ChannelHandler中及时release()。对于HTTP响应用Response.body().bytes()获取byte[]后立即处理避免长时间持有。对于日志调整AsyncLogger的RingBufferSize避免过大。5.4 “-XX:UseG1GC后jstat显示GCT飙升但YGC没变为什么”问题描述启用G1后jstat的GCTGC总耗时翻倍但YGC次数不变FGC为0。根因G1的GCT包含了Young GC、Mixed GC和并发周期Concurrent Cycle的全部耗时。G1的并发周期包括初始标记、并发标记、重新标记、清理虽然不STW但会消耗CPU时间计入GCT。jstat无法区分这部分耗时。验证方法jstat -gc -h10 pid 1000观察GCT和YGCT的差值。若差值很大说明并发周期耗时高。jstat -gc -h10 pid 1000同时jtop或htop观察CPU使用率若CPU高而YGCT不高基本确认是并发周期。解决方案降低并发周期频率-XX:G1ConcRefinementThreads默认值CPU核心数/4适当调小。加快并发标记-XX:G1ConcMarkThreads默认值CPU核心数/4适当调大。终极方案优化代码减少跨Region引用降低RSet更新开销。