JVM新生代回收机制、GC日志诊断与参数调优

发布时间:2026/10/23 11:15:25

JVM新生代回收机制、GC日志诊断与参数调优 1. 新生代为什么值得单独设计两个假说和一本真实的账刚接触 JVM 内存模型的人十有八九会卡在同一个疑问上堆里明明还有一大半空间为什么新生代那边已经 GC 了几百次更让人困惑的是有些服务把堆从 4G 扩到 16GGC 次数不减反增停顿还变长了。要回答这些问题绕不开新生代的垃圾回收这套设计而理解它的起点不是背参数而是先承认一个现实内存里的对象绝大多数是活不长的。你把一次请求过程中产生的临时字符串、包装类型、List 中间结果、序列化缓冲区列出来会发现它们从创建到失去引用可能只隔了几微秒。1.1 弱代假说绝大多数对象活不过第一轮回收分代收集的理论依据有两条业内叫弱代假说。第一条是绝大部分对象都是朝生夕灭的。第二条是熬过越多次垃圾回收的对象就越难死掉。这两条听起来像废话但它决定了整个回收器的资源分配方式。假设一个服务每秒要分配 300MB 对象其中 99% 在几十毫秒内就变成了垃圾那么真正需要长期保留的数据可能只有几 MB。如果每次回收都去扫描整个堆等于为了几 MB 的活对象把几百 MB 的死对象全部遍历一遍成本完全不成比例。新生代存在的意义就是把这件事切开用极低的成本、极高的频率处理掉那 99% 的短命对象剩下的 1% 晋升到老年代用更慢但更省空间的算法去管。所以你在监控上看到的 YGC 次数远多于 Full GC不是设计缺陷恰恰是设计生效的证据。反过来说如果 YGC 次数很少往往说明新生代开得过大或者对象直接在老年代分配了这反而值得警惕。我见过一个后台任务的配置堆 8G新生代只给了 256M结果每秒 YGC 三四次每次回收几十兆CPU 有相当一部分耗在 GC 线程上。把新生代调到 2G 之后YGC 间隔从 300ms 拉到 4 秒吞吐直接上去了。这个调整之所以有效根子就在弱代假说上——短命对象的密度足够高给它们一个足够大的“缓冲区”回收频率自然就降下来了。1.2 如果不分代全堆标记的成本会吃掉什么可以做个粗略的估算。一个 16G 的堆如果每轮回收都做全堆可达性分析即使标记阶段是并发的光是把 16G 内存里的对象头挨个读一遍就是百毫秒级别的工作量。而新生代回收只处理 Eden 加一块 Survivor通常只占堆的几分之一而且新生代的根扫描范围小得多——只需要扫 GC Roots 加上老年代指向新生代的跨代引用老年代那一大片区域根本不用碰。这也是为什么新生代回收的停顿能压到几毫秒到几十毫秒而全堆回收经常上百毫秒甚至上秒。代价当然也有为了知道“老年代里哪些对象引用了新生代”JVM 需要维护记忆集主流实现用的是卡表把老年代切成固定大小的卡片跨代引用发生时通过写屏障把对应卡片标记为脏。写屏障本身有开销卡表也要占内存这是分代收集必须付的税天下没有白拿的性能。1.3 三代人的分工新生代、老年代各自管什么新生代负责承接所有新对象的分配用复制算法高频回收老年代负责收留那些熬过多轮回收的对象和大对象用标记-整理或标记-清除元空间在 JDK 8 之后移到了本地内存存放类元数据和新生代回收基本是两套逻辑。这里要强调的是三者不是完全隔离的独立王国跨代引用会把它们串起来。一个新生代对象被老年代对象引用着哪怕它在 Eden 里“看起来很孤立”也不能被回收。很多人调 GC 时只盯着新生代大小忽略了对象晋升到老年代之后的行为。真实情况是新生代回收的参数微调影响的是晋升速率而晋升速率又决定了老年代多久被填满、多久触发一次 Full GC。把新生代调大YGC 少了但每次 YGC 的存活对象更多晋升的绝对量可能更大老年代反而更快被撑爆。这个连锁反应是后面所有调优讨论的主线。2. 新生代的内存账本Eden 和两块 Survivor 到底怎么分打开任何一份新生代的内存布局图都会看到 Eden、S0、S1 三块区域。默认比例是 8:1:1很多人记住了这个数字却说不清为什么偏偏是 8 而不是 4 或者 16。要弄明白这件事得先看清楚新生代这块地是怎么被使用的以及对象在里面的实际流转轨迹。2.1 8:1:1 背后的空间利用率计算新生代的可用空间由-Xmn或者NewRatio决定。假设堆 4G-Xmx4g -Xms4g默认-XX:NewRatio2也就是老年代:新生代 2:1新生代约 1.33G。再按-XX:SurvivorRatio8切Eden 占 8 份两个 Survivor 各占 1 份总计 10 份。算下来 Eden 大约 1.09G每个 Survivor 约 136M。关键在于任何时刻只有一个 Survivor 是“有内容的”另一个是空的备用区。所以新生代的真实可用空间是 Eden 一块 Survivor约 9 份而不是 10 份。复制算法的设计前提就是这样牺牲 10% 的空间换来回收时可以直接把存活对象搬到一个干净区域不用原地整理碎片。那为什么是 8:1 而不是 9:1 或者 4:1这其实是个经验平衡点。Survivor 太小时稍微有点存活对象就装不下直接晋升老年代等于把压力转嫁出去Survivor 太大时Eden 被挤压YGC 频率上升而且两块 Survivor 中始终有一块闲置空间浪费明显。8:1:1 在经过大量真实负载验证后被证明能覆盖大多数“短命对象占绝对多数”的场景。当然如果你的业务有大量中等寿命对象比如几秒钟的缓存、会话数据这个比例未必合适后面会讲怎么调。注意SurvivorRatio表示的是 Eden 与单个Survivor 的比值不是 Eden 与两个 Survivor 之和的比值。这一点经常被搞错按 8 去算成 Eden 占 80%、两个 Survivor 各占 10% 是对的但如果理解成 Survivo 总量占 1/8 就会算错容量。2.2 两块 Survivor 的分工为什么不能只留一块只用一块 Survivor 行不行理论上可以但复制算法会退化成原地整理。回收时 Eden 和 Survivor 里的存活对象必须搬到同一个区域而那个区域本身就有存活对象搬过去会覆盖只能先做整理成本瞬间上升。两块 Survivor 的核心价值是双缓冲回收时 From 区里的活对象统一搬到空的 To 区Eden 直接整块清空整个过程中不需要做任何碎片整理。回收结束后From 和 To 的角色互换原来的 From 被清空成为下一轮的 To。所以你在jstat输出里看到的 S0U 和 S1U往往是一个有值一个为 0或者大小交替变化这不是故障是角色轮换的正常表现。有一次同事看到 S1U 一直是 0怀疑 Survivor 没生效其实只是采样时间点刚好落在轮换的某一侧。这里有个隐含约束To Survivor 必须装得下 Eden 加 From 里所有要留下的对象。装不下怎么办JVM 不会傻等它会把装不下的部分直接晋升到老年代这就是所谓的“分配担保”在起作用。所以 Survivor 大小的意义不只是“给对象一个多活几轮的机会”它还决定了单次 YGC 的晋升溢出量。2.3 对象分配的真实路径TLAB、指针碰撞与分配失败理解了布局还要理解对象是怎么被放进去的。新建对象时JVM 并不是每次都直接去 Eden 的共享区域抢位置而是先看当前线程的 TLABThread Local Allocation Buffer本地线程分配缓冲。每个线程在 Eden 里预先圈一块私有小区域默认由-XX:UseTLAB开启分配时只需要移动一下指针不需要任何锁。这就是所谓的指针碰撞——因为新生代用的是复制算法内存天然连续没有碎片分配就是把指针往后推一格。TLAB 用完了怎么办线程会重新申请一块。如果 Eden 剩余空间不够就先尝试在共享区做一次 CAS 分配连共享区也放不下就触发 YGC。这里有个容易被忽略的细节TLAB 的剩余空间小于某个阈值时会被直接废弃这个阈值由-XX:TLABWasteTargetPercent控制默认 1%。也就是说每个 TLAB 尾部最多浪费 1% 的空间。TLAB 设得太小线程会频繁申请增加管理开销设得太大浪费的空间变多Eden 实际可用量下降。和对象分配强相关的还有一个参数-XX:PretenureSizeThreshold。超过这个大小的对象会跳过新生代直接在老年代分配避免大对象在 Eden 和 Survivor 之间来回复制。但这里有个大坑——这个参数只对 Serial 和 ParNew 收集器生效在 Parallel Scavenge 下设置它是无效的很多人设了半年发现没起作用就是因为收集器不匹配。3. 复制算法在新生代的落地成本账和担保机制新生代为什么不用标记-清除答案藏在对象存活率上。标记-清除的标记阶段开销和存活对象数量、堆大小相关清除阶段还要维护空闲列表分配时得在列表里找合适大小的洞。当存活率只有百分之几的时候这套机制的成本和收益完全失衡你花了大力气标记和清理最后只回收出一点点空间还得面对碎片问题。3.1 为什么新生代只适合复制算法复制算法的逻辑简单粗暴把 Eden 和 From Survivor 里的存活对象挑出来整块复制到 To Survivor然后把 Eden 和 From 整体清空。注意这里的重点——它不遍历死对象只处理活对象。存活率 5% 意味着只需要搬 5% 的数据剩下 95% 直接抛弃这就是它的效率来源。标记-清除恰好相反标记阶段必须遍历所有对象包括那些马上要死的。从数据上看新生代一次 YGC 的耗时 ≈ 根扫描时间 存活对象复制时间 卡表处理时间。前两项都和存活对象数量强相关所以新生代回收快不快很大程度上取决于这一轮有多少对象活下来。这也解释了一个反直觉的现象有时候业务量下降、QPS 减半YGC 反而变慢了——因为创建的对象少了但缓存里的对象寿命变长存活率上升复制成本反而增加。复制算法也有短板它要求有额外的空闲空间做腾挪空间利用率天然不满。所以它只适合放在新生代这种“垃圾密度极高”的地方。放到老年代就是灾难因为老年代存活率高复制成本会爆炸。3.2 一次复制的完整路径从 Eden 到 To Survivor对象从 Eden 搬到 To Survivor 的过程中有几个信息会同步更新。第一是对象的年龄计数器每熬过一次 YGC 就加一。第二是引用关系的修正所有指向被搬动对象的引用都要改成新地址这个修正过程依赖对象头里的转发指针。第三是卡表的更新如果对象的地址变了老年代里指向它的引用所在卡片可能需要重新标记。搬过去之后对象在 Survivor 之间还会来回倒腾。比如一个对象在 S0 里下一轮 YGC 时 S0 是 From它要么被复制到 S1要么年龄到点直接晋升。所以一个对象可能经历这样一条路径Eden → S0年龄 1→ S1年龄 2→ S0年龄 3→ 老年代。每轮都要被复制一次这就是“熬”的代价。对于中等寿命对象这个复制成本不容忽视这也是为什么有些场景会刻意调小晋升阈值。实操提醒如果你在 GC 日志里看到Desired survivor size ... bytes, new threshold 1说明存活对象已经多到让 JVM 主动把晋升阈值降到了 1对象刚活过一轮就直接进老年代。这种日志出现时Survivor 基本已经失去意义了。3.3 空间分配担保老年代什么时候被“借”出来担保机制是新生代回收里最容易被误解的部分。它的核心问题是如果 To Survivor 装不下所有存活对象多出来的往哪放答案是老年代。但老年代也不是想借就能借JVM 在做 YGC 之前会先做一次预判判断的依据老年代最大连续可用空间和两个量比大小判断条件含义结果老年代连续空闲 ≥ 新生代所有对象总大小极端情况下全部对象晋升也放得下直接执行 YGC最安全老年代连续空闲 新生代总大小但 ≥ 历次晋升平均大小按历史经验赌一把尝试 YGC失败再 Full GC老年代连续空闲 历次晋升平均大小连经验值都放不下直接 Full GC早期版本里有个-XX:HandlePromotionFailure开关控制是否允许担保失败JDK 6 Update 24 之后这个参数的语义被简化了规则变成只要满足上面两条中的任意一条就做 YGC不满足就 Full GC。所以你在新版 JVM 上设这个参数基本没有效果看到旧文章还在讲它知道历史背景就行。担保失败Promotion Failed是真实会发生的故障。它的表现是 YGC 日志打印完之后紧接着一次 Full GC而且 Full GC 之后老年代占用并没有明显下降因为对象根本没死只是换个地方住。这种情况的根因通常是 Survivor 太小或者晋升速率过高导致大量对象在同一轮集中涌进老年代把老年代挤爆。4. 一次 Young GC 的完整时序复盘与日志逐行解读光看原理容易飘落到日志上才算真懂。下面这段是从一个使用 Parallel Scavenge 的服务上截下来的典型输出我把每一段拆开讲。先看参数-Xmx4g -Xms4g -Xmn1g -XX:SurvivorRatio8对应 Eden 约 819M两个 Survivor 各约 102M新生代总容量约 1G。1024.335: [GC (Allocation Failure) [PSYoungGen: 838656K-61440K(917504K)] 2097152K-1421312K(4030464K), 0.0312567 secs] [Times: user0.12 sys0.00, real0.03 secs]4.1 触发条件Eden 分配失败怎么发生的日志开头的GC (Allocation Failure)就是这个意思某次对象分配没找到足够空间触发了新生代回收。注意它说的是分配失败不是内存不足两者天差地别。分配失败是常态说明 Eden 满了内存不足是OutOfMemoryError说明真的没地方放了。触发点通常出现在三种情况Eden 剩余空间放不下当前要分配的对象、TLAB 申请不到新块且共享区也满了、或者显式调用System.gc()这种情况日志里会显示GC (System.gc())。前两种是正常的第三种应该在生产环境用-XX:DisableExplicitGC关掉避免有人误调用引起不必要的停顿。触发之后JVM 会进入安全点Safepoint所有应用线程停下来这叫 STW。STW 的长度就是日志末尾的real0.03 secs。这 30 毫秒里GC 线程要做的事包括扫描 GC Roots、遍历新生代对象、复制存活对象、修正引用、更新卡表、清理 Eden 和 From。4.2 日志字段逐项拆解把中间那串数字拆开看字段含义本次数值838656K回收前新生代已用Eden From约 819MEden 基本满了-61440K回收后新生代已用To Survivor 里留下的约 60M(917504K)新生代总容量Eden 两块 Survivor约 896M2097152K回收前整个堆已用2G-1421312K回收后整个堆已用约 1.35G(4030464K)堆总容量约 3.84G0.0312567 secs本次 GC 的 STW 耗时31ms从这组数能读出不少信息。堆回收前用了 2G回收后 1.35G减少的部分是 650M 左右而新生代只减少了约 760M两个数对不上。差额去了哪一部分是新生代里被回收的对象中有少量被老年代引用的回收后老年代占用上升了另一部分是数字取整造成的误差。这提示我们一个排查思路YGC 之后老年代占用上升了多少就是这一轮的晋升量。如果这个增量在一段时间内持续走高老年代迟早出问题。再看Times那一行user0.12是所有 GC 线程消耗的 CPU 时间总和real0.03是墙钟时间。user 远大于 real说明这次回收是多线程并行做的0.12 / 0.03 4大致对应 4 个 GC 线程。如果 real 明显大于 user说明线程没跑满可能有其他进程在抢 CPU。4.3 晋升判定的三条路径年龄、动态年龄、大对象一个对象什么时候进老年代不是单一规则决定的至少有四条路可以走第一条是年龄达标。对象每熬过一次 YGC年龄加一达到-XX:MaxTenuringThreshold就晋升。这个值默认是 15CMS 下默认是 6因为 CMS 的 Survivor 空间紧张早点送走更划算。年龄存在对象头的 Mark Word 里只有 4 个 bit所以最大只能是 15设再大也没用。第二条是动态年龄判定。这是很多人不知道的机制JVM 在复制对象时会统计 Survivor 中各个年龄段的对象大小总和从年龄 1 开始累加当累加到某个年龄时总和超过了 Survivor 空间的一半由-XX:TargetSurvivorRatio控制默认 50就把这个年龄及以上的对象全部晋升不再等到 15。这条规则的设计初衷是防止 Survivor 被挤爆但它的副作用很致命——Survivor 越小越容易触发对象晋升越早老年代压力越大。第三条是大对象直接进老年代。超过-XX:PretenureSizeThreshold的对象跳过新生代前面提过它只对 Serial 和 ParNew 有效。第四条是担保溢出。To Survivor 装不下的对象被迫晋升。这条最不可控因为它取决于单次回收时的瞬时存活对象量波动可能很大。这四条路径叠加在一起解释了为什么很多人的调优看起来“没生效”你调了MaxTenuringThreshold15但动态年龄判定在年龄 3 就把对象送走了参数形同虚设。5. 参数调优新生代真正需要动手的几个旋钮新生代的参数有十几个但真正需要按业务特征去动的不超过六个。剩下的要么已经被版本废弃要么只在特定收集器下有影响。我按“容量类”和“晋升类”分开讲因为这两类参数的作用方向是相反的调错了会互相打架。5.1 容量类参数与一次完整的容量计算容量类主要是三个-Xmn直接指定新生代大小、-XX:NewRatio老年代与新生代比值、-XX:SurvivorRatioEden 与单个 Survivor 的比值。前两个是互斥的-Xmn优先级更高设了它NewRatio就失效。G1 下不建议用-Xmn因为 G1 的新生代是动态伸缩的硬设会限制它的自适应能力。算一遍就清楚了。假设某服务堆 8G-Xmx8g -Xms8g -Xmn3g -XX:SurvivorRatio8新生代 3G Eden 2 × Survivor 8 份 1 份 1 份 10 份每份 307.2MEden ≈ 2.4GS0 ≈ 307MS1 ≈ 307M新生代某时刻可用 2.4G 307M ≈ 2.7G再估算 YGC 频率。假设通过jstat观察到 Eden 每秒增长 500M那么两次 YGC 的间隔大约 2.4G / 500M ≈ 4.8 秒。如果实测间隔远小于这个值说明要么 Eden 比算的小要么分配速率比估的高需要用数据核实不能拍脑袋。反过来从 YGC 间隔倒推合适的新生代大小也很实用。业务要求 YGC 间隔不短于 10 秒实测分配速率 400MB/s那 Eden 至少要 4G按 8:1:1 折算新生代总量约 5G。这时候就要权衡新生代拿走 5G老年代还剩多少够不够撑。提示-Xmn设得过大还有一个隐性代价——单次 YGC 的复制量变大。因为 Eden 大意味着两次回收之间积累的对象多存活下来的绝对值也可能变多扫描和复制的耗时都会上升STW 反而变长。这不是玄学是实测中很常见的现象。5.2 晋升类参数三个值和一条隐藏规则晋升类的核心是-XX:MaxTenuringThreshold、-XX:PretenureSizeThreshold、-XX:TargetSurvivorRatio。第一个控制年龄上限第二个控制大对象门槛第三个控制动态年龄判定的触发比例。MaxTenuringThreshold的取值要结合 Survivor 大小看。如果 Survivor 只有 100M就算你把阈值设成 15动态年龄判定也会在对象总量超过 50M 时提前把它们送走。所以只调阈值不改 Survivor等于没调。真正的组合是调大 Survivor把 SurvivorRatio 调小 适当提高阈值给对象更多在新生代“多活几轮”的机会。TargetSurvivorRatio默认 50调大意味着允许 Survivor 更满才触发批量晋升但要小心超过 100 会导致 Survivor 溢出。一般不建议动除非你非常确定存活对象的大小分布。这里有个隐藏规则值得单独说Parallel Scavenge 默认开启-XX:UseAdaptiveSizePolicy它会根据吞吐和停顿目标动态调整 Eden、Survivor 的大小比例甚至修改晋升阈值。这意味着你手动设的SurvivorRatio可能在运行一段时间后被它悄悄改掉。我遇到过一次配置里明明写着SurvivorRatio8实际跑起来 Survivor 比例只有 4 左右查了半天才发现是自适应策略干的。要固定就得加-XX:-UseAdaptiveSizePolicy代价是失去自动调优能力。5.3 收集器差异新生代参数在不同实现下的生效范围不同收集器对参数的态度差别很大。Serial 与 ParNew年轻代用复制算法Survivor 结构清晰PretenureSizeThreshold有效SurvivorRatio有效参数行为最“老实”适合作为理解模型的教学样本。Parallel Scavenge同为复制算法但它是吞吐优先的默认开启自适应。它不认PretenureSizeThresholdMaxGCPauseMillis设了会牺牲吞吐比较适合后台批处理这类不在乎单次停顿的场景。G1新生代不再是一整块连续区域而是由若干 Region 拼成Eden Region 数量动态变化。回收时存活对象被复制到空闲 Region 里可能是新的 Survivor Region也可能直接是老年代 Region。SurvivorRatio在 G1 下语义变弱更常用的是-XX:G1NewSizePercent默认 5和-XX:G1MaxNewSizePercent默认 60配合-XX:MaxGCPauseMillis默认 200让 G1 自己去调节新生代规模。所以从 Parallel 迁到 G1 时把-Xmn原封不动搬过去是最常见的错误之一。6. 现场诊断把一次 Young GC 看清楚的三种手段参数怎么调最终要靠数据说话。新生代的问题在现场排查时有三个基本工具jstat看趋势GC 日志看单次细节堆转储看对象构成。三者配合基本能定位 90% 的问题。6.1 jstat 的读法与分配速率的估算方法jstat -gc pid 1000每秒打一行长期观察就能看出规律。关键列是这些列名含义关注点S0C/S1C两个 Survivor 的容量看是否与预期一致能暴露自适应策略改动S0U/S1U两个 Survivor 的使用量有值的一侧是当前 From/ToEC/EUEden 容量与使用量EU 增长速率就是分配速率OC/OU老年代容量与使用量OU 的台阶式上涨对应晋升YGC/YGCT新生代回收次数与总耗时平均单次 YGCT / YGCFGC/FGCT全堆回收次数与总耗时任何上涨都要警惕估算分配速率的方法很直接取两次采样用(EU2 - EU1) / 时间间隔得到 Eden 的增长速度。注意如果两次采样之间发生过 YGCEU 会掉回接近 0这时候要用 YGC 次数变化来校正。更稳的办法是直接看 Eden 容量除以 YGC 间隔因为每次 YGC 基本意味着 Eden 被填满了一轮这个值虽然略高估但趋势判断够用。jstat -gcutil pid 1000更直观直接给百分比。但要注意它的S0和S1列在 G1 下的含义有细微差别而且百分比会掩盖容量问题比如 Survivor 使用率 100% 究竟是因为它就 50M 还是因为它有 500M得配合-gc一起看。6.2 日志参数配置与自动化分析GC 日志必须开这是底线。JDK 8 上的配置-XX:PrintGCDetails -XX:PrintGCDateStamps -XX:PrintGCTimeStamps -XX:PrintHeapAtGC -Xloggc:/data/logs/gc-%t.log -XX:UseGCLogFileRotation -XX:NumberOfGCLogFiles5 -XX:GCLogFileSize50MJDK 9 之后换成了统一日志框架-Xlog:gc*,gcheapdebug,gcagetrace:file/data/logs/gc.log:time,uptime,level,tags:filecount5,filesize50Mgcagetrace这个标签特别有用它会打印每个年龄段的对象分布能直接看出动态年龄判定在哪个年龄触发。JDK 8 上没有这个只能靠PrintHeapAtGC输出的 Survivor 使用量反推。日志量大的时候手工看不过来用grep加awk做个粗统计就够grep Allocation Failure gc.log | awk -F- {print $2} | awk {print $1} | sort -n | tail -20这条命令能拉出晋升后老年代占用最高的若干次快速定位异常轮次。更进一步把每次的real后面的数值抽出来排序就能找到停顿最长的几次针对性分析。6.3 常见现象与排查方向速查表现象典型根因验证方式处理方向YGC 极频繁每次回收量很小Eden 过小或分配速率过高jstat看 EC 与 YGC 间隔增大 Eden或减少临时对象YGC 后老年代持续上涨晋升过快动态年龄判定提前触发看 Survivor 是否需要扩容用gcage确认年龄分布调小SurvivorRatio或增大 SurvivorYGC 耗时明显变长存活对象多复制量大日志里回收前后新生代差值排查是否有大对象长期滞留新生代频繁 Promotion FailedTo Survivor 装不下担保失败日志出现 Full GC 紧跟 YGC增大 Survivor或提前让对象进老年代Survivor 使用率长期 100%Survivor 过小或被自适应策略压缩jstat对比 S0C 与预期值关闭自适应固定SurvivorRatio设了PretenureSizeThreshold无效收集器不支持该参数确认收集器类型换 ParNew 或用其他方式限制大对象7. 我在实际项目里踩过的坑和几条经验盲目调大新生代结果 YGC 变慢。有个服务原来新生代 512MYGC 间隔 800ms同事觉得太频繁一口气把-Xmn调到 4G。间隔确实拉到了 6 秒但单次 YGC 从 8ms 涨到了 45ms尾延迟指标明显变差。原因很简单Eden 变大之后两次回收之间积累的对象变多存活绝对值上升复制量跟着涨。后来折中到 1.5G间隔 2.5 秒单次 15ms 左右才算平衡。这件事让我记住一条YGC 间隔和单次停顿是跷跷板很少能同时优化得先看业务更在意哪个。SurvivorRatio 调大反而害了新生代。这个参数名字很有欺骗性很多人以为“比例调大就是给 Survivor 更多空间”实际正好相反。SurvivorRatio16意味着 Eden 是单个 Survivor 的 16 倍Survivor 被压缩得很小。有个服务为了“让 Eden 更大”把它从 8 改成 16结果 Survivor 只剩 50M动态年龄判定在年龄 2 就触发对象两轮就进老年代老年代使用率肉眼可见地爬升最后靠调回 8 再加-XX:-UseAdaptiveSizePolicy才稳住。自适应策略悄悄改了配置。这个问题最隐蔽。配置文件里写得好好的参数运行时通过jstat一看完全不是那么回事。排查思路是先加-XX:PrintFlagsFinal | grep SurvivorRatio看 JVM 启动时实际接受的值再看运行一段时间后的S0C是否变化。如果变了基本就是UseAdaptiveSizePolicy在动加了-XX:-UseAdaptiveSizePolicy就老实了。关注晋升量的绝对值而不只是比例。监控面板上“晋升速率”往往显示百分比看起来不高一看绝对值每秒 200MB老年代撑不了多久。我的习惯是每次 YGC 之后记一下老年代的增量连续观察十几轮如果增量稳定在某个值附近可以据此推算 Full GC 大概多久出现如果增量在往上走说明有对象寿命在变长可能是缓存没设过期也可能是连接池对象堆积值得顺着查下去。别在没数据的时候改参数。这话听着像废话但我见过太多次“别人说-Xmn设成堆的 1/3 好”就直接照搬。新生代该给多少取决于两件事每秒分配速率和存活对象占比。前者靠jstat估后者靠 GC 日志里回收前后的差值算。这两个数拿到手参数区间基本就出来了剩下的就是微调验证。没有这两个数改参数和抽签没区别。
延伸阅读

