JAVA应用常见性能问题分析与优化
1. 性能分析基本流程
1.1 前提条件
- 测试方案中的指标:性能是否通过的标准之一
- 性能监控中的数据:性能分析的依据
1.2 性能问题的分类
- 响应慢
- 进程CPU飙高,load高
- load高,CPU低
- 无响应
- 大量失败, CPU低, load低
- TPS上不去
- 响应较快,但TPS较低(CPU高,load低)
- 内存泄漏
- 内存溢出
- GC频繁
1.3 案例
1.3.1 20路并发下,某接口的业务指标如下,对每个指标进行分析
- 事务失败占比–0%
- 本接口未出现失败数, 说明当前所有事务的返回结果 符合预期断言
- 如果有失败的情况, 需要结合最大响应时间判断,失败原因是因为超时还是业务处理失败
- 对于业务处理的失败,需要到业务日志中去查找,可能是因为并发导致异常
- TPS 31.5次/秒
- 1000/平均响应时间*并发数 ~ TPS
- 通过这个关系明确负载机, 性能脚本之间是否存在问题
- 期望值是: TPS > 并发数
- 平均响应时间 632ms(本公司要求指标是: 该路数并发下90%的响应时间)
- 以测试方案中的指标作为参考, 形成基本判断, 再分析响应构成
- 平均响应时间 > 方案中期望的时间: 不满足性能要求, 需要分析排查性能问题或者增加机器
- 平均响应时间 <= 方案中期望的时间: 满足性能要求, 但仍要分析排查是否有性能问题
- 平均响应时间 = 网络传输时间 + 系统处理时间
- 系统处理时间依赖业务日志统计: 平均响应时间 - 系统处理时间
如果差距很大:
(1)测试环境网络问题
(2)带宽限制问题
如果差距很小
(1)分析系统处理时间
- 被测接口内部逻辑:
- 统计整体耗时和重要RPC耗时
- 耗时逻辑分析
- 外部调用耗时, 如数据库, 后端服务, 外部地址, 需要判断合理性
- 注意返回书籍大小对耗时的影响(测试环境可能出现带宽限制)
- 内部逻辑耗时, 深入到具体的方法调用, 比如序列化和反序列化
- 比如java处理json数据的三种方法: FastJSON, Gson和Jackson
-
序列化(object ==> json)
-
反序列化(json ==> object)
-
- 接着从资源指标入手来分析
- 统计整体耗时和重要RPC耗时
2. java应用内存分析以及优化
2.1 Java内存分配主要包括以下几个区域:
- 栈: 存放基本类型的数据和对象的引用, 但对象本身不存放在栈中, 而是存放在堆中
- 堆: 存放用new产生的数据
- 静态域: 存放在对象中用static定义的静态成员
- 常量池: 存放常量
- main方法开始执行: int date = 9; data局部变量, 基础类型, 引用和值都存在栈中
- Test test = new Test(); test为对象引用, 存在栈中, 对象(new Test())存在堆中
- test.change(date); i为局部变量, 引用和值存在栈中, 当方法change执行完成后, i就会从栈中消失
- BirthDate d1 = new BirthDate(7,7,1970); d1为对象引用, 存在栈中, 对象(new BirthDate())存在堆中, 其中d, m, y为局部变量存储在栈中, 且他们的类型为基础类型, 因此他们的数据也存储在栈中, day, month, year为成员变量, 他们存储在堆中(new BirthDate()里面). 当BirthDate构造方法执行完之后, d, m, y将从栈中消失
- main方法执行完之后, date变量, test, d1引用将从栈中消失, new Test(), new BirthDate()将等待辣鸡回收
2.2 Java常见的内存问题表现形式
- OutOfMemory: 内存溢出: 是指程序在申请内存时, 没有足够的内存空间供其使用
- MemoryLeak: 内存泄漏: 始终程序在申请内存后, 无法是否已申请的内存空间
2.2.1 导致OutOfMemoryError异常的常见原因有以下几种
- 内存中加载的数据量过于庞大, 如: 一次从数据库中取出过多数据
- 集合类中有对对象的引用, 使用完成后未清空, 使得jvm不能回收
- 代码中存在死循环或者循环过程中产生过多重复的对象实体
- 使用第三方软件中的BUG
- 启动参数内存值设定的过小
(1)内存泄漏是导致内存溢出的原因之一, 内存泄漏积累起来将导致内存溢出
(2)内存泄漏可以通过完善代码来避免, 内存溢出可以通过调整配置来减少发生频率, 但无法彻底避免
2.3 内存溢出类型
虚拟机栈溢出, 本地方法栈溢出, 方法区溢出, 堆溢出, 运行时常量池溢出
2.3.1 异常类型
- java.lang. OutOfMemoryError: Java heap space
堆内存溢出
优化: 通过-Xmm(最小值) -Xms(初始值) -Xmx(最大值) 参数手动设置Heap(堆)的大小 - java.lang.OutOfMemoryError: PermGen space
PermGen Space溢出(方法区溢出, 运行时常量池溢出)
优化: 通过MaxPermSize参数设置PermGen apsce大小 - java.lang.StackOverflowError
栈溢出(虚拟机栈溢出, 本地方法栈溢出)
优化: 通过Xss参数调整
2.3.2 堆内存分析
- 收集堆内存. 命令: jmap -dump:format=b,file=heap.dump <pid>
或者 JVisualVM
- 使用工具分析堆内存。 工具:MAT-elipse插件。
点击了"Details"链接之后,除了在上一页看到的描述外,还有Shortest Paths To the Accumulation Point和Accumulated Objects部分,这里说明了从GC root到聚集点的最短路径,以及完整的reference chain
Analysis: IBM HeapAnalyzer ( ha456.jar )
Analysis: jmap –histio:live | more
3. Java应用CPU问题分析优化
现象: CPU占用高, 程序响应慢
要了解本身是否为CPU密集型, 比如大量的计算等. 被测服务器的核心数
3.1 分析方法
- 保存dump文件。
jstack <pid> >> thread.dump - 找到导致CPU高的线程。
top -H -p <pid> - Pid 十进制转十六进制
http://tool.oschina.net/hexconvert/ - 找到对应的线程,打开 thread.dump文件
查找:按十六进制值
找到对应的线程,把相关的方法找出来,可以精确到代码的行.
Jvisualvm CPU 抽样器
4. Java应用线程问题分析优化
- 保存堆栈信息。
Jstack >> thead.dump - 分析线程状态
cat thead.dump | grep ‘java.lang.Thread.State’ | awk ‘{print $2$3$4$5}’ | sort |
uniq -c
4.1 线程状态
- RUNNABLE: 线程正在执行中,占用了资源,比如处理某个请求/进行
计算/文件操作等 - BLOCKED/Waiting to lock(需关注)
- 线程处于阻塞状态,等待某种资源(可理解为等待资源超时的线程);
- “waiting to lock <xxx>”,即等待给xxx上锁,grep stack文件找locked <xxx> 查找获得锁的线程;
- “waiting for monitor entry” 线程通过synchronized(obj){……}申请进入了临界区,但该obj对应的monitor被其他线程拥有,从而处于等待。
- WAITING/TIMED_WAITING{定时}(关注)
- Deadlock(需关注):死锁,资源相互占用
- IO阻塞(程序表现为响应慢,Load高)
线程状态为“in Object.wait()”,说明正在等待线程池可用资源,由于线程池满导致新的IO请求处于排队等待状态,且发生在:at com.iflytek.diange.data.provider.sendsong.impl.SendSongImpl.getSendSongInfosByUserId(SendSongImpl.java:92)行
- 死锁(程序表现为无响应)
线程状态为“waiting to lock”: 两个线程各持有一个锁,又在等待另一个锁,故造成死锁,且发生在DeadLockTest.java:39行