java性能调优

以下部分内容整理自《分布式Java应用:原理与实践》——林昊

随着系统自身数据量的增长,访问量增加,系统的响应通常会越来越慢,或者是新的功能在性能上无法满足修去,这个时候需要对系统进行性能调优。调优是一个复杂的过程,涉及的方面有:硬件,操作系统,运行环境软件和应用本身。

调优步骤:

调优前,首先要做的是衡量系统现状,这也是判断调优结果的依据。衡量系统状态一般包含以下几个方面:

  1. 单位请求次数
  2. 单次请求时间
  3. 资源消耗

例如A系统目前每秒钟请求次数1000次,平均响应时间为1秒。

在有了系统现状后,可设定调优目标。通常调优目标是根据用户所能接受的响应速度或者系统所拥有的机器以及所支撑的用户量制定出来的。因此通常会设定出调优目标:所有请求要在500ms内返回。

在设定了调优目标后,要做的就是寻找性能瓶颈,这一步通常是最难的,可以结合一些工具来找出造成瓶颈点的代码。

找到瓶颈点代码后,通常要分析其需求场景结合一些优化技巧,制定优化策略。优化策略通常也会有多个,如何选择?一般要收益比较高的(优化后的预期效果/优化需要付出的代价)。

优化后,部署项目,检查时候达成目标。如果已经达成,则结束本次调优;如果没有达成,则需要继续寻找瓶颈点,或者考虑之前没有考虑的优化策略,继续优化,直到达成目标。

寻找性能瓶颈

性能瓶颈的表现一般有资源消耗过多,外部处理系统的性能不足,或者资源消耗不多,但程序响应速度达不到要求。资源一般消耗在CPU,文件IO,网络IO以及内存方面。机器的资源是有限的,当某个资源消耗过多时,通常会造成系统的响应速度慢。

CPU消耗分析

在Linux中,CPU主要用于中断,内核以及用户进程的任务处理,优先级为中断>内核>用户进程。

重要概念

  • 上下文切换

每个CPU(或者多核CPU中的每核CPU)在同一时间通常只能执行一个线程,Linux采用的是抢占式调度,即为每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或者有高优先级线程要执行时,Linux将切换执行的线程,在切换时要存储当前线程的执行状态,并恢复要执行线程的状态,这个过程被称为上下文切换。对于Java应用,典型的是在文件IO操作、网络IO操作、锁等待或者线程Sleep时,当前线程会进入阻塞或者休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用相应速度下降。

  • 运行队列

每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状态,那么在分配平均的情况下,每个CPU核运行队列里就会有两个线程。通常而言,系统的load主要由CPU的运行队列来决定,假设以上情况维持了一分钟,那么这一分钟系统的load就是2。但load是一个复杂的值,因此这也不是绝对的,运行队列值越大,意味着线程要消耗越长对的时间才能执行完。Linux System and NetWork performance Monitoring 中建议控制在每个CPU核上的运行队列为1-3个。

  • 利用率

CPU利用率为CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分使用百分比,这五个值是用来分析CPU消耗情况的关键指标。Linux System and NetWork performance Monitoring 中建议用户进程的CPU消耗/内核的CPU消耗的比率在65%-70%/30%-35%左右。

检测命令

在Linux中,可通过top或者pidstat命令来查看CPU消耗状况。

  1. top

输入top命令后即可查看CPU的消耗情况,CPU的汇总信息如下:

1
2
3
4
5
top - 15:16:34 up 728 days,  1:33, 19 users,  load average: 0.39, 0.43, 0.59
Tasks: 576 total, 2 running, 567 sleeping, 5 stopped, 2 zombie
Cpu(s): 14.9%us, 0.7%sy, 0.0%ni, 84.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 264484524k total, 111250372k used, 153234152k free, 13148064k buffers
Swap: 16777208k total, 0k used, 16777208k free, 68996980k cached

此处,需要关注的是第三行信息,其中

  • 14.9%us 表示用户进程处理所占的百分比
  • 0.7%sy 表示内核线程处理所占的百分比
  • 0.0%ni 表示被nice命令改变优先级的任务所占百分比
  • 84.3%id 表示CPU空闲时间所占的百分
  • 0.0%wa 表示在执行过程中等待IO所占的百分比
  • 0.0%hi 表示硬件中断所占的百分比
  • 0.0%si 表示软件中断所占的百分比

对于多个或者多核CPU,上面的显示则会是多个CPU所占用的百分比综合,因此会出现180%us这样的现象。如果需要查看每个核心的消耗情况,可进入top后按 1,就会按核心来显示消耗情况。如下:

1
2
3
4
5
6
7
8
top - 15:31:12 up 23:49,  1 user,  load average: 0.03, 0.02, 0.00
Tasks: 211 total, 1 running, 210 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3050276 total, 1505612 free, 417000 used, 1127664 buff/cache
KiB Swap: 3176444 total, 3176444 free, 0 used. 2324580 avail Mem

