jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址:https://github.com/FlashLightNing/openjdk-notes
    还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
    jvm源码阅读笔记[1]:如何触发一次CMS回收
    jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
    jvm源码阅读笔记[3]:从内存分配到触发GC的细节
    jvm源码阅读笔记[4]:从GC说到vm operation
    jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

    大家都知道年轻代中经历了多次GC之后仍然没有被回收的对象就会晋升到老年代中。它引入了一个“年龄”的状态:每经历一次GC,对象的年龄就会+1。而到了某一时刻,如果对象的年龄大于设置的一个晋升的阈值,该对象就会晋升到老年代中。对象的年龄最大只能是15,因为jvm中使用4个字节来表示对象的年龄。
    那么,问题来了,阈值都是怎么计算的呢?《深入理解Java虚拟机》中告诉我们,有这么一个参数:

-XX:MaxTenuringThreshold=6

表示设置对象最多经过6次GC而没有被回收的话,就会晋升到老年代中。
    但是正如这个参数中的max,它只是设置最大的阈值。而在运行的过程中,虚拟机会动态计算晋升的阈值。那么,它又是怎么来计算的呢?来看看下面的代码:
(地址:https://github.com/FlashLightNing/openjdk-notes/blob/master/src/share/vm/gc_implementation/shared/ageTable.cpp)
    

uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  //TargetSurvivorRatio默认50,意思是:在回收之后希望survivor区的占用率达到这个比例
  size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
  size_t total = 0;
  uint age = 1;
  assert(sizes[0] == 0, "no objects with age zero should be recorded");
  while (age < table_size) {//table_size=16
    total += sizes[age];
    //如果加上这个年龄的所有对象的大小之后,占用量>期望的大小,就设置age为新的晋升阈值
    if (total > desired_survivor_size) break;
    age++;
  }

  uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;

//可以看到加了PrintTenuringDistribution参数,会打印age分布图
  if (PrintTenuringDistribution || UsePerfData) {

    //打印期望的survivor的大小以及新计算出来的阈值,和设置的最大阈值
    if (PrintTenuringDistribution) {
      gclog_or_tty->cr();
      gclog_or_tty->print_cr("Desired survivor size " SIZE_FORMAT " bytes, new threshold %u (max %u)",
        desired_survivor_size*oopSize, result, (int) MaxTenuringThreshold);
    }

    total = 0;
    age = 1;
    while (age < table_size) {
      total += sizes[age];
      if (sizes[age] > 0) {
        if (PrintTenuringDistribution) {//打印各个年龄所占的空间
          gclog_or_tty->print_cr("- age %3u: " SIZE_FORMAT_W(10) " bytes, " SIZE_FORMAT_W(10) " total",
                                        age,    sizes[age]*oopSize,          total*oopSize);
        }
      }
      if (UsePerfData) {
        _perf_sizes[age]->set_value(sizes[age]*oopSize);
      }
      age++;
    }
    if (UsePerfData) {
      SharedHeap* sh = SharedHeap::heap();
      CollectorPolicy* policy = sh->collector_policy();
      GCPolicyCounters* gc_counters = policy->counters();
      gc_counters->tenuring_threshold()->set_value(result);
      gc_counters->desired_survivor_size()->set_value(
        desired_survivor_size*oopSize);
    }
  }

  return result;//最终返回计算的阈值
}

    
    所以呢,代码是不会欺骗你的。从所有年龄=0的对象占的空间开始累加,如果加上年龄=n的所有对象的空间之后,大于survivor区的大小*TargetSurvivorRatio/100(TargetSurvivorRatio默认值为50),若大于这个大小,则结束循环,将n和MaxTenuringThreshold比较,若n小,则阈值为n。若n大,则只能去设置的最大阈值MaxTenuringThreshold。
    
    好,接下来让我们测试一下
    参数及代码:

