寻找性能瓶颈
CPU消耗分析
基本概念
上下文切换:切换执行线程时,存储目前线程的执行状态,恢复要执行的线程的状态
运行队列:每个CPU核将要执行的线程
利用率:CPU在用户进程、内核、中断处理、IO等待、空闲,五个部分使用百分比
指令
1:top:查看CPU的消耗情况,按1按照核来显示消耗情况,按shift+h按线程查看CPU消耗情况
us:用户进程 sy:内核线程 ni:nice命令改变优先级 id:空闲时间 wa:等待io
hi:硬件中断 si:软件中断
2:pidstat:输入pidstat 1 2,在console上每隔1秒输出目前活动进程的CPU消耗情况,共输出2次
常见问题
1:us:
首先找到具体消耗CPU的线程所执行的代码
1:找到消耗CPU严重的线程及其ID,将ID转化为十六进制
2:通过kill -3 [javapid]或jstack的方式dump出应用的java线程信息(jstack [pid] | grep 'nid=0x6849')
3:通过ID找到对应的nid值的线程
原因:
1:线程一直属于可运行(Runnable)状态
2:频繁的GC
2:sy:
sy值高,表示Linux花费了更多的时间在进行线程切换
原因:启动的线程过多,且线程多数处于不断阻塞和执行状态的变化过程中,产生大量上下文切换
解决:通过kill -3 [javapid]或jstack -1 [javapid]方式dump出Java应用程序的线程信息,找出等待状态或锁竞争过多的线程
文件IO消耗分析
跟踪线程文件IO消耗的方法
1:pidstat:pidstat -d -t -p [pid] 1 100 类似的命令查看线程的IO消耗状况
2:iostat:查看各个设备的IO历史状况,可以输入iostat -x xvda 3 5定时采样查看IO的消耗状况
此时首要关注的是CPU中的iowait%所占的百分比
当IO消耗过高时
1:通过pidstat直接找到文件IO操作多的线程
2:结合jstack找到对应的Java代码
原因
多个线程需要大量内容写入的动作,或磁盘设备本身处理速度慢,文件系统慢,操作的文件本身很大造成的
网络IO消耗分析
采用sar分析网络IO的消耗状况
信息有三部分
第一部分为网卡成功接包和发包的信息
第二部分为网课失败接包和发包的信息
第三部分为sockets上的统计信息
对java而言,主要使用tcpsck和udpsck
当网络IO消耗高时,对线程dump,查找产生大量网络IO操作的线程
Java实现网络通信时,要将对象序列化为字节流进行发送,或读取字节流进行反序列化为对象,这个过程要消耗JVM堆内存
堆内存大小通常有限,所以一般不会造成网络IO消耗严重
内存消耗分析
JVM堆以外的内存方面的消耗
关注swap的消耗以及物理内存的消耗
查看内存消耗的方式:
1:vmstat:关注memory下的swpd、free、buff、cache以及swap下的si、so
swpd:虚拟内存已使用的部分,过高就是物理内存不够用
free:空闲的物理内存
buff:用于缓冲的内存
cache:作为缓存的内存
si:每秒从disk读至内存的数据量
so:每秒从内存中写入disk的数据量
2:sar:
通过sar的-r参数可以查看内存的消耗状况
kbswpfree:表示swap空闲的大小
kbswpused:表示已使用的swap大小
%swpused:表示使用的swap空间比率
系统中可用的物理内存为:kbmemfree+kbuffers+kbcached
sar相比vmstat的好处是可用查询历史状况,更加准确分析趋势状况
共同弱点是不能分析进程所占用的内存量
3:top:可用查看进程所消耗的内存量,不管看到的进程的消耗内存是包括JVM已分配的内存加上加上Java应用所耗费的JVM以外的物理内存
4:pidstat:可用查看进程消耗的内存量,命令格式为pidstat -r -p [pid] [interval] [times]
最佳的分析组合是结合top或pidstat,以及JVM的内存分析工具来共同分析内存的消耗状况
内存消耗过多一般解决思路:
1:分析耗费的是JVM外的物理内存还是JVM的堆
2:如果是物理内存,分析线程数量以及Direct ByteBuffer使用情况
3:如果是堆,结合分析工具来分析程序中具体对象的内存占用情况
资源消耗少,程序执行慢
1:锁竞争激烈:多数线程处于等待状态
2:未充分使用硬件资源:双核CPU,但程序都是单线程串行操作
3:数据量增长:数据库读写速度下降
调优
四个方面:硬件、操作系统、JVM、程序
JVM调优
代大小的调优
前缀知识:
-Xmx:设置JVM最大可用内存
-Xms:设置JVM初始内存
-Xmn:设置年轻代大小
-XX:NewRatio=a:设置年轻代与老年代的比值,老年代占比为a
-XX:SurvivorRatio=b:设置Survivor区与Eden区的比值,Eden区占比为b
-XX:MaxPermSize=c:设置持久代大小为c
-XX:MaxTenuringThreshold=d:设置垃圾最大年龄为d
1:避免新生代设置过小
一、minor GC次数频繁
二、minor GC对象直接进入老年代,占据老年代剩余空间,触发Full GC
2:避免新生代设置过大
一、老年代变小,导致Full GC频繁
二、minor GC耗时增加
3:避免Survivor区过小或过大
Survivor区大的话,可以存储更多在minor GC后仍存活的对象 避免其进入旧生代
但是Eden区变小,minor GC触发次数增加
Eden区大的话,minor GC触发次数降低,但是Survivor区变小,有超过Survivor区大小的对象在minor GC后没有被回收的话,就会直接进入老年代
4:合理设置新生代存活周期
增大存活周期后,对象在Minor GC阶段被回收的机会就增加了,但同时带来的是survivor区被占用
控制代大小指令的总结:
1:内存不够用时可以使用-Xms、-Xmx调整整个JVM堆的大小,能多大取决于操作系统和CPU
2:-Xmn让新生代变大,使得多数对象在minor GC阶段被回收,但意味着更频繁的Full GC
3:-XX:SurvivorRatio让Eden区变大,降低minor GC频率,但对没被会受到对象容易直接进入老年代。让Survivor区变大可以避免对象频繁直接进入老年代。
GC策略调优
看书p200页的案例:
213秒内发生了一次Full GC,不正常,根据分析发现大部分老年代对象可以回收,说明他们只要在新生代多活一段时间,就可以被minor GC回收,所以,扩大survivor区,最好是直接扩大堆内存。
第二种方法是GC策略调整为CMS GC,可以在发生Full GC前回收大部分新生代转入老年代的对象
总结
如果不是有确切的GC造成性能低的理由,就没必要做过多细节方面的调优,多数情况下只需选择GC策略并设置JVM堆大小即可
程序调优
1:CPU us高
原因:执行线程无任何挂起动作
优化方法:对这种线程的动作增加Thread.sleep,以损失单次执行性能为代价
还有一种情况是状态的扫描,等其他线程改变值后才能继续执行,这时采用wait/notify机制
2:CPU sy高
原因:线程的运行状态经常切换
优化方法:减少线程数
如果应用要支撑大量的并发,在减少线程数的情况下最好增加一个缓冲队列,避免线程数的减少造成系统出错率上升
原因二:线程之间锁竞争激烈
优化方法:降低锁竞争,如果线程数过多,调优后可能造成us值过高
原因三:网络IO或确实需要锁竞争,需要支撑高并发
优化方法:协程,Kilim框架 ,在线程阻塞时,不浪费相对昂贵的原生线程资源
Kilim承担了线程的调度以及上下文切换动作,带来了线程使用率的提升,但是在JVM堆中保存Task上下文信息,消耗更多的内存
3:文件IO消耗严重
原因:多个线程在写大量数据到同一文件,文件变大,写入速度变慢,各线程争抢文件锁
调优方法:异步写,批量读写,限流,限制单个文件大小,采用缓冲区避免与操作系统的频繁交互
4:网络IO消耗严重
原因:需要发送或接收的包太多
调优方法:限制发送packet的频率
5:内存消耗严重
1.释放不必要的引用
2.使用对象缓存池
3.采用合理的缓存失效算法
如果放入太多对象在缓存池中,反而会造成内存的严重消耗,要合理控制缓存池的大小
4.合理使用软引用和弱引用
6:资源消耗少,程序执行慢
1:锁竞争激烈
1.使用并发包中的类减少竞争
2.尽可能少用锁
3.拆分锁
4.去除读写操作的互斥锁
2:未充分发挥硬件资源
未充分使用CPU:在能并行处理的场景中未使用足够的线程
未充分使用内存:在内存资源消耗可接受的范围内使用缓存