年轻代垃圾回收及避免进行Full GC
年轻代垃圾回收及避免进行Full GC
一、介绍
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域,如图2-1所示。
图2-1 Java虚拟机运行时数据区
本部分,我们将关注堆(heap) 中一个主要区域,新生代(young generation)。首先我们会讨论为什么调整新生代的参数会对应用的性能如此重要,接着我们将学习新生代相关的JVM参数。
首选我们来看下堆内存的划分:
堆内存被划分为年轻代和老年代,年轻代又被细分为Eden分和Survivor1区和Survivor2区。从年轻代回收内存被称为 Minor GC,对老年代GC称为Major GC, 而Full GC是对整个堆即年轻代和老年代来说的,当年轻代和老年代都爆满时,就会触发Full GC,当然Full 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 survivor区中只保存死对象,可以视为清空。如果在复制过程中To survivor区被填满了,剩余的对象会被复制到老年代中。最后 From survivor区和 Tosurvivor区会调换下名字,在下次GC时,To survivor区会成为From survivor区。
下图演示GC过程,黄色表示死对象,绿色表示剩余空间,红色表示幸存对象
总结一下,对象一般出生在Eden区,年轻代GC过程中,对象在2个幸存区之间移动,如果对象存活到适当的年龄,会被移动到老年代。当对象在老年代死亡时,就需要更高级别的GC,更重量级的GC算法(复制算法不适用于老年代,因为没有多余的空间用于复制)。
现在应该能理解为什么年轻代大小非常重要了,如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。我们需要找到一个合适大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优。这就需要JVM参数了。
三、JVM参数调优
1. -XX:NewSize and -XX:MaxNewSize
就像可以通过参数(-Xms and -Xmx) 指定堆大小一样,可以通过参数指定年轻代大小。设置 XX:MaxNewSize 参数时,应该考虑到年轻代只是整个堆的一部分,年轻代设置的越大,老年代区域就会减少。一般不允许年轻代比老年代还大,因为要考虑GC时最坏情况,所有对象都晋升到老年代。 -XX:MaxNewSize 最大可以设置为-Xmx/2 .即年轻代最大可以设置成堆最大值的一半。考虑到性能,一般会通过参数 -XX:NewSize 设置年轻代初始大小。如果知道年轻代初始分配的对象大小(经过监控)
,这样设置会有帮助,可以节省年轻代自动扩展的消耗。
2. -XX:NewRatio
可以设置年轻代和老年代的相对大小。这种方式的优点是年轻代大小会随着整个堆大小动态扩展。参数 -XX:NewRatio 设置老年代与年轻代的比例。例如 -XX:NewRatio=3 指定老年代/年轻代为3/1. 老年代占堆大小的 3/4 ,年轻代占 1/4 .如果针对年轻代,同时定义绝对值和相对值,绝对值将起作用。下面例子:
$ java -XX:NewSize=32m -XX:MaxNewSize=512m -XX:NewRatio=3 MyApp
以上设置, JVM 会尝试为新生代分配四分之一的堆大小,但不会小于32MB或大于521MB。
在设置年轻代大小问题上,使用绝对值还是相对值,不存在通用准则 。如果了解应用的内存使用情况,设置固定大小的堆和年轻代更有利,当然也可以设置相对值。如果对应用的内存使用一无所知,正确的做法是不要设置任何参数,如果应用运行良好。很好,我们不用做任何额外动作.如果遇到性能或OutOfMemoryErrors, 在调优之前,首先需要进行一系列有目的的监控测试,缩小问题的根源。
3. -XX:SurvivorRatio
参数 -XX:SurvivorRatio 与 -XX:NewRatio 类似,作用于年轻代内部区域。-XX:SurvivorRatio 指定Eden与Survivor大小比例. 例如, -XX:SurvivorRatio=10 表示Eden是To Survivor大小的10倍(也是From Survivor的10倍).所以,Eden占年轻代大小的10/12,From Survivor和To Survivor 各占年轻代的1/12 .注意,两个Survivor区永远是一样大的。
设定幸存区大小有什么作用? 假设幸存区相对Eden太小, 相应新生对象的Eden永远很大空间, 我们当然希望,如果这些对象在GC时全部被回收,Eden被清空,一切正常.然而,如果有一部分对象在GC中幸存下来, 幸存区只有很少空间容纳这些对象.结果大部分幸存对象在一次GC后,就会被转移到老年代 ,这并不是我们希望的.考虑相反情况, 假设幸存区相对Eden太大,当然有足够的空间,容纳GC后的幸存对象. 但是过小的Eden意味着空间将越快耗尽,增加年轻代GC次数,这是不可接受的。
总之,我们希望最小化短命对象晋升到老年代的数量,同时也希望最小化年轻代GC 的次数和持续时间.我们需要找到针对当前应用的折中方案, 寻找适合方案的起点是了解当前应用中对象的年龄分布情况。
4. -XX:+PrintTenuringDistribution
参数 -XX:+PrintTenuringDistribution 指定JVM 在每次年轻代GC时,输出幸存区中对象的年龄分布。例如: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
第一行说明To survivor大小为 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 输出为:
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
对比前一次老年代分布。明显的,年龄2和年龄3 的对象还保持在幸存区中,因为我们看到年龄3和4的对象大小与前一次年龄2和3的相同。同时发现幸存区中,有一部分对象已经被回收,因为本次年龄2的对象大小为 12MB ,而前一次年龄1的对象大小为 19 MB。最后可以看到最近的GC中,有68 MB 新对象,从Eden区移动到幸存区。
注意,本次GC 幸存区占用总大小 84 MB -大于75 MB. 结果,JVM 把老年代阀值从15降低到2,在下次GC时,一部分对象会强制离开幸存区,这些对象可能会被回收(如果他们刚好死亡)或移动到老年代。
5. -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的对象被移动到老年代中。这种情况,可以尝试调整幸存区大小或目标使用率。
6. -XX:+NeverTenure and -XX:+AlwaysTenure
最后,我们介绍2个颇为少见的参数,对应2种极端的新生代GC情况.设置参数 -XX:+NeverTenure , 对象永远不会晋升到老年代.当我们确定不需要老年代时,可以这样设置。这样设置风险很大,并且会浪费至少一半的堆内存。相反设置参数 -XX:+AlwaysTenure, 表示没有幸存区,所有对象在第一次GC时,会晋升到老年代。
没有合理的场景使用这个参数。可以在测试环境中,看下这样设置会发生什么有趣的事.但是并不推荐使用这些参数.

浙公网安备 33010602011771号