本节的目标是作一些优化以知足对应用对延迟的需求。此次须要几个步骤,包括完善Java堆大小的配置,评估垃圾回收占用的时间和频率,也许还要尝试切换到不一样的垃圾回收器,以及因为使用了不一样的垃圾回收器,须要从新优化Java堆空间大小。
这一步有以下可能的结果:
一、应用的延迟需求被知足了。若是这一步的优化操做知足了应用的延迟需求,你能够继续下一步优化(优化吞吐量)。
二、应用的延迟需求未被知足。若是这一步的优化操做未能知足延迟需求,你可能须要从新看看延迟需求是否合理或者修改应用程序。一些可能的问题能够帮助改善应用的延迟问题:
a、优化Java堆以及修改应用以减小对象的分配和对象的长时间存活。
b、修改JVM的部署结构,让每个JVM作更少的工做。
上面的两个步骤均可以减小JVM的对象分配,所以减小垃圾回收的频率。
这一步从查看垃圾回收对应用的延迟的影响开始,基于前面一节“决定内存消耗”计算出来的Java堆大小。
下面列出了评估垃圾回收对延迟的影响须要进行的几个事情:
一、
测量
MinorGC的时间。
二、
测量
MinorGC的频率。
三、
测量
FullGC的时间。
四、
测量
FullGC的频率。
测量
垃圾回收的时间的和频率对于改善Java堆大小配置来讲是很是重要的。MinorGC的时间和频率的
测量结果能够用来改善young代的空间大小。测量最坏状况下FullGC的时间和频率能够用来决定old代的大小,以及是否须要切换成吞吐量垃圾回收器(经过使用-XX:+UseParalleOldGC或者-XX:+UseParallelGC)或者并发垃圾回收器(CMS,经过使用-XX:+UseConcMarkSweepGC)。在使用吞吐量垃圾回收器的时候,若是垃圾回收的延迟和频率过高以致使应用的延迟需求没法知足的时候才切换到CMS,若是选择了切换,须要对CMS垃圾回收器进行优化,后面会详细介绍这个问题。
接下来详细介绍前面提到的各类状况。
需求
下面列举了几个这一步优化操做需求,它们来源于应用的系统需求:
一、能够接收的平均暂停时间。平均暂停时间需求用于和MinorGC消耗的时间比较。
二、
能够接收的
MinorGC的频率。其实频道对于应用负责人来讲,没有平均延迟时间重要。
三、应用负责人可以接受的最大延迟时间。这个时间受到FullGC的影响。
四、应用负责人可以接收的最大延迟的频率,即FullGC的频率。其实,大多数时间应用管理员仍是更加关心应用的的最大延迟时间超过了最大延迟的频率。
一旦肯定了需求,这些垃圾回收器的时间消耗和频率均可以经过垃圾回收日志收集到。先把垃圾回收器设置为吞吐量垃圾回收器(设置-XX:+UseParallelOldeGC或者-XX:+UseParallelGC)。经过反复测试,可让young代和old代知足上面的要求。下面2节介绍如何优化young代和old代空间大小来观察MinorGC和最坏状况的FullGC的消耗时间和频率。
改善young代的大小
肯定young代的大小是经过评估垃圾回收的统计信息以及观察MinorGC的消耗时间和频率,下面举例说明如何经过垃圾回收的统计信息来肯定young代的大小。
尽管MinorGC消耗的时间和young代里面的存活的对象数量有直接关系,可是通常状况下,更小young代空间,更短的MinorGC时间。若是不考虑MinorGC的时间消耗,减小young代的大小会致使MinorGC变得更加频繁,因为更小的空间,用玩空间会用更少的时间。同理,提升young代的大小会下降MinorGC的频率。
当测试垃圾回收数据的时候,发现MinorGC的时间太长了,正确的作法就是减小young代的空间大小。若是MinorGC太频繁了就增长young代的空间大小。


