本文结合《Java性能权威指南》一书进行总结,用于Java代码性能调优实战。
改善性能涉及的3种不同活动:性能监控、性能分析、性能调优。
大多数操作系统的CPU使用率分为用户态CPU使用率和系统态CPU使用率。
系统态CPU使用率高意味着共享资源有竞争或者I/O设备之间有大量的交互。原本用于执行操作系统内核调用的CPU周期也可以用来执行应用代码。所以理想情况下,应用达到最高性能和扩展性时,系统态CPU使用率为0%。所以,提高应用性能和扩展性的一个目标是尽可能降低系统态CPU使用率。
Linux下监控CPU有如下几种方式:
vmstat命令显示如上,us表示用户态CPU使用率,sy表示系统态CPU使用率。id表示CPU空闲率或可用率,即id=100-us-sy
。
监控CPU调度程序运行队列对于分辨系统是否满负荷具有重要意义。如果在很长一段时间里,运行队列的长度超过了虚拟处理器的个数的一倍,就需要关注但不需要立刻采取行动。如果在很长一段时间里,运行队列的长度达到虚拟处理器的3~4倍或者更高,则需要立刻引起注意或采取行动。
解决运行队列长的办法有2种:
1、增加CPU以分担负载或减少处理器的负载量
2、分析系统中运行的应用,改进CPU使用率。如减少GC的频度、采用完成同样任务但CPU指令更少的算法
Linux上可以使用vmstat命令来监控运行队列长度。procs列的r子列的数字就代表运行队列中轻量级进程的实际数量。
当应用运行所需的的内存超过可用物理内存是,会发生页面交换。为应对这种情况,通常要为系统配置swap空间,swap空间会在一个独立的磁盘分区上,应用物理内存耗尽时,操作系统会将应用最少运行的部分置换到swap,需要访问时再置换回内存。这种置换活动会影响应用的响应性和吞吐量。
JVM垃圾收集器在系统页面交换时性能也很差。因为需要把置换出去的部分拿回到内存进行扫描,同时可能会使STW时间变长。
监控页面交换可用的命令:vmstat、top
,或者查看/proc/meminfo
文件。
使用vmstat显示如下,si表示内存页面换入的量,so表示页面内存换出的量。free列表示可用空闲内存,当页面交换时,能看到有变化。
使用pidstat命令可以监控锁竞争,pidstat -w
输出结果中cswch/s是让步式上下文切换,并不是所有上下文切换。
让步式切换消耗CPU时钟周期的百分比=让步式切换数*80 000/CPU每秒时钟周期
如下,pidstat -w
每5s监控java进程9391。服务器处理器为3.0GHz双核CPU,每秒时钟周期为3 000 000 000
。按上述公式,每个虚拟处理器上下文切换为3500/2=1750
,耗费的时钟周期为1750*80 000=140 000 000
,所以上下文切换浪费的时钟周期百分比为140 000 000 /3 000 000 000 = 4.7%
。符合一般性准则(让步时钟周期占用3% ~ 5%或者更多),说明该Java应用正面临锁竞争。
使用Linux版nicstat,需要自行编译安装。
nicstat语法:
nicstat [-hnsz] [-i interface[,...]] | [interval [count]]
-h:显示帮助信息
-n:仅显示非本地接口
-s:显示概要信息
-z:跳过0值
-i interface:网络接口设备名
interval:报告输出的频率(秒级)
count:报告的采样数
对于有有磁盘操作的应用,查找性能问题应该监控磁盘I/O。Linux可用iostat -xm
监控磁盘I/O使用率和系统态CPU使用率。
启动HotSpot VM(JVM)的组件是启动器,JVM有若干个启动器。启动器启动JVM执行步骤:
应用程序对垃圾收集器的影响主要由3个方面:
重要的垃圾回收数据包括:
-XX:+PrintGCTimeStamps
:输出自JVM启动以来到垃圾收集之间流逝的秒数
-XX:+PrintGCDetails
:输出自JVM启动以来的秒数
-Xloggc:<filename>
:将垃圾收集的统计数据直接输出到文件,用于离线分析
-XX:+PrintGCApplicationConcurrentTime
:应用并发时间
-XX:+PrintGCApplicationSttoppedTime
:应用停止时间
利用这两个时间。可以报告应用在安全点操作之间的运行时间,有助于理解和量化延迟对JVM的影响,也可以用来辨别是JVM安全点操作还是应用程序引入的延迟。
显式垃圾收集容易识别,垃圾收集日志中会有特定文字(System)显式,说明System.gc()所引起
离线分析工具:GCHisto
当你想找出哪些方法被优化,或某些情况下的逆转化或重新转化时,监控JIT编译就有用了。
使用-XX:+PrintComplication
参数可以为每次编译生成一行日志,对JIT编译器进行监控。
JVM会把所有类的元数据信息都加载到永久代。
-XX:PermSize
和-XX:MaxPermSize
参数可调整永久代大小。为避免Full GC扩大或缩小永久代的可分配空间,两个参数可以设置为相同值 应用监控常用方法是查看日志。有些应用内建MBean,通过Java SE监控管理API进行监控。
jstack
命令可赚取线程信息,快速定位Java应用中的锁竞争。如下图,线程信息中粗体字waiting 通lock<0x22e88b10>(a Queue)
,表示正在等待锁,通过观察,该锁目前有线程ReadThead-33
持有。
Java性能优化可以归纳为以下几类:
对性能需求的这种分类为系统需求。系统需求关注应用程序运行时的特定方面:吞吐量、响应时间、内存消耗、启动时间、可用性、可管理性,等等。
JVM调优流程如下:
JVM提供的收集器:Serial收集器、Throughput收集器、Concurrent Mark-Sweep收集器、G1收集器。
垃圾收集性能的3个属性:
1、吞吐量:指不考虑垃圾收集硬气的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标
2、延迟:由于垃圾收集引起的停顿时间
3、内存占用:垃圾收集器流畅运行所需要的的内存数量
垃圾收集器调优3个基本原则:
1、Minor GC回收原则:每次Minor GC都尽可能多地收集垃圾对象
2、GC内存最大化原则:处理吞吐量和延迟问题时,垃圾处理器能用的内存越大,垃圾收集的效果越好,应用程序运行也越流畅
3、GC调优3选2原则:在吞吐量、延迟、内存占用3个属性中任意选择两个进行垃圾收集器的调优。(其中一个属性性能的提高几乎是以另一个或两个的性能属性的损失为代价。现实情况下,极少出现3个属性同等重要的情况)
JVM调优时使用的辅助命令参数:
-XX:PrintGCTimeStamps | 打印从JVM启动到GC开始所经历的时间 |
-XX:PrintGCDateStamps | 输出YYYY-MM-DDTHH-MM-SS.mmm-TZ格式的垃圾收集时间 |
-XX:PrintGCDetails | 打印垃圾收集器相关数据 |
-Xloggc:< filename > | 将GC日志记录到< filename >文件中 |
堆中活跃数据大小,是应用程序运行稳定时,Full GC后老年代和永久代占用的空间大小。
为更好度量活跃数据大小,最好在多次Full GC后查看堆的占用情况,同时要确保发生Full GC时应用程序处于稳定状态。正常情况下,应用不会频繁发生Full GC,可以通过jmap命令人工触发。命令:jmap -histo:live Java进程号
,进程号可以通过jps命令获得。
多次Full GC后,计算出Java堆活跃数据占用以及GC时间的平均值。根据活跃数据代销定义初始化Java堆大小,考虑Full GC的影响,推荐基于最差延迟进行估算。
Java堆初始值通用法则:
通用法则一:将初始值-Xms和最大值-Mmx设置为活跃数据大小的3~4倍
通用法则二:永久代的初始值-XX:PermSize和最大值-XX:MaxPermSize应该是活跃数据的1.2~1.5倍或更大
补充法则:新生代空间应为老年代空间活跃数据的1~1.5倍
综上,如果Java堆的初始值及最大值为活跃数据大小的3~4倍、新生代为活跃数据大小的1 ~ 1.5倍,那么老年代应设置为活跃数据大小的2 ~ 3倍。
1、从Throughput收集器迁移到CMS收集器时,需要遵守一个通用原则:将老年代空间增大20%~30%,这样才能更有效地运行CMS收集器
2、使CMS调优具有挑战性的几方面因素:对象从新生代提升到老年代的速率;并行老年代垃圾收集线程回收空间的速率;由于CMS收集器回收位于对象之间的垃圾对象而造成老年代空间的碎片化
3、指导原则:CMS包括Minor GC所带来的开销应小于10%;如果观察到CMS垃圾收集的开销在3%或者更少,说明通过调优吞吐量性能提升的空间极其有限
HotSpot Vm逃逸分析可以通过-XX:+DoEscapeAnalysis
命令行选项开启。
借助逃逸分析,HotSpot VM的JIT编译器可以应用以下的优化技术:
1、对象展开。一种在可能直接回收的空间而非Java堆上分配对象字段的技术
2、标量替换。一种减少内存访问的优化技术
3、栈上分配。一种在线程的栈帧上而非Java堆上分配对象的优化技术
4、消除同步。如果线程分配的对象不会发生逃逸,且该线程持有了该对象上的锁,由于其他线程不会访问该对象,这个锁可以通过JIT编译器移除
5、消除垃圾收集的读/写屏障。如果线程分配的对象不发生逃逸,该对象只能从线程本地的根节点访问,因此在其他对象中存储其他地址时不需要执行读或写屏障
-XX:+PrintComplication
选项通知JVM为每次它优化或逆优化的函数输出一条日志-XX:+PrintInlining
启动参数,可以看到哪些方法被编译器进行了内联优化