JVM年轻代Minor GC详解及相关JVM调优

JVM为什么要将堆分为不同的Generation?JVM年轻代的GC是如何进行的?这些与JVM性能调优有什么关系?我们深入探讨...

2020年02月02日

目录


为什么年轻代如此重要

如果仅仅从JVM的实现功能来说,JVM并不需要一个年轻代,对象的创建回收仅仅在Heap当中进行就可以了。将Heap分出来一个年轻代是为了进行更好的GC回收性能优化。具体来说将Heap分为Yong GenerationOld Generation有两个方面的主要目的:

  • 简化了创建对象时分配内存空间的复杂性(仅仅在Yong Generation中为新生成的对象分配空间)。
  • 根据Yong GenerationOld Generation的不同特点,可以分别使用不同的GC算法。

大量的数据表明,Java应用程序都遵循一个共同的特征:

  1. 大部分的Java对象在很年轻的时候就死了。也就是说,一个Java对象在创建之后,在这个应用运行过程中,很多都不会再被引用。
  2. 很少有新生成的对象被已经存活很久的老对象引用。

基于上述两方面的考虑,JVM需要有一个很快速的方法能够获取新生成的对象,比如说一个专门的空间来存这些新的对象。这样,Heap分为Yong GenerationOld Generation,新生成的对象都在Yong Generation中,Old Generation中存放的是长时间存活的对象,JVM的GC算法就可以在Yong Generation中快速回收死亡的对象,而不必要遍历Heap上所有对象一遍。

ORACLE HotSpot JVM进了一步将Yong Generation分为三个子区域:一个相对较大的Eden区域和两个较小的Suvivor区域(分别作为FromTo)。

下图是Heap区域的整个内存划分:

Heap Memory Model


年轻代Minor GC是如何进行的

基于上述的Yong Generation的内存模型,新创建的对象是在Eden上分配内存的(如果新生成的对象足够大,不足以在Eden上分配,该对象会被直接分配到Old Generation中),在第一次Minor GC(对Yong Generation的GC称为Minor GC)中,Eden区存活的对象(Live Object)会被复制到Suvivor区域中,这些对象一直存活在那里直到达到一定的年龄(这里所谓的年龄是指在它们被创建后经历了对少次的GC),然后仍然存活的对象会被复制到Old Generation中。Suvivor区域的目的是让哪些新生成的对象在第一次GC之后存活的稍微长时间一点,这样以便于在它们死的时候能够被回收。

基于大部分的新生对象都会在GC的过程中被回收,JVM对Yong Generation区域采取一种COPY策略(复制算法):

  1. 最开始,EdenFromTo区域都是空的,新生成的对象会分配在Eden中。
  2. 新生成的对象多了以后,第一次Minor GC会将Eden区域中存活的对象复制到From区域中,复制到Suvivor区域中的对象会记录该对象的经历GC的次数,以便标记该对象存活的年龄。
  3. 接下来会触发多次GC,每一次GC会将Eden中存活的对象复制到To区域。From区域中存活对象会根据其年龄决定其去向:如果年龄没有达到GC规定的年龄,则对象也会被复制到To区域中;反之会被复制到Old Generation中。这时候EdenFrom区域可以被清空了(因为里面的对象全部是死对象),只有To去中存在存活的对象(这里面的对象有不同的年龄(或者说经历的不同的GC次数))。
  4. 最后,FromTo区域会互换角色,这样再下次GC中同样会从EdenFrom区域中复制对象,To区域始终保持为空,等待下次GC时接受存活的对象。

Minor GC的触发经常是对象没有地方再分配的时候进行,也就是Eden满的时候。

Minor GC VS Major GC VS Full GC:Minor GC是对Yong Generation进行回收的算法;Major GC是对Old Generation的回收算法,Full GC是对整个Heap进行的回收算法。

下图表示一次Minor GC过程。绿色表示未使用空间,红色表示存活的对象,黄色表示死亡对象。这个示例中假设Suvivor有足够的区域来存放活着的对象。

一次Minor GC过程

总结一下Yong Generation中对象的生命历程:对象的创建一定发生在Eden区中(除非对象太大,Eden区放不下),接下来生命力顽强的对象会在Suvivor去中被复制来复制去,经历了多次GC之后,如果还存活会被复制到Old Generation中,当然,过程中可能会被GC回收。


