从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
个人源码地址: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
有没有办法解决呢?有。关闭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究竟对内存做了什么?