更多相关文章

2026/10/23 11:10:24

D2503A高效降压型DC-DC转换器,助力电源设计新突破

在现代电子设备中,电源管理芯片扮演着至关重要的角色。它不仅关系到设备的稳定运行,还直接影响着能效和性能表现。高性能降压型DC-DC转换器——D2503A。凭借其卓越的性能和广泛的应用场景,D2503A将成为您电源设计中的理想选择。一、产品简介D…

2026/10/23 12:06:04

Java进程终止失败?从端口占用到优雅停机的排查与根治

你正在开发一个Java后端服务,改完代码满怀期待地点击“Rerun”,结果开发工具没有像往常一样直接重启,而是弹出一个英文报错:couldnt terminate the existing process for 8080。接下来不管你怎么点停止按钮,界面上的小…

2026/10/23 12:06:04

MATLAB压缩感知图像重建:512×512 Lena图15%采样实现高保真重构

简介:本资源是一套面向数字图像处理、计算机视觉及信号处理方向学习者与研究者的二维图像压缩感知(CS)重构算法MATLAB实现代码,聚焦稀疏信号理论在图像重建中的落地应用,适用于高年级本科生、研究生及工程实践人员理解…

2026/10/23 12:06:04

米散射MATLAB实现:从气溶胶到颗粒检测的五个光学参量计算

