Java应用性能调优实践

本文是LZ在网上看到的文章,LZ觉得写得非常好,很有实际意义,可以作为以后的一个调优指导,故将文章搬到这里,如有侵权,欢迎与我联系,原文地址:https://www.ibm.com/developerworks/cn/java/j-lo-performance-tuning-practice/index.html

Java应用性能优化是一个老生常谈的话题,典型的性能问题如页面相应慢、接口超时、服务器负载高、并发数低、数据库频繁死锁等。尤其是“糙快猛”的互联网开发模型大行其道的今天,随着系统访问量的日益增加和代码的臃肿,各种性能问题开始纷至沓来。Java应用性能的瓶颈点非常多,比如磁盘、内存、网络I/O等系统因素,Java代码,JVM GC,数据库,缓存等。

笔者根据个人经验,将Java性能优化分为4个层级:应用层,数据库层,框架层,JVM层。每层的优化程度逐级增加,设计的知识和解决的方式也尽不相同。比如应用层需要理解代码逻辑,通过Java线程栈定位有问题代码行等;数据库层面需要分析SQL,定位死锁等;框架层需要懂源代码,理解框架机制;JVM层需要对GC的类型和的工作机制有深入了解,对各种JVM参数了然于胸。

围绕Java性能优化,有两种基本的分析方法:现场分析法和事后分析法。现场分析法通过保留现场,再采用诊断工具分析定位。现场分析对线上影响比较大,部分场景不太合适。事后分析法需要尽可能多的收集现场数据,然后立即恢复服务,同时针对收集的现场数据进行事后分析和复现。

下面我们从性能诊断工具出发,分享一些工作中遇到的真实案例。

性能诊断工具

性能诊断一种是针对已经确定有性能问题的系统和代码进行诊断,还有一种是对预上线系统提前性能测试,确定性能是否符合上线要求。本文主要针对前者,后者可以使用各种性能压测工具进行压测,比如JMeter等,但这不在本文讨论范围。针对Java应用,性能诊断工具主要分为两层:OS层面和Java应用层面。

OS诊断

OS诊断主要关注的是CPU,Memory,I/O三个方面。

CPU诊断

对于CPU主要关注平均负载(Load Average),CPU使用率,上下文切换次数(Context Switch)。

通过top命令可以查看系统平均负载和CPU使用率,输入o之后输入字段可以进行倒序排序
top -pid 查看某个进程的系统负载情况和cpu使用率

1
2
3
4
5
6
7
8
9
➜  ~ top -pid 1492
Processes: 374 total, 3 running, 371 sleeping, 1518 threads 10:46:03
Load Avg: 2.19, 2.15, 2.12 CPU usage: 3.61% user, 3.37% sys, 93.1% idle
SharedLibs: 254M resident, 51M data, 25M linkedit.
MemRegions: 93149 total, 3736M resident, 106M private, 1037M shared.
PhysMem: 10G used (2500M wired), 5673M unused.
VM: 2268G vsize, 1313M framework vsize, 14421757(64) swapins, 15490743(0) swapou
Networks: packets: 32037356/29G in, 20370581/3792M out.
Disks: 3967796/117G read, 5007257/171G written.

平均负载 Load AVG 有3列数字2.19, 2.15, 2.12,分别表示过去1分钟,5分钟,15分钟机器的负载。按照经验,若数值小于0.7*CPU个数,则系统工作正常。这台机器是2个CPU,15分钟内平均负载为2左右,这个其实还好,如果是四五倍的话那这个就负载比较高了,这就需要定位具体的原因了。

通过vmstat命令可以查看CPU的上下文切换次数,上下文切换场景主要有如下几种:

  1. 时间片用完,CPU正常调度下一个任务。
  2. 被其他优先级更高的任务抢占。
  3. 执行任务碰到I/O阻塞,挂起当前任务,切换到下一个任务。
  4. 用户代码主动挂起当前任务让出CPU。
  5. 多任务抢占资源,由于没有抢到被挂起。
  6. 硬件中断。

Java线程上下文切换主要来自共享资源的竞争,一般单个对象加锁很少成为系统瓶颈,除非锁粒度过大。但在一个访问频度高,对多个对象连续加锁的代码块中就容易出现大量的上下文切换,成为系统瓶颈。比如在我们的系统中就出现过log4j1.x在较大并发下大量打印日志,出现频繁上下文切换,大量线程阻塞,导致系统吞吐量大降的情况,其代码如下,升级到log4j2.x才解决这个问题。

1
2
3
4
5
6
7
8
for(Category c = this; c != null; c=c.parent) {
// Protected against simultaneous call to addAppender, removeAppender,…
synchronized(c) {
if (c.aai != null) {
write += c.aai.appendLoopAppenders(event);
}
}
}

这也给我们平时写代码的时候提了个醒,synchronized尽量放在for循环外面。

Memory

从操作系统角度,内存关注应用进程是否足够,可以使用free -m命令查看内存的使用情况。通过top命令可以查看进程使用的虚拟内存VIRT和物理内存RES,根据公式RES = VIRE + SWAP可以推算出具体应用使用的交换分区(Swap)情况,使用交换分区过大会影响Java应用性能,可以将Swap值尽可能调小。因为对于Java应用来说,占用太多交换分区可能会影响性能,毕竟磁盘的性能比内存慢太多。

I/O

I/O包括磁盘I/O和网络I/O,一般情况下磁盘更容易出现I/O瓶颈。通过iostat可以查看磁盘的读写情况,通过CPU的I/O wait可以看出磁盘I/O是否正常。如果磁盘I/O一直处于很高的状态,说明磁盘太慢或者故障,成为了性能瓶颈,需要进行应用优化或者更换磁盘。

Java应用诊断工具

应用代码诊断

应用代码性能问题是相对好解决的一类性能问题。通过一些应用层面监控报警,如果确定有问题的功能和代码,直接通过代码就能定位;或者通过top + jstack 就可以定位,找出有问题的线程栈,定位到问题线程的代码上,也可以发现问题。对于更复杂,逻辑更多的代码段,通过Stopwatch打印性能日志往往也可以定位大多数应用代码性能问题。

常用Java应用诊断包括线程、堆栈、GC方面的诊断。

jstack

jstack命令通常配合top使用,通过top -H -p <pid>定位为Java进程和线程,再利用jstack -l <pid>导出线程栈。由于线程栈是瞬态的,因此需要多次dump,一般3次dump,每次间隔5秒。将top定位的Java线程pid转换成16进制,得到Java线程栈中的nid,可以站到对应的问题线程栈。

文章目录
  1. 1. 性能诊断工具
    1. 1.1. OS诊断
      1. 1.1.1. CPU诊断
      2. 1.1.2. Memory
      3. 1.1.3. I/O
    2. 1.2. Java应用诊断工具
      1. 1.2.1. 应用代码诊断
        1. 1.2.1.1. jstack
|
c