Java 应用性能优化是一个程序员必须要考虑的问题,典型的性能问题如页面响应慢、接口超时,服务器负载高、并发数低,数据库频繁死锁等。
Java应用性能的瓶颈点非常多,比如磁盘、内存、网络 I/O 等系统因素,Java 应用代码,JVM GC,数据库,缓存等。可以将 Java 性能优化分为 4 个层级:应用层、数据库层、框架层、JVM 层,如图 1 所示。
图 1.Java 性能优化分层模型
每层优化难度逐级增加,涉及的知识和解决的问题也会不同。比如应用层需要理解代码逻辑,通过 Java 线程栈定位有问题代码行等;数据库层面需要分析 SQL、定位死锁等;框架层需要懂源代码,理解框架机制;JVM 层需要对 GC 的类型和工作机制有深入了解,对各种 JVM 参数作用了然于胸。
1 CPU 诊断
对于 CPU 主要关注平均负载(Load Average),CPU 使用率,上下文切换次数(Context Switch)。
通过 top 命令可以查看系统平均负载和 CPU 使用率,图 2 为通过 top 命令查看某系统的状态。
图 2.top 命令示例
平均负载有三个数字:63.66,58.39,57.18,分别表示过去 1 分钟、5 分钟、15 分钟机器的负载。按照经验,若数值小于 0.7*CPU 个数,则系统工作正常;若超过这个值,甚至达到 CPU 核数的四五倍,则系统的负载就明显偏高。
图 2 中 15 分钟负载已经高达 57.18,1 分钟负载是 63.66(系统为 16 核),说明系统出现负载问题,且存在进一步升高趋势,需要定位具体原因了。
通过 vmstat 命令可以查看 CPU 的上下文切换次数:
上下文切换次数发生的场景主要有如下几种:
• 1)时间片用完,CPU 正常调度下一个任务;
• 2)被其它优先级更高的任务抢占;
• 3)执行任务碰到 I/O 阻塞,挂起当前任务,切换到下一个任务;
• 4)用户代码主动挂起当前任务让出 CPU;
• 5)多任务抢占资源,由于没有抢到被挂起;
• 6)硬件中断。
Java 线程上下文切换主要来自共享资源的竞争。一般单个对象加锁很少成为系统瓶颈,除非锁粒度过大。但在一个访问频度高,对多个对象连续加锁的代码块中就可能出现大量上下文切换,成为系统瓶颈。
比如在我们系统中就曾出现 log4j 1.x 在较大并发下大量打印日志,出现频繁上下文切换,大量线程阻塞,导致系统吞吐量大降的情况,其相关代码如清单 1 所示,升级到 log4j 2.x 才解决这个问题。
2 Memory
从操作系统角度,内存关注应用进程是否足够,可以使用 free –m 命令查看内存的使用情况。
通过 top 命令可以查看进程使用的虚拟内存 VIRT 和物理内存 RES,根据公式 VIRT = SWAP + RES 可以推算出具体应用使用的交换分区(Swap)情况,使用交换分区过大会影响 Java 应用性能,可以将 swappiness 值调到尽可能小。
因为对于 Java 应用来说,占用太多交换分区可能会影响性能,毕竟磁盘性能比内存慢太多。
3 I/O
I/O 包括磁盘 I/O 和网络 I/O,一般情况下磁盘更容易出现 I/O 瓶颈。通过 iostat 可以查看磁盘的读写情况,通过 CPU 的 I/O wait 可以看出磁盘 I/O 是否正常。
如果磁盘 I/O 一直处于很高的状态,说明磁盘太慢或故障,成为了性能瓶颈,需要进行应用优化或者磁盘更换。
4 Java 应用诊断及工具
应用代码性能问题是相对好解决的一类性能问题。通过一些应用层面监控报警(UMP),如果确定有问题的功能和代码,直接通过代码就可以定位;或者通过 top+jstack,找出有问题的线程栈,定位到问题线程的代码上,也可以发现问题。对于更复杂,逻辑更多的代码段,通过 Stopwatch 打印性能日志往往也可以定位大多数应用代码性能问题。
常用的 Java 应用诊断包括线程、堆栈、GC 等方面的诊断。
jstack
jstack 命令通常配合 top 使用,通过 top -H -p pid 定位 Java 进程和线程,再利用 jstack -l pid 导出线程栈。由于线程栈是瞬态的,因此需要多次 dump,一般 3 次 dump,一般每次隔 5s 就行。将 top 定位的 Java 线程 pid 转成 16 进制,得到 Java 线程栈中的 nid,可以找到对应的问题线程栈。
JProfiler
JProfiler 可对 CPU、堆、内存进行分析,功能强大.