本部分,我们将关注堆(heap) 中一个主要区域,新生代(young generation)。首先我们会讨论为什么调整新生代的参数会对应用的性能如此重要,接着我们将学习新生代相关的jvm参数。
单纯从jvm的功能考虑,并不需要新生代,完全可以针对整个堆进行操作。新生代存在的唯一理由是优化垃圾回收(gc)的性能。更具体说,把堆划分为新生代和老年代有2个好处:简化了新对象的分配(只在新生代分配内存),可以更有效的清除不再需要的对象(即死对象)(新生代和老年代使用不同的gc算法)
通过广泛研究面向对象实现的应用,发现一个共同特点:很多对象的生存时间都很短。同时研究发现,新生对象很少引用生存时间长的对象。结合这2个特点,很明显 gc 会频繁访问新生对象,例如在堆中一个单独的区域,称之为新生代。在新生代中,gc可以快速标记回收”死对象”,而不需要扫描整个heap中的存活一段时间的”老对象”。
sun/oracle 的hotspot jvm 又把新生代进一步划分为3个区域:一个相对大点的区域,称为”伊甸园区(eden)”;两个相对小点的区域称为”from 幸存区(survivor)”和”to 幸存区(survivor)”。按照规定,新对象会首先分配在 eden 中(如果新对象过大,会直接分配在老年代中)。在gc中,eden 中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过gc的次数),会被移动到老年代。
基于大多数新生对象都会在gc中被收回的假设。新生代的gc 使用复制算法。在gc前to 幸存区(survivor)保持清空,对象保存在 eden 和 from 幸存区(survivor)中,gc运行时,eden中的幸存对象被复制到 to 幸存区(survivor)。针对 from 幸存区(survivor)中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold),对象会被复制到to 幸存区(survivor)。如果达到阀值对象被复制到老年代。复制阶段完成后,eden 和from 幸存区中只保存死对象,可以视为清空。如果在复制过程中to 幸存区被填满了,剩余的对象会被复制到老年代中。最后 from 幸存区和 to幸存区会调换下名字,在下次gc时,to 幸存区会成为from 幸存区。