参考:http://www.jianshu.com/p/f91fde4628a5
-verbose:gc -Xmx200M -Xmn50M -XX:TargetSurvivorRatio=60  -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxTenuringThreshold=6 

    打印的GC日志截图:
    

    
    可以看到,阈值一开始是6,后面变成了2,然后又变成了6,最后又变成了4。
    让我们来计算一下是不是这样呢?
    年轻代是50m,然后eden和survivor区的比例默认是8:1:1.所以计算出来的survivor区的size=50*1024*1024*0.1=5242880byte。
    图中打印的Desired survivor size=3145728byte.
    计算出来的:5242880byte*TargetSurvivorRatio/100=5242880*60/100=3145728!
    正好相等,没有一点点偏差!!
    接下来算一下为什么第2次GC的时候,阈值变成2了呢?
    age=1的大小+age=2的大小=2622024+2115896=4737920>3145728(Desired survivor size)。所以参考上面的代码,阈值=2。
再来看第3次为什么=6了?
    第3次GC时,只有一个age=3的对象,所占大小=2622024<3145728。所以在循环添加时会一直到age最大值15。注意是个while循环和break的条件。

while (age < table_size) {//table_size=16
    total += sizes[age];
    //如果加上这个年龄的所有对象的大小之后,占用量>期望的大小,就设置age为新的晋升阈值
    if (total > desired_survivor_size) break;
    age++;
  }

    
    然后又根据以下代码:
    

uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;

    所以阈值变成了6。
    后面的就不一一分析了。
    在这里,总结一下就是:

年轻代晋升到老年代的年龄阈值是由参数TargetSurvivorRatio和参数MaxTenuringThreshold影响的。
MaxTenuringThreshold决定了晋升的最大阈值。
TargetSurvivorRatio影响运行过程中虚拟机计算的晋升阈值。
假设晋升阈值为n,那么一定满足以下条件:所有年龄<=n的对象大小之和 < survivor区的大小*TargetSurvivorRatio%

    不知道说到这里,我讲清楚没有…

    但是,这种调整晋升阈值的方式只适用于defNew,parNew年轻代收集器。对于ParallelScavenge收集器,还另有一番天地。
(地址:https://github.com/FlashLightNing/openjdk-notes/blob/master/src/share/vm/gc_implementation/parallelScavenge/psAdaptiveSizePolicy.cpp)
    PSAdaptiveSizePolicy.cpp中有这样一段计算晋升阈值的代码

    
    以及这么一段加减的代码:
    

    可以看到,对于ps来说,调整晋升的阈值是通过判断收集器young gc和full gc的耗时的。如果young gc太频繁,虚拟机认为可以降低晋升阈值从而减少年轻代的对象,让更多的对象晋升到老年代,达到减少young gc的时间。
    反之,如果full gc太频繁了,虚拟机就提高晋升的阈值,减少晋升到老年代的对象,从而控制full gc的时间。
_threshold_tolerance_percent=1.0 + ThresholdTolerance/100.0.
ThresholdTolerance默认值为10.也就是说

young_gc_time>full_gc_time*1.1,则threshold降低。
full_gc_time>young_gc_time*1.1,则threshold提高。

    但是需要注意的坑是:如果你的应用在没有用PS之前只是young gc频繁,而基本没有full gc的话,在用了PS之后,因为young gc频繁(比如1秒钟或者几秒钟一次)而导致该阈值会不断减少,最终减到1.这会导致什么问题呢?所有年龄>1的对象都会晋升到老年代,引起没必要的full gc。也许只有等到full gc很频繁之后,才会提高晋升的阈值。
    参考这篇文章:http://blog.csdn.net/lirenzuo/article/details/77529025
jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解_第1张图片
    有没有办法解决呢?有。关闭UseAdaptiveSizePolicy即可。

-XX:-UseAdaptiveSizePolicy

    顺便要说的是,如果用的ps,那么因为没有用ageTable里面的那种计算方法,就算加了PrintTenuringDistribution也就不会打印对象age分布图。而在用CMS的时候,加上该参数,因为年轻代用的parNew收集器,阈值计算用的是ageTable的算法,所以会打印age分布图。还有一点是,在cms下,UseAdaptiveSizePolicy参数是默认关闭的。而在PS下是打开的。
    

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址:https://github.com/FlashLightNing/openjdk-notes
    还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk
    jvm源码阅读笔记[1]:如何触发一次CMS回收
    jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解
    jvm源码阅读笔记[3]:从内存分配到触发GC的细节
    jvm源码阅读笔记[4]:从GC说到vm operation
    jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

你可能感兴趣的:(jvm,openjdk,源码,阅读,jvm,垃圾回收)