默认情况下,top是按照进程来显示CPU消耗的,如果需要按照线程查看,可进入top后按 shift+h ,就会按照线程查看CPU消耗情况。如下:

1
2
3
4
5
6
7
8
9
10
top - 15:39:06 up 23:57,  1 user,  load average: 0.08, 0.06, 0.02
Threads: 357 total, 1 running, 356 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.1 sy, 0.0 ni, 99.9 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3050276 total, 1505032 free, 416576 used, 1128668 buff/cache
KiB Swap: 3176444 total, 3176444 free, 0 used. 2324896 avail Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1844 mysql 20 0 1239012 136296 15476 S 0.3 4.5 0:04.47 mysqld
2344 root 20 0 647140 38368 27164 S 0.3 1.3 0:15.53 dockerd
2422 root 20 0 647140 38368 27164 S 0.3 1.3 0:13.78 dockerd
  1. pidstat

pidstat 是 sysstat 中的工具,要使用 pidstat ,需要先安装 sysstat。

输入 pidstat 1 2,在console上将会每隔1秒输出目前活动进程的CPU消耗情况,共输出2次。

如果想查看某进程中线程的CPU消耗情况,可以输入 pidstat -p pid -t 1 5

总结:当CPU消耗严重时,主要体现在us 、sy 、wa 或hi 的值变高,wa的值是IO等待造成的,hi 的值变高主要是硬件中断造成的,例如网卡接收数据频繁。

java应用的CPU调优

对于java应用而言,CPU消耗严重主要体现在us 、sy两个值上。

  1. us

当us值过高时,表示运行的应用消耗了大部分的CPU。在这种情况下,对于Java应用而言,最重要的是找到具体消耗CPU的线程所执行的代码。可采用如下方法。

首先通过Linux命令找到CPU消耗严重的线程及ID,将次ID转为十六进制,之后通过kill -3 javapidjstack pid | grep 'nid=ID' 找出具体信息,前面两个命令需要多次使用,以确定找到真实消耗CPU的线程和代码。

  1. sy

当sy 值高时,表示 Linux花费了太多的时间在上下文切换上,Java应用造成这种情况的原因是启动的线程比较多,且这些线程多数处于要不断阻塞(例如锁等待,IO等待)和执行状态变化过程中,这就导致了操作系统需要不断地切换执行线程,产生大量的上下文切换。

文件IO消耗分析

Linux在操作文件时,将数据放入文件缓冲区,直到内存不够或系统要释放内存给用户进程使用。因此在查看Linux内存状况时,经常会发现可用(free)的物理内存不多,但是 cached 用了很多,这是Linux提升文件IO速度的一种做法。在这种做法下,如果物理空闲的内存够用,通常只有在写文件和第一次读取文件时才会产生真正的文件IO。

查看进程文件IO还是使用 pidstat 命令:pidstat -d -t -p pid 1 100 ,其中,KB_rd/s 表示每秒读取的KB数,KB_WR/s 表示每秒写入的KB数。

网络IO消耗分析

对于分布式java应用或使用微服务的应用,网络IO的消耗也值得关注。

在Linux中可采用sar来分析网络IO的消耗状况。sar -n ALL 1 2 执行后以1秒为频率一共输出两次网络IO的情况。

内存消耗分析

Java内存的消耗主要是在JVM堆内存上,在正式环境中,多数Java应用都会讲-Xms和-Xmx设置为相同的值,避免运行期间不断地申请内存。对于java内存消耗状况的分析,我会单独再写。

需要注意的是,在应用启动时设置的-Xmx的值对堆外内存是没有效果的。你可以使用DirectByteBuffer实现对堆外内存的操作,对应java的类为ByteBuffer

程序执行慢的原因分析

有些情况是资源消耗不多,但是程序仍然执行慢,这种现象多出现于访问量不是很大,消耗多是应用内耗产生的。原因主要有以下三种。

  1. 锁竞争激烈

例如数据库连接池提供的连接数通常是有限的,假设提供了10个连接,但如果此时有50个 线程要进行数据库操作,那么就会造成另外的40个线程处于等待状态。

  1. 未充分使用硬件资源

例如机器是四核的,但程序都是单线程串行操作,并没有充分发挥硬件资源的作用,此时可以进行优化来充分使用硬件资源,提升程序执行速度。

  1. 数据量增长

例如数据库中单表的数据由100w上涨到了10000w,数据库读写速度大幅度下降。

对于以上情况,可以使用jprofile等商业工具进行分析,从而找到执行耗时比率较大的代码。

调优

找到系统性能瓶颈后,接下来就是调优了。调优可以从硬件、操作系统、JVM和程序四个方面着手。硬件和操作系统的调优可以参考计算机系统等相关书籍,接下来主要探讨的是JVM和程序方面的调优。

JVM调优

JVM调优主要是内存管理方面你的调优,包括各个代大小,GC策略等。由于GC会影响应用线程,严重影响性能,这些调优对于应用而言还是很重要的。根据应用的情况选择不同的内存管理策略有些时候能够降低GC导致的应用暂停时间,大幅度地提升应用性能,尤其是对于内存消耗较多的应用。