上图是一个展现了MinorGC的例子,这个例子是运行在以下的HotSpot VM命令参数下的。
-Xms6144m -Xmx6144m -Xmn2048m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+UserParallelOldGC
上图显示了MinorGC平均的消耗时间是0.05秒,平均的频率是2.147秒1次。当计算MinorGC的消耗时间和频率的时候,越多的数据参与计算,准确性会越高。而且应用要处于稳定运行状态下来收集MinorGC信息也是很是重要的。
下一步是比较MinorGC的平均时间和系统对延迟的要求,若是MinorGC的平均时间大于了系统的要求,减小young代的空间大小,而后继续测试,再收集数据以及从新评估。
若是MinorGC的频率大于了系统的要求,就增长young代的空间大小,而后继续测试,再收集以及从新评估。
也许须要数次重复才可以让系统达到延迟要求。当你改变young代的空间大小的时候,尽可能保持old代的空间大小不要改变。
从上图的垃圾回收信息来看,若是应用的延迟要求是40毫秒的话,观察到的MinorGC的延迟是58毫秒,比系统的要求高出了很多。上面例子使用的命令选项是
-Xms6144m -Xmx6144m -Xmn2048m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+UserParallelOldGC
意味着old代的空间大小是4096M,减少young代的空间大小的10%并且要保持old代的空间大小不变,可使用以下选项。
-Xms5940m -Xmx5940m -Xmn1844m -XX:PermSize=96 -XX:MaxPermSize=96 -XX:+UserParallelOldGC
注意的是young代的空间大小从2048M减小到1844M,整个Java堆的大小从6144M减小到5940M,二者都是减小了204m。
不管是young的空间调大仍是调小,都须要从新收集垃圾回收信息和从新计算MinorGC的平均时间和频率,以达到应用的延迟要求,可能须要几个轮回来达到这个要求。
为了说明了增长young代的大小以下降MinorGC的频率,咱们下面举一个例子。若是系统要求的频率是5秒一次,这个上面的例子中是2.147秒一次,也就是说它用了2.147秒,填充满了2048M空间,若是须要5秒一次的频率,那么就须要5/2.147倍的空间,即2048*5/2.147等于4700M。所以young代的空间须要调整到4700M。下面是一个示例来讲明配置这个:
-Xms8796m -Xmx8796m -Xmn4700m -XX:PermSize=96m -XX:MaxPermSize=96m -XX:+UsePrallelOldGC
注意是-Xms和-Xmx也同步调整了。
另一些调整young代的空间须要注意的事项:
一、old代的空间必定不能小于活动对象的大小的1.5倍。
二、young代的空间至少要有Java堆大小的10%,过小的Java空间会致使过于频繁的MinorGC。
三、当提升Java堆大小的时候,不要超过JVM可使用的物理内存大小。若是使用过多的物理内存,会致使使用交换区,这个会严重影响性能。
若是在仅仅是MinorGC致使了延迟的状况下,你没法经过调整young代的空间来知足系统的需求,那么你须要重 新修改应用程序、修改JVM部署模型把应用部署到多个JVM上面(一般得要多机器了)或者从新评估系统的需求。
若是经过调整MinorGC可以知足应用的延迟需求,接下来就能够调整old代了,以达到最坏状况下的延迟和延迟频率的需求。下一节详细说明这个问题。
完善old代的大小
这一节的目标是评估因为FullGC引发的最差暂停时间和频率。
同前面一个节“完善young代大小”同样,垃圾回收的统计信息是必须的,在稳定状态下,FullGC的时间代表了应用最差的延迟,若是发生了多个FullGC,计算多个FullGC的平均消耗时间,更多数据可以更好的评估。
计算两次不一样的FullGC之间的时间差,能够提供出FullGC的频率,下图用一个列子来讲明两个FullGC:


