文章目录
- 九、应用程序吞吐量调优
- 1、CMS吞吐量调优
- 2、Throughput收集器调优
- 3、Survivor空间调优
- 4、 调优并行垃圾收集线程
- 5、下一步
- 十、极端示例
- 十一、其他性能命令行选项
- 1、实验性(最近最大)优化
- 2、逃逸分析
- 3、偏向锁
- 4、大页面支持
- Linux上使用大页面
九、应用程序吞吐量调优
这是调优的最后一步。吞吐量调优的主要输入是应用程序的吞吐量要求。另一个重要的输入是可用于部署Java应用程序的内存使用量。正如最大化GC内存原则所表述的, Java堆可用的内存越多,应用程序的性能越好。这一原则不仅适用于吞吐量的性能,也适用于延迟性能。
现实情况中有可能出现调优后仍无法达到应用程序的吞吐量要求的情况。这种情况下,我们有必要回顾应用程序的吞吐量要求,修改应用程序,或者改变JVM的部署模式。一旦采用了上面任何一个方案,都需要重新开始调优过程。
1、CMS吞吐量调优
使用CMS收集器时,为了获得更大的吞吐量性能提升你需要使用一些配置选项。
- 增加新生代空间大小。增加新生代空间大小可以降低Minor GC的频率,从而减少固定时间内Minor GC的次数。
- 增加老年代空间的大小。增加老年代空间的大小可以降低CMS周期的频率并减少内存碎片,最终减少并发模式失效以及Stop-The-World压缩式垃圾收集发生的几率。
- 按照7.8节中介绍的方法进一步优化新生代堆的大小。调整新生代中Eden空间和Survivor空间的大小以优化对象老化,减少由新生代提升到老年代的对象数目,最终减少CMS周期的发生数。
- 进一步优化CMS周期的启动条件,尽可能的在较晚的时候进行(参考7.8节)。在较晚的时候启动CMS周期能够降低CMS周期发生的频率。但是,更晚时候启动CMS的后果是出现并发模式失效,而且发生Stop-The-World压缩式垃圾收集的几率也会增大。
- 尝试使用7.11节介绍的附加命令行选项。
以上任何一个选项,或者几个选项的组合都可以减少垃圾收集器消耗的CPU周期数,从而将更多的CPU周期用于执行应用程序。对于提高吞吐量,同时义期望不触发Stop-The-World压缩式垃圾收集增大延迟的目标而言,前两个选项是更理想的方法。
一个指导原则是,CMS包括Minor GC所带来的开销应该小于10% ,你可能将这个值减少到1%-3%,通常情况下,如果当前观察到CMS垃圾收集的开销在3%或更少,通过调优吞吐量性能提升的空间就极其有限了。
2、Throughput收集器调优
对Throughput收集器进行吞吐量性能调优的目标是尽可能避免发生Full GC,或者更理想的情况下在稳定态时永远不发生Full GC。为了达到这个目标需要优化对象老化频率。
Throughput收集器默认启用了一个称为自适应大小调整的特性。自适应大小调整根据对象分配以及存活率自动地对新生代的Eden和Survivor空间进行调整以最优化对象老化频率。对大多数的应用程序而言,自适应调优就已经够用。然而,有一些应用需要竭尽可能地寻找吞吐量性触提升的机会,对于这些应用禁用自适应大小调整,对Eden空间、Survivor空间以及老年代空间进行细粒度的调优就必不可少了。使用下面的选项可以禁用自适应大小调整:
-XX:-UseAdaptiveSizePolicy
通过一个附加HotSpot VM命令行选项-XX:+PrintAdaptiveSizePolicy
可以生成更详细的Survivor空间占用日志,无论是Survivor空间溢出,还是对象从新生代提升进入老年代统统囊括其中。
下面是一个垃圾收集日志示例,该日志使用-XX:+PrintGCDateStamps
、-XX:PrintGCDetails
、-XX:-UseAdaptiveSizePolicy
(关闭白适应大小调整)和-XX:+PrintAdaptivesizePolicy
选项控制生成:
使用-XX:+PrintAdaptiveSizePolicy
选项增加的日志信息以GCAdaptiveSizePolicy开头,survived标签的右边是"To" Survivor空间中存活对象的大小。换句话说,它是Minor GC之后"To" Survivor空间的空间占用的空间大小。这个示例中,Survivor空间占用了224 408 984字节。promote标签右边是由新生代提升至老年代空间的对象大小(10904 856字节),overflow标签右边的文字表明是否有Survivor空间的对象溢出到了老年代空间。
在开始微调之前,请先禁用自适应大小调整,使用-XX:-UseAdaptiveSizePolicy
和-XX:+PrintAdaptiveSizePolicy
选项收集垃圾收集日志中的Survivor空间的统计信息。这些信息将作为初始数据为调优决策提供服务。
首先需要寻找的是应用程序稳定态时发生的Full GC。在日志中包含日期/时间截对定位应用程序何时从初始化阶段转入稳定态阶段非常有帮助。
稳定态时观察Full GC,可能发现有时短期存在的对象也被提升到了老年代空间。如果Full GC发生在稳定态,请首先确认老年代空间的大小是否为活跃数据大小的1.5倍,即Full GC之后老年代实际占用的空间大小的1.5倍。如有必要,增大老年代空间以满足1.5倍的通用原则。遵守这个原则可以确保你遭遇这些场景时仍有一定量的峰值储备,可以使应用程序继续工作。
确认有足够的老年代空间可用之后就可以开始着手分析稳定态发生的各个Minor GC了。首先请查看Survivor空间是否发生了溢出。Survivor空间发生溢出的示例如下:
如果Survivor空间在稳定态发生溢出,对象将在其达到极限年龄老化死去之前被提升到老年代空间。换句话说:对象被急速地提升到老年代空间。频繁地Survivor空间溢出会导致频繁的Full GC。如何调整Survivor空间的大小是我们接下来要讨论的话题。
3、Survivor空间调优
调整Survivor空间大小的目标是在短期存活对象被提升到老年代空间之前,尽可能长时间地保持/老化(Age )这些对象。我们可以从查看稳定态发生的Minor GC入手,尤其要注意存活的对象大小。从初始态转入到稳定态,可能需要考虑忽略刚开始的几个Minor GC的数据,因为应用程序在初始化阶段可能分配一些长期存在的对象,这些对象在提升进入老年代之前需要一些老化的时间。对于大多数的应用,通常忽略应用程序达到稳定态之前5-10个Minor GC即可。
每次Minor GC中存活对象的大小可以作为附加信息通过-XX:+PrintAdaptiveSizePolicy
·选项输出到日志中。下面是一个示例的输出,存活对象的大小为224 408 984字节:
通过存活对象的最大值结合目标Survivor空间的占用,就可以确定稳定态时,要最有效地老化对象所需要的最低Survior空间大小。如果目标Survivor空间占用没有通过-XX:TargetSurvivorRatio=<percent>
选项显式设定,目标Survivor空间则使用默认值50%。
首先,对最差情况下的Survivor空间进行调优。忽略应用程序进入稳定态之前的5-10个MinorGC的数据,找到稳定态下Full GC之间的所有Minor GC中最大的存活对象大小。
为了计算有效老化最大存活对象所需的最小Survivor空间,最大存活对象大小必须除以目标Survivor空间占用百分比,即50%或者使用-XX:TargetSurvivorRatio=<percent>
选项设置的百分比。
调整Survivor空间的大小并不仅仅是将Survivor空间大小设置成某个值,或者调整为稍大于从GC日志中获得的最大存活对象的值那么简单。在增大Survivor空间时应该保持Eden空间的大小恒定不变。你应该按照Survivor空间的增量,增大新生代空间,同时维持老年代空间大小不变。牺牲老年代来增大新生代空间同样也有其不良后果。如果应用程序的内存占用条件允许,同时又有足够的内存,那么最好的选择是增大Java堆(通过-Xms
和-Xmx
选项)的大小,而不是从老年代空间中获取空间。
为了计算有效老化最大存活对象所需的最小Survivor空间,最大存活对象大小必须除以目标Survivor空间占用百分比,即50%或者使用-XX:TargetSurvivorRatio=<percent>
选项设置的百分比。
我们使用下面的命令行选项举例说明:
JVM堆的总大小为13GB,新生代空间大小为4GB,老年代空间的大小为9GB (13-4-9)。
每个Survivor空间均为512MB (4GB/(6 + 2) = 0.5 GB-512 MB),假设分析垃圾收集日志之后发现应用程序稳定态时的最大存活对象大小为495 880 312字节,大约473MB (495 880 312/(1024 *1024)=473)。由于设置命令行选项时没有显式使用-XX:TargetSurvivorRatio= <percent>
选项目标Survivor空间的占用为默认值50%,可以根据最差情况Survivor对象大小设置最小Survivor空间,或者将其设定得稍微高一些,在本例中是495 880 312/50%=991 760 624字节,大约为946MB。
根据上面的初始命令行选项,一个4GB的新生代空间被划分成了两块各512MB字节大小的Survivor空间和一块3G字节大小(4-(0.5 * 2)=3)的Eden空间。对最差情况Survivor空间的分析表明每块Survivor空间的大小应该至少为946MB。1024MB的Surviver空间,即1GB的Survivor空间非常接近每块Survivor空间946MB的要求。
如果应用程序的内存占用要求不是问题,同时系统中有足够的可用内存,增大Survivr空间以应对稳定态最差情况下的存活对象大小,同时保持Eden空间和老年代大小不变的命令行选项如下:
一个通用原则是使用Throughput收集器时,垃圾收集的开销应该小于5%。如果可以将垃圾收集的开销减少到1%甚至更少,那基本上就已经到了极限,进一步优化需要进行除了本章介绍的这些调优方法之外的特殊的JVM调优,花费的代价很大。
对于有内存限制的情况,不能增加对应大小的内存(由于应用程序内存占用要求,或者其他限制),还有一个可以考虑的选项。计算下最小值、最大值、平均值标准偏差以及中位数的存活对象大小。这些计算提供了应用程序的对象分配率,即对象分配是否足够稳定;或者对象分配是否有大幅度的波动。如果不存在大幅波动,即最大值与最小值之间差距不大,或者标准差很小,可以尝试提高目标Survivor空间的占用百分比(-XX:TargetSurvivorRatio=<n>
),将其用默认值从50%,提高到60%, 70%, 80%甚至90%。需要注意的是,如果应用程序的对象分配有大幅波动,将目标Survivor空间占用的大小设置成大于50会导致Survivor空间溢出。
4、 调优并行垃圾收集线程
并行垃圾收集器使用的线程数也应该依据系统上运行的应用程序数以及底层的硬件平台进,行相应的调优。7.8.11节曾提到过,多个应用程序运行于同一个系统上时,建议通过命令行选项-XX:ParallelGCThreads=<n>
将并行垃圾收集的线程数设置为小于其默认值。否则,由于大量的垃圾收集线程同时运行,其他应用程序的性能将受到严重影响。
多个应用程序运行于同一系统上时设置并行垃圾收集线程的一个通用原则是用虚拟处理器的数目(Runtime. availableProcessors()
的返回值)除以该系统上运行的应用程序数。
5、下一步
如果你已经到达JVM调优流程的这一步,却仍然无法达到应用程序的吞吐量要求,可以尝试使用7.11节中的选项。如果其中任何一个选项都无法满足你的应用程序的吞吐量要求,那就需要回顾应用程序的性能要求,修改应用程序,或者改变JVM的部署模式一旦选择了一个方式,你就可以继续新一轮的调优了。
下一节我们将介绍一些极端示例,即一些不适用于通用JVM调优原则的场景。
十、极端示例
有些时候前面介绍的常规JVM调优原则并不适用。本节将探讨可能出现的极端场景。
- 有些应用程序大对象分配率很高,而长期存活对象的数目很少。这些应用程序需要比老年代空间大得多的新生代空间。
- 有些应用程序只有极少量的对象提升。这些应用程序不需要将老年代的空间设置的比活跃数据大小大太多,因为老年代的空间增长非常缓慢。
- 有些应用程序对延迟性要求很高,使用CMS收集器,只需要很小的新生代空间和一个大的老年代空间就可以将Minor GC引入的延迟控制得很小。这种配置下,对象很可能会被迅速提升至老年代,而不是在Survivor空间中有效地老化。CMS收集器在它们提升之后会再对这些对象进行垃圾收集。使用一个大的老年代空间可以减小老年代空间的碎片。
十一、其他性能命令行选项
1、实验性(最近最大)优化
如果应用程序的干系人为了提升性能,愿意接受由于启用实验性优化(最新的优化)带来的额外风险,就可以考虑使用-XX:AggressiveOpts
命令行选项。如果你关注应用程序的稳定性胜于性能,建议不要使用这个选项。
2、逃逸分析
逃逸分析( Escape Analysis )是一种评估Java对象可见范围的技术。尤其是指由某个执行线程创建的Java对象在另一个线程中可以访问,此时我们称该对象“逃逸”了。如果Java对象不发生逃逸,可以采用其他方法进行调优。因此,这种优化技术被称为逃逸分析。
HotSpot VM的逃逸分析优化可以通过下面的命令行选项开启:
-XX:+DoEscapeAnalysis
自Java 6 Update23之后,该选项被默认开启。借助逃逸分析, HotSpot虚拟机的JIT编译器可以应用下面任何一种优化技术。
- 对象展开。这是一种在可能直接回收的空间而非Java堆上分配对象字段的技术。
- 标量替换。这是一种减少内存访问的优化技术。
- 栈上分配。这是一种在线程的栈帧上而非Java堆上分配对象的优化技术。非逃逸对象由于不会被其他线程访问可以直接在线程栈帧上分配。线程栈帧上的分配可以减少对象在Java堆上分配的数目,从而减少垃圾收集的频率。
- 消除同步。如果线程分配的对象不会发生逃逸,且该线程持有了该对象上的锁,由于其他线程不会访问该对象,这个锁可以通过IT编译器移除。
- 消除垃圾收集的读/写屏障。如果线程分配的对象不发生逃逸,该对象只能从线程本地的根节点访问,因此在其他对象中存储其地址时不需要执行读或写屏障。只有在对象可以被另一个线程访问时,才需要读/写屏障。这常常发生在分配的对象被赋给了另一对象中的字段,并因此能被另一线程访问时,也就是发生了“逃逸”。
3、偏向锁
偏向锁是一种偏向于最后获得对象锁的线程的优化技术。当只有一个线程锁定该对象,没有锁冲突的情况下,其锁开销可以接近lock-free。
Java 6 HotSpot JDK中默认就已经开启偏向锁-XX:+UseBiasedLocking
。经验表明,这个功能对于大多数的应用程序而言这个功能是有效的。然而,还是有一部分应用程序使用该选项的效果并不理想。例如,对存在锁切换的应用,为了避免发生Stop-The-World操作,有必要取消偏向。如果不确定应用程序是否属于此类Java应用,可以分别在开启和关闭偏向锁的情况下测试一组性能数据进行比较。
4、大页面支持
计算机系统的内存被划分成称为“页”的固定大小的块。程序访问内存的过程中会将虚拟内存地址转换成物理内存地址。虚拟地址到物理地址的转换是通过页表完成的。为了减少每次内存访问时访问页表的代价,通常的做法是使用一块快速缓存,对虚拟地址到物理地址的转换进行缓存。这块缓存被称为转译快查缓存(TLB)。
使用TLB完成从虚拟地址到物理地址的映射比遍历整个页表的方式要快得多,TLB通常只能容纳固定数量的条目。TLB中的一条记录就是按页面大小统计的一块内存地址区间的映射。因此,系统的页面越大,每个条目能映射的内存地址区间越大,每个TLB能管理的空间也越大。TLB代表的地址区间越大,地址转译请求在TLB中失效的可能性就越小。当一个地址转译请求无法在TLB中找到匹配项时,我们称之发生了"TLB失效"。TLB失效事件发生时常常需要遍历内存中的页表,查找虚拟地址到物理地址的映射。与在TLB中查找地址映射比较起来,遍历页表是一项非常昂贵的操作。由此可见,使用大页面的好处是其减小了TLB失效的几率。
Linux上使用大页面
截至本书写作时,在Linux上使用大页面,除了需要使用-XX: +UseLargePages
命令行选项,还需要修改操作系统的配置。根据Linux发行版和内核的不同,需要进行的修改也有所不同。
如果Linux系统中的大页面没有设置正确,HotSpot VM仍然会接受-XX:+UseLargePages
选项,但是它会报告无法获得大页面,最后回退到使用底层系统默认支持的页面大小。