垃圾收集器知识点总结-MaxTenuringThreshold

本文详细探讨了Java垃圾收集器中的MaxTenuringThreshold参数,解释了其实际含义并非对象必须经过15次GC后晋升,而是最大阈值。当对象在Survivor空间中达到一定比例时,阈值会动态调整,导致对象提前晋升到老年代。线上应用频繁GC的问题,通过分析源码和调整策略,揭示了阈值动态变化的原因。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MaxTenuringThreshold该参数用于控制对象经过GC多少次仍然存活后晋升到老年代的最大阈值,参数最大可配置为15,即对象最多经过15次GC后仍然存活就会晋升到老年代

记住!是最大!也就是说实际上不一定会经过15次才能晋升!这个值的JVM内部会进行动态计算然后动态改变的。

最近在线上有个应用GC非常频繁,MaxTenuringThreshold参数配置的是15,而以前对这个参数的理解是一定要经过15次后才会晋升,所以导致整个排查过程根本就不对,线上日志如下:

Desired survivor size 184549376 bytes, new threshold 1 (max 15)
- age   1:        369096960 bytes,        369096960 total
....下面进行GC,年轻代有很多对象全跑到了老年代

当时思考方向是:age为1,离15有很大的距离,不应该会晋升,老年代空间增加是因为大对象

这个思考方向真是大错特错了,因为对MaxTenuringThreshold理解错了。注意日志中有几个关键字

new threshold 1 (max 15)

意思是说阈值现在变成了1,而不是15!所以直接就晋升了!那么为什么会变成1呢,这个动态计算的算法是怎么样的,参考了一下一些大神的文章再看了下源码,终于发现原因所在,直接看源码会比较清晰,源码如下:

int ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  //TargetSurvivorRatio默认为50
  //desired_survivor_size = survivor的空间*50%
  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
  size_t total = 0;
  int age = 1;
  assert(sizes[0] == 0, "no objects with age zero should be recorded");
  while (age < table_size) {
    // 循环遍历所有年龄代的对象累加得到一个大小
    total += sizes[age];
    // 如果该大小大于desired_survivor_size,即survivor的空间*50%,那么退出循环
    if (total > desired_survivor_size) break;
    age++;
  }
  // 如果算出来的age大于MaxTenuringThreshold则使用MaxTenuringThreshold,否则使用计算出来的age
  int result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;

  if (PrintTenuringDistribution || UsePerfData) {
    if (PrintTenuringDistribution) {
      gclog_or_tty->cr();
      // 这里就是线上出现的那个日志所在的地方
      gclog_or_tty->print_cr("Desired survivor size %ld bytes, new threshold %d (max %d)",
        desired_survivor_size*oopSize, result, MaxTenuringThreshold);
    }
  //....
  }
  // 返回计算的年龄
  return result;
}

经过多次GC,存活的对象会在From和To两个地方来回的存放,而前提是两个空间能有足够的大小去存放这些数据,上述算法中,会计算每个年龄的大小,如果达到某个年龄后发现总大小以及大于Survivor的大小的50%,那么这时候就需要调整阈值,不能继续等到15次GC后才晋升,这样会导致Survivor的空间不足,所以调整阈值,让其尽快晋升

回到线上的那个例子,线上我记得是没配置TargetSurvivorRatio属性,那么按照50%算,survivor * 50%=184549376,在while循环中计算第一个年龄代=369096960>184549376,那么退出循环,此时计算得到的年龄为1,所以这个年龄的对象会提前晋升

参考文章:
https://www.jianshu.com/p/f91fde4628a5
https://blog.youkuaiyun.com/FoolishAndStupid/article/details/77596050?fps=1&locationNum=1