年轻代相关的JVM性能调优参数

通过上述内容,我们知道Yong Generation的大小对于JVM性能来说是至关重要的:如果Yong Generation过小,新生成的对象很容易就进入到Old Generation中,需要更大的成本来进行GC回收;如果Yong Generation过大,新生成的对象会再Suvivor区域中被复制来复制去,同样会影响JVM的性能。很不幸的是,没有一个固定的标准是最优的,最优策略需要根据Java应用的特点进行不同的测试得出来,这个时候JVM参数就很有用处了。

-XX:NewSize and -XX:MaxNewSize

与设定整个Heap大小的参数(-Xmx & -Xms)一样,同样可以通过参数来设定Yong Generation大小的最大和最小限制。当我们设定-XX:MaxNewSize大小的时候,我们要考虑到Yong GenerationHeap的一部分,它的大小应该小于Old Generation的大小,这是因为在最坏的情况下,我们会将Yong Generation的所有对象复制到Old Generation中,因此-Xmx/2-XX:MaxNewSize的上限。

出于性能方面的考虑,我们可以用参数-XX:NewSize指定Yong Generation的初始化大小。这种方法很有效,尤其是当我们经过测试知道新生代与老生代分配地址比例的时候。

-XX:NewRation

-XX:NewRation可以设定Yong GenerationOld Generation的大小比例关系。

如果混合使用了初始化、上限和比例的参数,例如下面的场景:

java -XX:NewSize=32M -XX:MaxNewSize=512M -XX:NewRation=3 MyApp

在这样设定的情况下,JVM会试图让Yong GenerationOld Generation大小的1/3,但是不会让Yong Generation的大小低于32M或者高于512M。

-XX:SuvivorRatio

-XX:SuvivorRatio可以设定EdenFrom或者To的比例。例如-XX:SuvivorRatio=10表示EdenYong Generation的10/12,FromTo都占Yong Generation的1/12。

-XX:+PrintTenuringDistibution

当设定-XX:+PrintTenuringDistibution时,就是告诉JVM打印出Suvivor区域所有对象年龄分布(age distribution)。例如:

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

第一行表示Suvivor To区域中大约的容量是75M,对象经历GC的阈值(threshold)是15次。紧接着第二行表示Suvivor To区域中有大约19M对象经历了1次GC,这时候Suvivor TO 区域中中也有大约19M对象,说明第一次GC所有的对象都存活过来,并且被复制到了Suvivor From中。最后一行表示Suvivor To区域中有大约2M的对象经历3次GC,这时候Suvivor TO区域中大约有22M对象,没有超过第一行的容量75M。

还有很多参数:-XX:InitalTenuringThreadhold-XX:MaxTenuringThreadhold-XX:TargetSuvivorRatio-XX:+NeverTenure-XX:+AlwaysTenure等。

总之,Yong Generation的大小在整个应用JVM性能调优中起着很重要的作用,在设定Yong Generation的参数时,一定要兼顾Old Generation的带下,才能很好的起到作用。

MetaspaceSize和MaxMetaspacesize

MetaspaceSize的含义是Metaspace达到这个值进行full gc。

应用运行一段时间后,就能够通过日志获取到该进程实际占用的Metaspace大小,并且在运行完一段时间后该值应该不太会变化。为什么运行一段时间再获取该值靠谱呢?因为JVM对类是动态加载的,运行一段时间该用的功能都用了,该加载的类都加载了,Metaspace存储class运行时相关信息,比如字节码,类型信息,方法信息等,不存储对象信息不会一直增大,所以就比较稳定。然后如果要设置MaxMetaspaceSize,可以比你获取到运行一段时间后实际MetaspaceSize再略大一点就可以了。

实际上我理解就让默认值不设限制就可以,只是设置了的话,出现Metaspace内存溢出,能够了解到该应用占Metaspace的大小。即使大项目Metaspace也不会占用太大,一个项目class文件源代码就几M,十多M。应该不会因为Metaspace的占用,对服务器内存造成影响(现在服务器内存一般至少十多G)。

参考链接:https://stackoverflow.com/questions/36465192/guidelines-to-set-metaspacesize-java-8

更新记录

  • 2022-03-07 11:43 周末和俊波讨论JVM参数MetaspaceSize记录。