若是没有FullGC,能够人为的去干预,前面说过,可使用VisualVM来触发FullGC。另外,评估FullGC的频率须要知道对象的转移率,这个转移率说明对象从young代转移到old代。接下来的介绍如何评估转移率。
接下有个几个MinorGC的例子,他们被用来评估FullGC的频率。
2010-12-05T14:40:29.564-0800: [GC [PSYoungGen: 2045989K->249795K(2097152K)] 3634533K->1838430K(6291456K), 0.0543798 secs] [Times: user=0.38 sys=0.01, real=0.05 secs]2010-12-05T14:40:31.949-0800: [GC [PSYoungGen: 2047896K->247788K(2097152K)] 3655319K->1859216K(6291456K), 0.0539614 secs] [Times: user=0.35 sys=0.01, real=0.05 secs]2010-12-05T14:40:34.346-0800 [GC [PSYoungGen: 2045889K->248993K(2097152K)] 3677202K->1881099K(6291456K), 0.0532377 secs] [Times: user=0.39 sys=0.01, real=0.05 secs]2010-12-05T14:40:36.815-0800 [GC [PSYoungGen: 2047094K->247765K(2097152K)] 3696985K->1900882K(6291456K), 0.0543332 secs] [Times: user=0.37 sys=0.01, real=0.05 secs]
从上面的例子能够看出:
一、Java堆的大小是6291456K或6144M
二、young代的大小是2097152K或2048M
三、old代的大小是6144M-2048M = 4096M
在这个例子中,活动对象的大小差很少是1370M。那么old代还有2726M剩余空间(4096M-1370M=2726M)。
填充完成2736M空间须要多长时间是由young代向old代的转移率决定的。这个转移率的计算经过查看每次MinorGC后old代的占用空间的增加状况以及MinorGC发生的时间。old代的空间占用是MinorGC以后Java堆中对象大小减去young代的大小,经过这个公式计算,能够看出在这个例子中每次MinorGC以后,old代的空间占用状况是:
1588635K,第一个MinorGC
1611428K,第二次MinorGC
1632106K,第三次MinorGC
1653117K,第四次MinorGC
每次的增量分别是
22793K,第一次和第二次的增量
20678K,第二次和第三次的增量
21011K,第三次和第四次的增量
平均每次MinorGC转移大概201494K或者叫21M。
若是剩余的空间都是按照设个转移率来转移到old代的话,且知道MinorGC的频率是每2.147秒一次。所以,这个转移率是201494K/2.147s差很少10M/s,那么一共的空间是2736M空间须要273.6s差很少4.5分钟一次。
所以,经过前面的案例分析,应用的最差延迟的频率是4.5分钟。这个评估能够经过让应用处于稳定运行状态超过4.5分钟来验证。
若是评估和观察的FullGC的频率高于了应用对最坏延迟频率的要求,那么能够提升old代的空间大小。若是改变old代的大小,保持young代的空间恒定,在优化young代的时候也说这个问题,二者应该独立优化,以保证有高效。
若是这步已经达到了你最坏延迟的要求,那么这一步调优延迟就算已经完成了,就能够进入下一步去调优“吞吐量”了。
若是你未能达到了应用对最坏延迟时间和频率的性能要求,因为FullGC的执行时间太长了,而后你能够把垃圾回收器切换CMS(concurrent garbage collection)。CMS有能力让垃圾回收尽可能是多线程的,即让程序保持在运行状态。要使用CMS能够经过下面这条命令选项:-XX:+UseConcMarkSweepGC。
后面详细说明如何调优CMS。
优化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空间。好比说,下面的一条语句:
Map<String,String> map = new HashMap<String,String>();
一个新的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空间的大小,以下面选项:
-Xmx1568m -Xms1568m -Xmn544m -XX:SurvivorRatio=15
能够产生以下的任期分布:
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垃圾回收周期的启动,下节详细介绍。
CMS垃圾回收器周期
一旦young的空间大小(包含eden和survivor空间)已经完善得知足应用对MinorGC产生延迟要求,注意力能够转移到优化CMS垃圾回收器,下降最差延迟时间的时间长度以及最小化最差延迟的频率。目标是保持可用的old代空间和并发垃圾回收,避免stop-the-world压缩垃圾回收。
stop-the-world压缩垃圾回收是垃圾回收影响延迟的最差状况,对某些应用来讲,恐怕没法彻底避免开这些,可是本节提供的优化信息至少能够减小他们的频率。
成功的优化CMS垃圾回收器须要达到的效果是old代的里面的垃圾回收的效率要和young代转移对象到old代的效率相同,没有可以完成这个标准能够称为“比赛失败”,比赛失败的结果就是致使stop-the-world压缩垃圾回收。不比赛中失败的一个关键是让下面两个事情结合起来:一、old代有足够的空间。二、启动CMS垃圾回收周期开始时机——快到回收对象的速度比较转移对象来的速度更快。
CMS周期的启动是基于old代的空间大小的。若是CMS周期开始的太晚,他就会输掉比赛,没有可以快速的回收对象以免溢出old代空间。若是CMS周期开始得太早,会形成没必要要的压力以及影响应用的吞吐量。可是,一般来说过早的启动总比过晚的启动好。
HotSpot VM自动地计算出当占用是多少时启动CMS垃圾回收周期。不过在一些场景下,对于避免stop-the-world垃圾回收,他作得并很差。若是观察到stop-the-world垃圾回收,你能够优化该何时启动CMS周期。在CMS垃圾回收中,stop-the-world压缩垃圾回收在垃圾回收日志中输出是“concurrent mode failure”,下面一个例子:
174.445: [GC 174.446: [ParNew: 66408K->66408K(66416K), 0.0000618
secs]174.446: [CMS ( concurrent mode failure): 161928K->162118K(175104K),
4.0975124 secs] 228336K->162118K(241520K)
若是你发现有concurrent mode failure你能够经过下面这个选项来控制何时启动CMS垃圾回收:
-XX:CMSInitiatingOccupancyFraction=<percent>
这个值指定了CMS垃圾回收时old代的空间占用率该是什么值。举例说明,若是你但愿old代占用率是65%的时候,启动CMS垃圾回收,你能够设置-XX:CMSInitiatingOccupancyFraction=65。另一个能够同时使用的选项是
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSInitiatingOccupancyOnly指定HotSpot VM老是使用-XX:CMSInitiatingOccupancyFraction的值做为old的空间使用率限制来启动CMS垃圾回收。若是没有使用-XX:+UseCMSInitiatingOccupancyOnly,那么HotSpot VM只是利用这个值来启动第一次CMS垃圾回收,后面都是使用HotSpot VM自动计算出来的值。
-XX:CMSInitiatingOccupancyFraction=<percent>这个指定的值,应该比垃圾回收以后存活对象的占用率更高,怎么样计算存活对象的大小前面在“决定内存占用”的章节已经说过了。若是<percent>不比存活对象的占用量大,CMS垃圾回收器会一直运行。一般的建议是-XX:CMSInitiatingOccupancyFraction的值应该是存活对象的占用率的1.5倍。举例说明一下,假如用下面的Java堆选项配置:
-Xmx1536m -Xms1536m -Xmn512m
那么old代的空间大小是1024M(1536-512 = 1024m)。若是存活对象的大小是350M的话,CMS垃圾回收周期的启动阀值应该是old代占用空间是525M,那么占用率就应该是51%(525/1024=51%),这个只是初始值,后面还可能根据垃圾回收日志进行修改。那么修改后的命令行选项是:
-Xmx1536m -Xms1536m -Xmn512m -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=51
该多早或者多迟启动CMS周期依赖于对象从young代转移到old代的速率,也就是说,old代空间的增加率。若是old代填充速度比较缓慢,你能够晚一些启动CMS周期,若是填充速度很快,那么就须要早一点启动CMS周期,可是不能小于存活对象的占用率。若是须要设置得比存活对象的占用率小,应该是增长old代的空间。
想知道CMS周期是开始的太早仍是太晚,能够经过评估垃圾回收信息识别出来。下面是一个CMS周期开始得太晚的例子。为了更好阅读,稍微修改了输出内容:
注意FullGC在CMS-inital-mark以后很快就发生了。CMS-initial-mark是报告CMS周期多个字段中的一个。下面的例子会使用到更多的字段。[ParNew 742993K->648506K(773376K), 0.1688876 secs][ParNew 753466K->659042K(773376K), 0.1695921 secs][CMS-initial-mark 661142K(773376K), 0.0861029 secs][Full GC 645986K->234335K(655360K), 8.9112629 secs][ParNew 339295K->247490K(773376K), 0.0230993 secs][ParNew 352450K->259959K(773376K), 0.1933945 secs]
下面是一个CMS开始的太早了的状况:
[ParNew 390868K->296358K(773376K), 0.1882258 secs][CMS-initial-mark 298458K(773376K), 0.0847541 secs][ParNew 401318K->306863K(773376K), 0.1933159 secs][CMS-concurrent-mark: 0.787/0.981 secs][CMS-concurrent-preclean: 0.149/0.152 secs][CMS-concurrent-abortable-preclean: 0.105/0.183 secs][CMS-remark 374049K(773376K), 0.0353394 secs][ParNew 407285K->312829K(773376K), 0.1969370 secs][ParNew 405554K->311100K(773376K), 0.1922082 secs][ParNew 404913K->310361K(773376K), 0.1909849 secs][ParNew 406005K->311878K(773376K), 0.2012884 secs][CMS-concurrent-sweep: 2.179/2.963 secs][CMS-concurrent-reset: 0.010/0.010 secs][ParNew 387767K->292925K(773376K), 0.1843175 secs][CMS-initial-mark 295026K(773376K), 0.0865858 secs][ParNew 397885K->303822K(773376K), 0.1995878 secs]
CMS-initial-mark表示CMS周期的开始, CMS-initial-sweep和CMS-concurrent-reset表示周期的结束。注意第一个CMS-initial-mark报告堆大小是298458K,而后注意,ParNew MinorGC报告在CMS-initial-mark和CMS-concurrent-reset之间只有不多的占用量变化,堆的占用量能够经过ParNew的->的右边的数值来表示。在这个例子中,CMS周期回收了不多的垃圾,经过在CMS-initial-mark和CMS-concurrent-reset之间只有不多的占用量变化可看出来。这里正确的作法是启动CMS周期用更大的old代空间占用率,经过使用参数
-XX:+UseCMSInitiatingOccupancyOnly和-XX:CMSInitiatingOccupancyFraction=<percent>。基于初始(CMS-initial-mark)占用量是298458K以及Java堆的大小是773376K,就是CMS发生的占用率是35%到40%(298458K/773376K=38.5%),可使用选项来强制提升占用率的值。
下面是一个CMS周期回收了大量old代空间的例子,并且没有经历stop-the-world压缩垃圾回收,也就没有并发错误(concurrent mode failure)。一样的修改输出格式:
[ParNew 640710K->546360K(773376K), 0.1839508 secs][CMS-initial-mark 548460K(773376K), 0.0883685 secs][ParNew 651320K->556690K(773376K), 0.2052309 secs][CMS-concurrent-mark: 0.832/1.038 secs][CMS-concurrent-preclean: 0.146/0.151 secs][CMS-concurrent-abortable-preclean: 0.181/0.181 secs][CMS-remark 623877K(773376K), 0.0328863 secs][ParNew 655656K->561336K(773376K), 0.2088224 secs][ParNew 648882K->554390K(773376K), 0.2053158 secs][ParNew 489586K->395012K(773376K), 0.2050494 secs][ParNew 463096K->368901K(773376K), 0.2137257 secs][CMS-concurrent-sweep: 4.873/6.745 secs][CMS-concurrent-reset: 0.010/0.010 secs][ParNew 445124K->350518K(773376K), 0.1800791 secs][ParNew 455478K->361141K(773376K), 0.1849950 secs]
在这个例子中,在CMS周期开始的时候,CMS-initial-mark代表占用量是548460K。在CMS周期开始和结束(CMS-concurrent-reset)之间,ParNew MinorGC报告显著的减小了对象的占用量。尤为,在CMS-concurrent-sweep以前,占用量从561336K下降到了368901K。这个代表在CMS周期中,有190M空间被垃圾回收。须要注意的是,在CMS-concurrent-sweep以后的第一个ParNew MinorGC报告的占用量是350518K。这个说明超过190M被垃圾回收(561336K-350518K=210818K=205.88M)。
若是你决定优化CMS周期的启动,多尝试几个不一样的old代占用率。监控垃圾回收信息以及分析这些信息能够帮助你作出正确的决定。
强制的垃圾回收
若是你想要观察经过调用System.gc()来启动的FullGC,当使用用CMS的时候,有两种方法来处理这种状况。
一、你能够请求HotSpot VM执行System.gc()的时候使用CMS周期,使用以下命令选项:
-XX:+ExplicitGCInvokesConcurrent 或者 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
第一个选项在Java 6及更新版本中可以使用,第二选项在从Java 6 Update 4以后才有。若是能够,建议使用后者。
二、你能够请求HotSpot VM选项忽视强制的调用System.gc(),可使用以下选项:
-XX:+DisableExplicitGC
这个选项用来让其余垃圾回收器忽略System.gc()请求。
当关闭的强制垃圾回收须要当心,这样作可能对Java性能产生很大的影响,关闭这个功能就像使用System.gc()同样须要明确的理由。
在垃圾回收日志里面找出明确的垃圾回收信息是很是容易的。垃圾回收的输出里面包含了一段文字来讲明FullGC是用于调用System.gc().下面是一个例子:
注意Full GC后面的(System)标签,这个说明是System.gc()引发的FullGC。若是你在垃圾回收日志里面观察到了明确的FullGC,想一想为何会出现、是否须要关闭、是否须要把应用源代码里面的相关代码删除掉,对CMS垃圾回收周期是否有意义。2010-12-16T23:04:39.452-0600: [Full GC (System)[CMS: 418061K->428608K(16384K), 0.2539726 secs]418749K->4288608K(31168K),[CMS Perm : 32428K->32428K(65536K)],0.2540393 secs][Times: user=0.12 sys=0.01, real=0.25 secs]
并发的Permanent代垃圾回收
FullGC发生多是因为permanent空间满了引发的,监控FullGC垃圾回收信息,而后观察Permanent代的占用量,判断FullGC是不是因为permanent区域满了引发的。下面是一个因为permanent代满了引发的FullGC的例子:
2010-12-16T17:14:32.533-0600: [Full GC[CMS: 95401K->287072K(1048576K), 0.5317934 secs]482111K->287072K(5190464K),[CMS Perm : 65534K->58281K(65536K)], 0.5319635 secs][Times: user=0.53 sys=0.00, real=0.53 secs]
注意permanent代的空间占用量,经过CMS Perm :标签识别。permanent代空间大小是括号里面的值,65536K。在FullGC以前permanent代的占用量是->左边的值,65534K,FullGC以后的值是58281K。能够看到的是,在FullGC以前,permanent代的占用量以及基本上和permanent代的容量很是接近了,这个说明,FullGC是由Permanent代空间溢出致使的。一样须要注意的是,old代尚未到溢出空间的时候,并且没有证听说明CMS周期启动了。
HotSpot VM默认状况下,CMS不会垃圾回收permanent代空间,尽管垃圾回收日志里面有CMS Perm标签。为让CMS回收permanent代的空间,能够用过下面这个命令选项来作到:
-XX:+CMSClassUnloadingEnabled
若是使用Java 6 update 3及以前的版本,你必须指定一个命令选项:
-XX:+CMSPermGenSweepingEnabled
你能够控制permanent的空间占用率来启动CMS permanent代垃圾回收经过下面这个命令选项:
-XX:CMSInitiatingPermOccupancyFraction=<percent>
这个参数的功能和-XX:CMSInitiatingOccupancyFraction很像,他指的是启动CMS周期的permanent代的占用率。这个参数一样须要和-XX:+CMSClassUnloadingEnabled配合使用。若是你想一直使用-XX:CMSInitiatingPermOccupancyFraction的值做为启动CMS周期的条件,你必需要指定另一个选项:
-XX:+UseCMSInitiatingOccupancyOnly
CMS暂停时间优化
在CMS周期里面,有两个阶段是stop-the-world阶段,这个阶段全部的应用线程都被阻塞了。这两阶段是“初始标记”阶段和“再标记”阶段,尽管初始标记解决是单线程的,可是经过不须要花费太长时间,至少比其余垃圾回收的时间短。再标记阶段是多线程的,线程数可经过命令选项来控制:
-XX:ParallelGCThreads=<n>
在Java 6 update 23以后,默认值是经过Runtime.availableProcessors()来肯定的,不过是创建在返回值小于等于8的状况下,反之,会使用Runtime.availableProcessors()*5/8做为线程数。若是有多个程序运行在同一个机器上面,建议使用比默认线程数更少的线程数。不然,垃圾回收可能会引发其余应用的性能降低,因为在同一个时刻,垃圾回收器使用太多的线程。
在某些状况下设置下面这个选项能够减小再标记的时间:
-XX:+CMSScavengeBeforeRemark
这个选项强制HotSpot VM在FullGC以前执行MinorGC,在再标记步骤以前作MinorGC,能够减小再标记的工做量,因为减小了young代的对象数,这些对象可以在old代获取到的。
若是应用有大量的引用或者finalizable对象须要处理,指定下面这个选项能够减小垃圾回收的时间:
-XX:+ParallelRefProcEnabled
这个选项能够用HotSpot VM的任何一种垃圾回收器上,他会是用多个的引用处理线程,而不是单个线程。这个选项不会启用多线程运行方法的finalizer。他会使用不少线程去发现须要排队通知的finalizable对象。
下一步
这一步结束,你须要看看应用的延迟须要是否知足了,不管是使用throughput垃圾回收器或者并发垃圾回收器。若是没有可以知足应用的须要,那么回头看看需求是否合理或者修改应用程序。若是知足了应用的需求,那么咱们就进入下一步——优化吞吐量。