<think>好的,用户之前问了关于Minor GC和Young GC的区别,现在他们又接着问与CMS并用的年轻代垃圾收集器通常是哪一个。我需要先确认用户的知识背景,可能他们已经了解CMS是老年代收集器,但需要明确年轻代的搭配。 首先,回顾CMS(Concurrent Mark Sweep)收集器的特点。CMS主要用于老年代的垃圾回收,以降低停顿时间为目标,采用并发标记清除算法。但CMS自身并不处理年轻代的垃圾回收,所以必须搭配一个年轻代的收集器。 接下来,我需要确定通常与CMS搭配的年轻代收集器。常见的组合是ParNew收集器,因为ParNew是Serial收集器的多线程版本,专门用于年轻代,且能够与CMS配合工作。这一点在Java 8及之前版本中是标准配置。用户可能在使用较旧的系统或特定版本的Java,所以需要指出版本兼容性,比如在JDK 9之后CMS被弃用,所以搭配可能变化。 然后,考虑是否有其他可能性,比如Serial收集器,但Serial是单线程的,通常不会在高并发场景下与CMS搭配使用。Parallel Scavenge收集器虽然也是年轻代的,但由于其设计目标(吞吐量优先)与CMS(低延迟)不兼容,所以一般不会一起使用。需要解释为什么Parallel Scavenge不适合,避免用户混淆。 用户可能的问题还包括为什么选择ParNew而不是其他收集器,这时候需要说明ParNew和CMS的兼容性,以及它们的工作模式如何互补。同时,需要提到启动参数,比如使用-XX:+UseConcMarkSweepGC会自动选择ParNew作为年轻代收集器,这样用户知道如何配置。 还要注意用户可能的误区,比如在较新的JDK版本中CMS已经不再推荐使用,转而使用G1或ZGC等,所以是否需要提及这一点?根据用户的问题,他们可能还在使用旧系统,所以可以简要提到版本变化的影响,但主要回答还是围绕传统的搭配。 最后,总结关键点,明确回答ParNew是通常的选择,并解释原因,同时对比其他可能的收集器为何不适用,确保用户全面理解。</think>--- ### **一、核心答案** 与 **CMS(Concurrent Mark-Sweep)收集器** 搭配使用的年轻代垃圾收集器通常是 **ParNew收集器**。这是JVM中经典的“**分代组合**”模式: - **年轻代**:ParNew(多线程复制算法) - **老年代**:CMS(并发标记清除算法) --- ### **二、为什么选择ParNew?** 1. **设计兼容性** - ParNew是 **Serial收集器的多线程版本**,专为年轻代设计,其内存布局和回收策略(复制算法)与CMS对老年代的管理无缝衔接。 - CMS需要与支持 **“同步标记”(同步STW阶段)** 的年轻代收集器配合,而ParNew满足这一要求。 2. **多线程优势** - ParNew通过多线程并行回收年轻代,显著减少GC停顿时间,与CMS的低延迟目标一致。 - 对比单线程的Serial收集器,ParNew更适合多核服务器环境。 3. **参数联动** - 启用CMS时(`-XX:+UseConcMarkSweepGC`),JVM默认自动选择ParNew作为年轻代收集器。 - 示例启动参数: ```bash java -XX:+UseConcMarkSweepGC -XX:+UseParNewGC ... ``` --- ### **三、与其他收集器的兼容性对比** | 年轻代收集器 | 能否与CMS搭配? | 原因 | |--------------------|-----------------|----------------------------------------------------------------------| | **Serial** | ❌ 否 | 单线程回收,与CMS的并发目标冲突 | | **Parallel Scavenge** | ❌ 否 | 设计目标(高吞吐量)与CMS(低延迟)冲突,且内存布局不兼容 | | **G1** | ❌ 否 | G1是整堆回收设计,与CMS分代模型不兼容 | | **ParNew** | ✅ 是 | 多线程、分代复制算法,与CMS协同工作 | --- ### **四、工作流程示例(ParNew + CMS)** 1. **年轻代回收(ParNew)** - **触发条件**:Eden区空间不足。 - **执行过程**: - 暂停用户线程(STW),多线程复制存活对象到Survivor区或晋升至老年代。 - 存活对象年龄超过阈值(`-XX:MaxTenuringThreshold`)直接进入老年代。 2. **老年代回收(CMS)** - **触发条件**:老年代占用达到阈值(`-XX:CMSInitiatingOccupancyFraction`)。 - **执行过程**: - 初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除。 --- ### **五、注意事项** 1. **JDK版本限制** - **JDK 9+** 中CMS已被标记为废弃(Deprecated),JDK 14+ 中彻底移除。 - 替代方案:使用 **G1**(`-XX:+UseG1GC`)或 **ZGC**(`-XX:+UseZGC`)等现代收集器。 2. **内存碎片问题** - CMS采用标记-清除算法,可能产生内存碎片,需通过参数 `-XX:+UseCMSCompactAtFullCollection` 在Full GC时压缩内存。 3. **晋升失败风险** - 若年轻代晋升速度过快,而CMS无法及时回收老年代空间,可能触发Full GC(Serial Old),导致长暂停。 --- ### **六、总结** - **ParNew是CMS的“黄金搭档”**:两者协同实现年轻代多线程低停顿、老年代并发回收的低延迟目标。 - **适用场景**:对延迟敏感、老年代对象存活周期较长的应用(如Web服务)。 - **演进趋势**:随着CMS的淘汰,建议迁移至G1/ZGC等更先进的收集器。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值