优化CMS(concurrent garbage collection)
使用CMS,old代的垃圾回收执行线程会和应用程序的线程最大程度的并发执行。这个提供了一个机会来减少最坏延迟的频率和最坏延迟的时间消耗。CMS没有执行压缩,所以可以避免old代空间的stop-the-world压缩(会让整个应用暂停运行)。
优化CMS的目标就是避开stop-the-world压缩垃圾回收,然而,这个说比做起来容易。在一些的部署情况下,这个是不可避免的,尤其是当内存分配受限的时候。
在一些特殊的情况下,CMS比其他类型的垃圾回收需要更多优化,更需要优化young代的空间,以及潜在的优化该什么时候初始化old代的垃圾回收循环。
当从吞吐量垃圾回收器(Throughput)迁移到CMS的时候,有可能会获得更慢的MinorGC,由于对象从young代转移到old会更慢 ,由于CMS在old代里面分配的内存是一个不连续的列表,相反,吞吐量垃圾回收器只是在本地线程的分配缓存里面指定一个指针。另外,由于old代的垃圾回收线程和应用的线程是尽可能的并发运行的,所以吞吐量会更小一些。然而,最坏的延迟的频率会少很多,由于在old代的不可获取的对象能够在应用运行的过程被垃圾回收,这样可以避免old代的空间溢出。
使用CMS,如果old代能够使用的空间有限,单线程的stop-the-world压缩垃圾回收会执行。这种情况下,FullGC的时间会比吞吐量垃圾回收器的FullGC时间还要长,导致的结果是,CMS的绝对最差延迟会比吞吐量垃圾回收器的最差延迟严重很多。old代的空间溢出以及运行了stop-the-world垃圾回收必须被应用负责人重视,由于在响应上会有更长的中断。因此,不要让old代运行得溢出就非常重要了。对于从吞吐量垃圾回收器迁移到CMS的一个比较重要的建议就是提升old代20%到30%的容量。
在优化CMS的时候有几个注意点,首先,对象从young代转移到old代的转移率。其次,CMS重新分配内存的概率。再次,CMS回收对象时候产生的old代的分隔,这个会在可获得的对象中间产生一些空隙,从而导致了分隔空间。
碎片可以被下面的几种方法寻址。第一办法是压缩old代,压缩old代空间是通过stop-the-world垃圾回收压缩完成的,就像前面所说的那样,stop-the-world垃圾回收会执行很长时间,会严重影响应用的响应时间,应该避开。第二种办法是,对碎片编址,提高old代的空间,这个办法不能完全解决碎片的问题的,但是可以延迟old代压缩的时间。通常来讲,old代越多内存,由于碎片导致需要执行的压缩的时间久越长。努力把old的空间增大的目标是在应用的生命周期中,避免堆碎片导致stop-the-world压缩垃圾回收,换句话说,应用GC最大内存原则。另外一种处理碎片的办法是减少对象从young代移动到old的概率,就是减少MinorGC,应用MinorGC回收原则。
任期阀值(tenuring threshold)控制了对象该什么时候从young代移动到old代。任期阀值会在后面详细的介绍,它是HotSpot VM基于young代的占用空间来计算的,尤其是survivor(幸存者)空间的占用量。下面详细介绍一下survivor空间以及讨论任期阀值。
survivor空间
survivor空间是young代的一部分,如下图所示。young代被分成了一个eden区域和两个survivor空间。
两个survivor空间的中一个被标记为“from”,另外一个标记为“to”。新的Java对象被分配到Eden空间。比如说,下面的一条语句:
- <span style="font-size:14px;"> Map<String,String> map = new HashMap<String,String>();</span>
一个新的HashMap对象会被放到eden空间,当eden空间满了的时候,MinorGC就会执行,任何存活的对象,都从eden空间复制到“to” survivor空间,任何在“from” survivor空间里面的存活对象也会被复制到“to” survivor。MinorGC结束的时候,eden空间和“from” survivor空间都是空的,“to” survivor空间里面存储存活的对象,然后,在下次MinorGC的时候,两个survivor空间交换他们的标签,现在是空的“from” survivor标记成为“to”,“to” survivor标记为“from”。因此,在MinorGC结束的时候,eden空间是空的,两个survivor空间中的一个是空的。
在MinorGC过程,如果“to” survivor空间不够大,不能够存储所有的从eden空间和from suvivor空间复制过来活动对象,溢出的对象会被复制到old代。溢出迁移到old代,会导致old代的空间快速增长,会导致stop-the-world压缩垃圾回收,所以,这里要使用MinorGC回收原则。
避免survivor空间溢出可以通过指定survivor空间的大小来实现,以使得survivor有足够的空间来让对象存活足够的岁数。高效的岁数控制会导致只有长时间存活的对象转移到old代空间。
岁数控制是指一个对象保持在young代里面直到无法获取,所以让old代只是存储长时间保存的对象。
survivor的空间可以大小设置可以用HotSpot命令行参数:-XX:SurvivorRatio=<ratio>
<ratio>必须是以一个大于0的值,-XX:SurvivorRatio=<ratio>表示了每一个survivor的空间和eden空间的比值。下面这个公式可以用来计算survivor空间的大小
- survivor spave size = -Xmn<value>/(-XX:SurvivorRatio=<ratio>+2)
这里有一个+2的理由是有两个survivor空间,是一个调节参数。ratio设置的越大,survivor的空间越小。为了说明这个问题,假设young代的大小是-Xmn512m而且-XX:SurvivorRatio=6.那么,young代有两个survivor空间且空间大小是64M,那么eden空间的大小是384M。
同样假如young代的大小是512M,但是修改-XX:SurvivorRatio=2,这样的配置会使得每一个survivor空间的大小是128m而eden空间的大小是256M。
对于一个给定大小young代空间大小,减小ratio参数增加survivor空间的大小而且减少eden空间的大小。反之,增加ratio会导致survivor空间减少而且eden空间增大。减少eden空间会导致MinorGC更加频繁,相反,增加eden空间的大小会导致更小的MinorGC,越多的MinorGC,对象的岁数增长得越快。
为了更好的优化survivor空间的大小和完善young代空间的大小,需要监控任期阀值,任期阀值决定了对象会再young代保存多久。怎么样来监控和优化任期阀值将在下一节中介绍。
任期阀值
“任期”是转移的代名词,换句话说,任期阀值意味着对象移动到old代空间里面。HotSpot VM每次MinorGC的时候都会计算任期,以决定对象是否需要移动到old代去。任期阀值就是对象的岁数。对象的岁数是指他存活过的MinorGC次数。当一个对象被分配的时候,它的岁数是0。在下次MinorGC的时候之后,如果对象还是存活在young代里面,它的岁数就是1。如果再经历过一次MinorGC,它的岁数变成2,依此类推。在young代里面的岁数超过HotSpot VM指定阀值的对象会被移动到old代里面。换句话说,任期阀值决定对象在young代里面保存多久。
任期阀值的计算依赖于young代里面能够存放的对象数以及MinorGC之后,“to” servivor的空间占用。HotSpot VM有一个选项-XX:MaxTenuringThreshold=<n>,可以用来指定当时对象的岁数超过<n>的时候,HotSpot VM会把对象移动到old代去。内部计算的任期阀值一定不会超过指定的最大任期阀值。最大任期阀值在可以被设定为0-15,不过在Java 5 update 5之前可以设置为1-31。
不推荐把最大任期阀值设定成0或者超过15,这样会导致GC的低效率。
如果HotSpot VM它无法保持目标survivor 空间的占用量,它会使用一个小于最大值的任期阀值来维持目标survivor空间的占用量,任何比这个任期阀值的大的对象都会被移动到old代。话句话说,当存活对象的量大于目标survivor空间能够接受的量的时候,溢出发生了,溢出会导致对象快速的移动到old代,导致不期望的FullGC。甚至会导致更频繁的stop-the-world压缩垃圾回收。哪些对象会被移动到old代是根据评估对象的岁数和任期阀值来确定的。因此,很有必要监控任期阀值以避免survivor空间溢出,接下来详细讨论。
监控任期阀值
为了不被内部计算的任期阀值迷惑,我们可以使用命令选项-XX:MaxTenuringThreshod=<n>来指定最大的任期阀值。为了决定出最大的任期阀值,需要监控任期阀值的分布和对象岁数的分布,通过使用下面的选项实现
- -XX:+PrintTenuringDistribution
-XX:+PrintTenuringDistribution的输出显示在survivor空间里面有效的对象的岁数情况。阅读-XX:+PrintTenuringDistribution输出的方式是观察在每一个岁数上面,对象的存活的数量,以及其增减情况,以及HotSpot VM计算的任期阀值是不是等于或者近似于设定的最大任期阀值。
-XX:+PrintTenuringDistribution在MinorGC的时候产生任期分布信息。它可以同其他选项一同使用,比如-XX:+PrintGCDateStamps,-XX:+PrintGCTimeStamps以及-XX:+PringGCDetails。当调整survivor空间大小以获得有效的对象岁数分布,你应该使用-XX:+PrintTenuringDistribution。在生产环境中,它同样非常有用,可以用来判断stop-the-world的垃圾回收是否发生。
下面是一个输出的例子:
Desired survivor size 8388608 bytes, new threshold 1 (max 15)
- age 1: 16690480 bytes, 16690480 total
在这里例子中,最大任期阀值被设置为15,(通过max 15表示)。内部计算出来的任期阀值是1,通过threshold 1表示。Desired survivor size 8388608 bytes表示一个survivor的空间大小。目标survivor的占有率是指目标survivor和两个survivor空间总和的比值。怎么样指定期望的survivor空间大小在后面会详细介绍。在第一行下面,会列出一个对象的岁数列表。每行会列出每一个岁数的字节数,在这个例子中,岁数是1的对象有16690480字节,而且每行后面有一个总的字节数,如果有多行输出的话,总字节数是前面的每行的累加数。后面举例说明。
在前面的例子中,由于期望的survivor大小(8388608)比实际总共survivor字节数(16690480)小,也就是说,survivor空间溢出了,这次MinorGC会有一些对象移动到old代。这个就意味着survivor的空间太小了。另外,设定的最大任期阀值是15,但是实际上JVM使用的是1,也表明了survivor的空间太小了。
如果发现survivor区域太小,就增大survivor的空间,下面详细介绍如何操作。
设定survivor空间
当修改survivor空间的大小的时候,有一点需要记住。当修改survivor空间大小的时候,如果young代的大小不改变,那么eden空间会减小,进一步会导致更频繁的MinorGC。因此,增加survivor空间的时候,如果young代的空间大小违背了MinorGC频率的需求,eden空间的大小同需要需要增加。换句话说,当survivor空间增加的时候,young代的大小需要增加。
如果有空间来增加MinorGC的频率,有两种选择,一是拿一些eden空间来增加survivor的空间,二是让young的空间更大一些。常规来讲,更好的选择是如果有可以使用的内存,增加young代的空间会比减少eden的空间更好一些。让eden空间大小保持恒定,MinorGC的频率不会改变,即使调整survivor空间的大小。
使用-XX:+PrintTenuringDistribution选项,对象的总字节数和目标survivor空间占用可以用来计算survivor空间的大小。重复前面的例子:
Desired survivor size 8388608 bytes, new threshold 1 (max 15)
- age 1: 16690480 bytes, 16690480 total
存活对象的总字节数是1669048,这个并发垃圾回收器(CMS)的目标survivor默认使用50%的survivor空间。通过这个信息,我们可以知道survivor空间至少应该是33380960字节,大概是32M。这个计算让我们知道对survivor空间的预估值需要计算对象的岁数更高效以及防止溢出。为了更好的预估survivor的可用空间,你应该监控应用稳定运行情况下的任期分布,并且使用所有的额外总存活对象的字节数来作为survivor空间的大小。
在这个例子,为了让应用计算岁数更加有效,survivor空间需要至少提升32M。前面使用的选项是:
- -Xmx1536m -Xms1536m -Xmn512m -XX:SurvivorRatio=30
那么为了保持MinorGC的频率不发生变化,然后增加survivor空间的大小到32M,那么修改后的选项如下:
- -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivvorRatio=15
当时young代空间增加了,eden空间的大小保持大概相同,且survivor的空间大小增减了。需要注意的时候,-Xmx、-Xms、-Xmn都增加了32m。另外,-XX:SurvivvorRatio=15让每一个survivor空间的大小都是32m (544/(15+2) = 32)。
如果存在不能增加young代空间大小的限制,那么增加survivor空间大小需要以减少eden空间的大小为代价。下面是一个增加survivor空间大小,每一个survivor空间从16m增减加到32m,那么会见减少eden的空间,从480m减少到448m(512-32-32=448,512-16-16=480)。
- -Xms1536m -Xms1536m -Xmn1512m -XX:SurvivorRatio=14
再次强调,减少eden空间大小会增加MinorGC的频率。但是,对象会在young代里面保持更长的时间,由于提升survivor的空间。
假如运行同样的应用,我们保持eden的空间不变,增加survivor空间的大小,如下面选项:
- <span style="font-size:14px;"> -Xmx1568m -Xms1568m -Xmn544m -XX:SurvivorRatio=15</span>
可以产生如下的任期分布:
Desired survivor size 16777216 bytes, new threshold 15 (max 15)- age 1: 6115072 bytes, 6115072 total
- age 2: 286672 bytes, 6401744 total
- age 3: 115704 bytes, 6517448 total
- age 4: 95932 bytes, 6613380 total
- age 5: 89465 bytes, 6702845 total
- age 6: 88322 bytes, 6791167 total
- age 7: 88201 bytes, 6879368 total
- age 8: 88176 bytes, 6967544 total
- age 9: 88176 bytes, 7055720 total
- age 10: 88176 bytes, 7143896 total
- age 11: 88176 bytes, 7232072 total
- age 12: 88176 bytes, 7320248 total
从任期分布的情况来看,survivor空间没有溢出,由于存活的总大小是7320248,但是预期的survivor空间大小是16777216以及任期阀值和最大任期阀值是相等的。这个表明,对象的老化速度是高效的,而且survivor空间没有溢出。
在这个例子中,由于岁数超过3的对象很少,你可能像把最大任期阀值设置为3来测试一下,即设置选项-XX:MaxTenuringThreshhold=3,那么整个选项可以设置为:
- -Xmx1568m -Xms1658m -Xmn544m -XX:SurvivorRatio=15 -XX:MaxTenuringThreshold=3
这个选项设置和之前的选项设置的权衡是,后面这个选择可以避免在MinorGC的时候不必要地把对象从“from” survivor复制到“to” survivor。在应用运行在稳定状态的情况下,观察多次MinorGC任期分布情况,看是否有对象最终移动到old代或者显示的结果还是和前面的结果类似。如果你观察得到和前面的任期分布情况相同,基本没有对象的岁数达到15,也没有survivor的空间溢出,你应该自己设置最大任期阀值以代替JVM默认的15。在这个例子中,没有长时间存活的对象,由于在他们的岁数没有到达15的时候就被垃圾回收了。这些对象在MinorGC中被回收了,而不是移动到old代里面。使用并发垃圾回收(CMS)的时候,对象从young代移动到old代最终会导致old的碎片增加,有可能导致stop-the-world压缩垃圾回收,这些都是不希望出现的。宁可选择让对象在“from” survivor和“to” survivor中复制,也不要太快的移动到old代。
你可能需要重复数次监控任期分布、修改survivor空间大小或者重新配置young代的空间大小直到你对应用由于MinorGC引起的延迟满意为止。如果你发现MinorGC的时间太长,你可以通过减少young代的大小直到你满意为止。尽管,减少young代的大小,会导致更快地移动对象到old代,可能导致更多的碎片,如果CMS的并发垃圾回收能够跟上对象的转移率,这种情况就比不能满足应用的延迟需求更好。如果这步不能满足应用的MinorGC的延迟和频率需求,这个时候就有必要重新审视需求以及修改应用程序了。
如果满足对MinorGC延迟的需求,包括延迟时间和延迟频率,你可以进入下一步,优化CMS垃圾回收周期的启动,下节详细介绍。