我们不应该把大量时间都消耗在那些小的性能改进上,过早的考虑优化是所有噩梦的根——高德纳
程序员应该编写清晰、直接、已读和易理解的代码,虽然算法和设计改变了复杂程序的结构,但是提供了更好的性能。真正的优化最好留到最后,等到性能分析表明这些措施有巨大收益的时候才进行。
文中部分内容节选自——Java性能优化权威指南
目录
性能调优的三个重要指标
调优的三个基本原则
GC堆大小调优法则
计算活跃数据大小
活跃数据大小是应用处于稳定状态时,长期存活在Java堆中占用的空间大小。Full GC之后Java堆中老年代和永久代占用的空间大小。
Java应用程序的活跃数据大小可通过GC日志进行收集,包括如下内容
- 应用处于稳定状态时,老年代占用的Java堆大小;
- 应用处于稳定状态时,永久代占用的Java堆大小;
通过Full GC日志进行统计,命令jmap -histo:live <pid> 多次求平均值
如何进行调优?
1.对应用延迟/响应性的调优
![](https://i-blog.csdnimg.cn/blog_migrate/7a2da4461319d3cf21ff8bf7af092184.png)
![](https://i-blog.csdnimg.cn/blog_migrate/2ef21c1ce9b8e7f200d55caf4dda850a.png)
优化新生代的大小
MinorGC需要的时间与新生代中可访问的对象直接相关。
- 新生代空间越小,Minor GC的持续的时间越短。
- 较小的新生代空间,会加大Minor GC的频率
分析MinorGC时,如果GC太慢,应减小年轻代空间;如果GC频率过高,应加大年轻代空间。
注意:在调整年轻代大小时应该保证老年代的空间不变
还需要谨记以下准则:
- 老年代空间大小不应该小于活跃数据大小的1.5倍
- 新生代空间至少应为Java堆大小的10%,通过Xmx和Xms可设定该值
- 增大Java堆大小时,需要注意不要超过JVM可用的物理内存数
如果只考虑MinorGC引起的延迟,而调整新生代的大小又无法满足应用程序的平均停顿时间或延迟性要求,就只能修改应用程序或者改变JVM的部署模式,即在多个JVM上部署应用程序。
优化老年代的大小
- 从老年代中减去活跃数据的大小可以得到可用老年代空间大小
- 根据MinorGC的执行后老年代空间提升大小,以及MinorGC执行的时间,计算出每秒老年代的空间增长量
- 通过增长量计算出多长时间能够占满老年代空间,通过查看日志对比是否与估算一致,验证预测
- 如果预测达不到最差的FullGC的频率要求,就应该扩大Old区降低FullGC的频率。增加老年代空间时要保证新生代大小恒定
如果更改老年代的大小后,只观察到了FullGC说明,老年代和新生代空间比例失调。一般的出现该情况是因为经过FullGC,老年代空间仍然不足以容纳从新生代提升对象。
如果调整大小不满足延迟性要求,需要更换并行度更高的垃圾回收器,加快对老年代空间的清理。
Survivor空间介绍
当Eden空间被填满时会方式MinorGC,活跃对象会从Eden空间复制到标记为“To”的Survivor空间,同时“From”Survivor空间中存活下来的对象也会被复制到"To"Survivor空间中去。一旦完成MinorGC,Eden空间会清空,“From”Survivor空间也会变空,而“To”Survivor空间中保存了还活跃的对象。之后,Survivor空间将互相交换标记成为下次的MinorGC做准备,清空的From变成To,To区变成了From区。每次MinorGC结束,Eden空间和一块Survivor变为空。
如果“To”区容纳不了,Eden区和From区复制过来的活跃对象,超出的部分将被直接提升到老年代。溢出至老年代空间会导致非计划的老年代空间消耗加锁,最终导致Stop-The-World压缩式GC的产生。
-XX:SurvivorRatio=<ratio> survivor=-Xmn<value>/(-XX:SurvivorRatio=<ratio>+2)
注意:针对Java应用程序的低延迟性要求进行调优时,要避免Stop-The-World压缩式GC,尽量遵从MinorGC回收原则
晋升阈值 -XX:MaxTenuringThreshold=<n> 最大0-15
通过-XX:+PrintTenuringDistribution监控晋升的分布或者对象年龄分布,并以此为依据确定最优的最大晋升阈值。
-XX:+PrintTenuringDistribution
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintTenuringDistribution
一起配合查看晋升阈值
CMS调优延迟
应用程序生命周期中努力达到的一个目标,让老年代空间大到足以避免由堆内存碎片引起的Stop-The-World压缩(为GC申请最大内存原则)。处理碎片问题的另一个方法是减少对象从新生代提升至老年代的比率,即“MinorGC回收原则”。
晋升阈值控制新生代中的对象何时提升至老年代
一旦包含Eden空间和Survivor空间在内的新生代空间优化完成,MinorGC引入的延迟达到应用程序的要求之后,就可以将经历转向CMS收集器的调优上,减少最差情况的延迟并最小化延迟发生的频率。进一步维持空闲老年代的恒定,并由此避免发生Stop-The-World压缩式垃圾回收。
什么是失速:成功的CMS收集器调优要能以对象从新生代提升到老年代的同等速度对老年代中的对象进行垃圾收集,达不到这个标准就称之为失速。
CMS周期的初始化基于老年代空间占用情况。如果CMS周期开始的太晚,就会发生失速。如果它无法以足够快的速度回收对象,就无法避免老年代空间的耗尽。单CMS周期过早开始,又会引起无用的消耗,影响应用程序的吞吐量。通常,早启动CMS周期要比晚启动CMS要好,启动太晚有可能引发回收不及时导致内存溢出。
-XX:CMSInitiatingOccupancyFraction=<percent> 设置老年代空间达到多少开始收集
-XX:+UseCMSInitiatingOccupancyOnly总是使用-XX:CMSInitiatingOccupancyFraction设定的值作为启动CMS周期的老年代空间占用阈值。如果开启-XX:+UseCMSInitiatingOccupancyOnly则HotSpot VM仅在第一使用设定的值,在之后仍保持自适应的模式。
保证-XX:CMSInitiatingOccupancyFraction的比率应大于老年代与活跃对象的比率(Full GC后堆内的对象大小求平均)
CMS周期中有两个阶段是STW阶段,处于这两个阶段线程会被阻塞,这两个阶段分别是初始化标记和重新标记阶段。初始化标记是单线程的,但极少占用时间,通常小于其他垃圾收集停顿。重新标记阶段是多线程的。通过-XX:ParallelGCThreads=<n>设置重新标记阶段的线程数 。
默认设置的线程数=8+(Runtime.availableProcessors()-8)*5/8,多应用运行同一个系统场景里,建议将CMS收集线程数设置的小于默认值。
-XX:+CMSScavengeBeforeRemark
该命令会在进入CMS重新标记阶段之前先执行一次MinorGC。MinorGC通过减少引用老年代空间的新生代对象数目,将重新标记阶段的工作量减少到最少。
2.对应用吞吐量调优
吞吐量的主要输入是应用程序的吞吐量要求。应用程序的吞吐量通常在应用层面而不是在JVM的层面进行度量。
- 应用程序必定有一些吞吐量的性能指标包括,或者根据应用程序进行的操作能衍生出某种吞吐量的指标。
- Java应用程序的内存使用量,Java堆内存可使用的内存越多,应用程序的性能越好。
如果出现调优后仍然无法达到应用程序的吞吐量要求的情况。修改应用程序或者改变JVM的部署模式。
CMS吞吐量的调优
Throughput收集器调优
Throughput(通过-XX:UseParallelOldGC和-XX:+UseParallelGC)提供的吞吐量性能是HotSpot中诸多垃圾回收器中最好的。Throughput收集器默认开启自适应大小调整的特性。通过-XX:-UseAdaptiveSizePolicy关闭这一特性。
-XX:TagretSurvivorRatio=<n>,设置目标Survivor空间的占用百分比,默认50%