简介:这份MATLAB程序面向光学、大气科学与遥感领域的学习者与科研人员,提供经典米散射(MIE散射)理论的数值实现,用于计算球形粒子在光照下的散射系数、吸收系数、消光系数、后向散射系数及不对称因子等宏观光学参数。资…

2026/10/23 12:06:04

Codex 自动化连续失败别盲目重跑:先固定原 ID、额度和恢复条件

Codex 自动化连续失败别盲目重跑:先固定原 ID、额度和恢复条件 自动化在保存或提交后超时,最危险的反应是立即再点一次。对会创建外部记录、扣减额度或发送通知的任务,应先把已知身份和未知状态落盘,再进入只读对账。这样恢复时能沿原对象继续,而不会制造重复结果。 三个…

2026/10/23 12:01:02

ZooKeeper监控Dubbo Provider下线:邮件预警系统设计与实现

简介:这是一套基于Zookeeper监控Dubbo Provider下线的工程源码,面向分布式系统运维与Java服务端开发者,解决Dubbo服务异常下线难以及时感知的问题。工程采用独立部署、代码零入侵设计,无需改动原有Provider与Consumer代码&#xf…

2026/10/22 11:29:28

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/23 0:02:26

Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战

说实话,这几年接触了不少想入门FPGA的朋友,如果你问哪个项目能一口气把Verilog的核心基本功串起来,我通常会推荐电子密码锁。这个项目看着不起眼,却把矩阵键盘扫描、按键消抖、状态机设计、动态显示刷新、外设控制这些数字逻辑里的…

2026/10/23 0:02:26

数据服务监控运维实战:从指标体系到告警治理与故障排查

周末凌晨两点,手机震了。第一反应是“数据服务挂了”。打开电脑看了下,果然,某张核心报表的数据产出时间已经超过SLA两小时,下游三个业务方连着发来问询。这种场景,做过大数据运维的人都不会陌生。数据服务的监控与运维…

2026/10/23 0:02:26

FreeRTOS任务通知实战详解:内核原理、API与避坑指南

裸机编程切到RTOS之后,任务之间的通信和同步永远是躲不开的坎。前面聊过队列、信号量、互斥锁、事件组,这次把FreeRTOS任务通知(Task Notification)单独拎出来讲透。原因很简单:任务通知是FreeRTOS里最容易用错、也最容…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