代大小调优

在不采用G1(JDK1.7提供的不区分年轻代和老年代的垃圾收集器)的情况下,通常Minor GC会远快于Full GC,因为Minor GC 采用的是标记复制算法,Full GC采用的是标记整理/标记清除算法,后者的复杂度比前者的复杂度高。

在代大小调优上,最关键的参数有以下几点:

  • -Xms,-Xmx 通常设置为相同的值,避免运行时要不断地扩展JVM内存空间,这个值决定了Heap所使用的最大空间。
  • -Xmn 决定了新生代(New Generation)空间的大小。
  • -XX:SurvivorRatio 是控制新生代Eden、S0、S1三个区域的比率,例如比值是 8:1:1 时,值为8
  • -XX:MaxTenuringThreshold 控制对象在经历多少司Minor GC 后才转入老年代,通常又将此值成为新生代存活周期,此参数只有在串行GC(-XX:+UseSerialGC)时有效,其他GC方式则有Sun JDK自行决定。

  • 当新生代过小时,Minor GC的次数更加频繁,大对象更容易进入老年代,从而触发Full GC。

  • 当新生代过大时,老年代变小,有可能导致Full GC频繁,Minor GC耗时也会增加。
  • 当Survivor Space(S0/S1)过大时,Minor GC次数增加,大对象不容易进入老年代。过小时,Minor GC次数变少,但大对象更容器直接进入老年代。
  • 当新生代存活周期数增加,对象在Minor GC阶段回收的机会就增加了,但是Survivor Space 更容易被占用。

GC策略调优

串行GC性能较差,像 Serial 在收集和复制时都是一个线程,Parallel 在收集是多个线程,复制时是一个线程,一般都不会使用Serial GC。一般情况下,我们使用CMS收集器,使用方式如下:

1
-XX:+UseConcMarkSweepGC  -XX:+UseCMSCompactAtFullCollection -XX:CMSMaxAbortablePrecleanTime=5 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplication-StoppedTime

程序调优

CPU消耗严重

当us 高说明执行线程无任何挂起动作,且一直执行,对于这种情况,可以在方法中增加Thread.sleep,释放CPU的执行权

当sy 高说明线程切换太过频繁,最简单的优化就是减少线程数。也可以减少线程间通信,降低锁竞争,或者采用协程代替线程。

IO消耗严重

  • 异步写文件
  • 批量读写
  • 限流
  • 限制文件大小

网络IO消耗严重

限流,限制发送packet的频率。

内存消耗严重

  • 释放不必要的引用
  • 使用对象缓存池
  • 采用合理的缓存时效方法(FIFO,LRU,LFU)
  • 合理使用SoftReference和WeakReference(前者在内存不够用的时候回收,后者在Full GC时立刻回收)

资源消耗少,程序运行慢调优

锁竞争激烈

容易导致CPU sy,即系统上下文切换频繁导致内核线程运行占比较高

解决方法:

  1. 使用无阻塞的并发类实现阻塞式的并发类(基于CAS ),例如使用ConcurrentLinkedQueue来代替BlockingQueue。
  2. 尽量少用锁
  3. 拆分锁

未充分使用CPU

多用并行处理

JVM调优常用参数命令

输出日志

在jvm启动参数中加入以下参数

1
2
// 以下含义分别是 输出GC信息,输出GC简要信息,输出GC详细信息,输出GC的时间信息,输出GC造成的应用暂停时间,将GC日志输出到gc.log文件中
-verbose:gc -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime -Xloggc:gc.log

查看运行时状态

  • jmap

-heap

打印heap的概要信息,GC使用算法,heap的配置及使用情况

1
jmap -heap pid

Heap Configuration参数中,MinHeapFreeRatio对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40),MaxHeapFreeRatio对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)

-histo

打印堆的对象统计,包括对象数,内存大小等。

1
jmap -histo:live pid | grep more

附 - jmap输出中class name非自定义类的说明:

BaseType Character Type Interpretation
B byte signed byte
C char Unicode character
D double double-precision floating-point value
F float single-precision floating-point value
I int integer
J long long integer
L; reference an instance of class
S short signed short
Z boolean true or false
[ reference one array dimension,[I表示int[]
  • jstat

-gc

垃圾回收统计

1
jstat -gc pid
  • S0C:第一个幸存区的大小
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • EC:伊甸园区的大小
  • EU:伊甸园区的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

**-gcutil

总结垃圾回收统计

1
jstat -gcutil pid
  • S0:幸存1区当前使用比例
  • S1:幸存2区当前使用比例
  • E:伊甸园区使用比例
  • O:老年代使用比例
  • M:元数据区使用比例
  • CCS:压缩使用比例
  • YGC:年轻代垃圾回收次数
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

参考

Java SE 技术

Java SE HotSpot 概览

Java HotSpot VM Options

Java EE7 技术

Java Platform SE 7 API

Memory Management in the Java HotSpot™ Virtual Machine

关于Java HotSpot VM的常见问题解答