<a href="https://blog.codecentric.de/files/2011/08/young_gc.png" target="_blank">https://blog.codecentric.de/files/2011/08/young_gc.png</a>
上图演示gc过程,黄色表示死对象,绿色表示剩余空间,红色表示幸存对象
总结一下,对象一般出生在eden区,年轻代gc过程中,对象在2个幸存区之间移动,如果对象存活到适当的年龄,会被移动到老年代。当对象在老年代死亡时,就需要更高级别的gc,更重量级的gc算法(复制算法不适用于老年代,因为没有多余的空间用于复制)
现在应该能理解为什么新生代大小非常重要了(译者,有另外一种说法:新生代大小并不重要,影响gc的因素主要是幸存对象的数量),如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。我们需要找到一个合适大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优。这就需要jvm参数了
-xx:newsize and -xx:maxnewsize
就像可以通过参数(-xms and -xmx) 指定堆大小一样,可以通过参数指定新生代大小。设置 xx:maxnewsize 参数时,应该考虑到新生代只是整个堆的一部分,新生代设置的越大,老年代区域就会减少。一般不允许新生代比老年代还大,因为要考虑gc时最坏情况,所有对象都晋升到老年代。(译者:会发生oom错误) -xx:maxnewsize 最大可以设置为-xmx/2 .
考虑性能,一般会通过参数 -xx:newsize 设置新生代初始大小。如果知道新生代初始分配的对象大小(经过监控) ,这样设置会有帮助,可以节省新生代自动扩展的消耗。
-xx:newratio
可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数 -xx:newratio 设置老年代与新生代的比例。例如 -xx:newratio=3 指定老年代/新生代为3/1. 老年代占堆大小的 3/4 ,新生代占 1/4 .
如果针对新生代,同时定义绝对值和相对值,绝对值将起作用。下面例子:
<code>$ java -xx:newsize=32m -xx:maxnewsize=512m -xx:newratio=3 myapp</code>
以上设置, jvm 会尝试为新生代分配四分之一的堆大小,但不会小于32mb或大于521mb
在设置新生代大小问题上,使用绝对值还是相对值,不存在通用准则 。如果了解应用的内存使用情况,设置固定大小的堆和新生代更有利,当然也可以设置相对值。如果对应用的内存使用一无所知,正确的做法是不要设置任何参数,如果应用运行良好。很好,我们不用做任何额外动作.如果遇到性能或outofmemoryerrors, 在调优之前,首先需要进行一系列有目的的监控测试,缩小问题的根源。
-xx:survivorratio
参数 -xx:survivorratio 与 -xx:newratio 类似,作用于新生代内部区域。-xx:survivorratio 指定伊甸园区(eden)与幸存区大小比例. 例如, -xx:survivorratio=10 表示伊甸园区(eden)是 幸存区to 大小的10倍(也是幸存区from的10倍).所以,伊甸园区(eden)占新生代大小的10/12, 幸存区from和幸存区to 每个占新生代的1/12 .注意,两个幸存区永远是一样大的..
设定幸存区大小有什么作用? 假设幸存区相对伊甸园区(eden)太小, 相应新生对象的伊甸园区(eden)永远很大空间, 我们当然希望,如果这些对象在gc时全部被回收,伊甸园区(eden)被清空,一切正常.然而,如果有一部分对象在gc中幸存下来, 幸存区只有很少空间容纳这些对象.结果大部分幸存对象在一次gc后,就会被转移到老年代 ,这并不是我们希望的.考虑相反情况, 假设幸存区相对伊甸园区(eden)太大,当然有足够的空间,容纳gc后的幸存对象. 但是过小的伊甸园区(eden),意味着空间将越快耗尽,增加新生代gc次数,这是不可接受的。
总之,我们希望最小化短命对象晋升到老年代的数量,同时也希望最小化新生代gc 的次数和持续时间.我们需要找到针对当前应用的折中方案, 寻找适合方案的起点是 了解当前应用中对象的年龄分布情况。
-xx:+printtenuringdistribution
参数 -xx:+printtenuringdistribution 指定jvm 在每次新生代gc时,输出幸存区中对象的年龄分布。例如:
<code>desired survivor size 75497472 bytes, new threshold 15 (max 15) - age 1: 19321624 bytes, 19321624 total - age 2: 79376 bytes, 19401000 total - age 3: 2904256 bytes, 22305256 total</code>
第一行说明幸存区to大小为 75 mb. 也有关于老年代阀值(tenuring threshold)的信息, 老年代阀值,意思是对象从新生代移动到老年代之前,经过几次gc(即, 对象晋升前的最大年龄). 上例中,老年代阀值为15,最大也是15.
之后行表示,对于小于老年代阀值的每一个对象年龄,本年龄中对象所占字节 (如果当前年龄没有对象,这一行会忽略). 上例中,一次 gc 后幸存对象大约 19 mb, 两次gc 后幸存对象大约79 kb , 三次gc 后幸存对象大约 3 mb .每行结尾,显示直到本年龄全部对象大小.所以,最后一行的 total 表示幸存区to 总共被占用22 mb . 幸存区to 总大小为 75 mb ,当前老年代阀值为15,可以断定在本次gc中,没有对象会移动到老年代。现在假设下一次gc 输出为:
<code>desired survivor size 75497472 bytes, new threshold 2 (max 15) - age 1: 68407384 bytes, 68407384 total - age 2: 12494576 bytes, 80901960 total - age 3: 79376 bytes, 80981336 total - age 4: 2904256 bytes, 83885592 total</code>
对比前一次老年代分布。明显的,年龄2和年龄3 的对象还保持在幸存区中,因为我们看到年龄3和4的对象大小与前一次年龄2和3的相同。同时发现幸存区中,有一部分对象已经被回收,因为本次年龄2的对象大小为 12mb ,而前一次年龄1的对象大小为 19 mb。最后可以看到最近的gc中,有68 mb 新对象,从伊甸园区移动到幸存区。
注意,本次gc 幸存区占用总大小 84 mb -大于75 mb. 结果,jvm 把老年代阀值从15降低到2,在下次gc时,一部分对象会强制离开幸存区,这些对象可能会被回收(如果他们刚好死亡)或移动到老年代。
-xx:initialtenuringthreshold, -xx:maxtenuringthreshold and -xx:targetsurvivorratio
参数 -xx:+printtenuringdistribution 输出中的部分值可以通过其它参数控制。通过 -xx:initialtenuringthreshold 和 -xx:maxtenuringthreshold 可以设定老年代阀值的初始值和最大值。另外,可以通过参数 -xx:targetsurvivorratio 设定幸存区的目标使用率.例如 , -xx:maxtenuringthreshold=10 -xx:targetsurvivorratio=90 设定老年代阀值的上限为10,幸存区空间目标使用率为90%。
有多种方式,设置新生代行为,没有通用准则。我们必须清楚以下2中情况:
1 如果从年龄分布中发现,有很多对象的年龄持续增长,在到达老年代阀值之前。这表示 -xx:maxtenuringthreshold 设置过大
2 如果 -xx:maxtenuringthreshold 的值大于1,但是很多对象年龄从未大于1.应该看下幸存区的目标使用率。如果幸存区使用率从未到达,这表示对象都被gc回收,这正是我们想要的。 如果幸存区使用率经常达到,有些年龄超过1的对象被移动到老年代中。这种情况,可以尝试调整幸存区大小或目标使用率。
-xx:+nevertenure and -xx:+alwaystenure
最后,我们介绍2个颇为少见的参数,对应2种极端的新生代gc情况.设置参数 -xx:+nevertenure , 对象永远不会晋升到老年代.当我们确定不需要老年代时,可以这样设置。这样设置风险很大,并且会浪费至少一半的堆内存。相反设置参数 -xx:+alwaystenure, 表示没有幸存区,所有对象在第一次gc时,会晋升到老年代。
没有合理的场景使用这个参数。可以在测试环境中,看下这样设置会发生什么有趣的事.但是并不推荐使用这些参数.
结论
适当的配置新生代非常重要,有相当多的参数可以设置新生代。然而,单独调整新生代,而不考虑老年代是不可能优化成功的。当调整堆和gc设置时,我们总是应该同时考虑新生代和老年代。
在本系列的下面2部分,我们将讨论 hotspot jvm 中老年代 gc 策略,我们会学习“吞吐量gc收集器” 和 “并发低延迟gc收集器”,也会了解收集器的基本准则,算法和调整参数.