目录
当我们的系统的响应变慢或者在性能上无法满足需求时,我们就需要考虑对系统进行性能调优,一般情况,我们调优的步骤是:
通常情况下,造成性能瓶颈的原因主要两方面原因:
1)资源消耗过多、外部处理系统的性能不足
资源主要消耗在CPU、IO(文件IO、网络IO)以及内存(JVM内存、物理内存等)方面,机器的资源是有限的,当某资源消耗过多时,通常会造成系统的响应变慢。
外部处理系统的性能不够主要是所调用的其它系统提供的功能或者数据库操作的响应速度不够。
2)资源消耗不多,但程序的响应速度仍达不到要求
这种情况的主要原因程序代码运行效率不够高、未充分使用资源或者程序的结构不合理。
脉络图:
1、CPU层
1.1 前置知识
在Linux中,CPU主要用于中断、内核以及用户进程的任务处理,优先级为中断>内核>用户进程。
1)上下文切换
每个CPU(或多核CPU中的每核CPU)在同一时间只能执行一个线程,Linux采用的是抢占式调度。
上下文切换过程:
当一个CPU上有多个线程并发执行时,会给每个线程分配一定的执行时间,当到达执行时间、线程中有阻塞或高优先级线程要执行时,Linux将切换执行的线程,在切换时要存储目前线程的执行状态,并恢复要执行线程的状态。
在Java应用中,在进行IO操作、锁等待或线程Sleep时,当前线程会进入阻塞或者休眠状态,就会触发上下文切换。但是,上下文切换过多会造成内核占据较多的CPU使用,导致应用性能下降。
2)运行队列
每个CPU核都维护了一个可运行的线程队列。也就是分配给在每个CPU的运行的线程。
3)利用率
CPU利用率:CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分的使用百分比。
CPU us:用户进程处理所占的百分比
CPU sy:内核线程处理所占的百分比
CPU ni:被nice命令改变优先级的任务所占的百分比
CPU id:CPU空闲时间所占的百分比
CPU wa:执行过程中等待IO所占的百分比
CPU hi:硬件中断所占的百分比
CPU si:软件中断所占的百分比
1.2 CPU的消耗问题
对于Java应用而言,CPU的消耗主要体现在us和sy两个值上面。
1、CPU us值过高
1)问题:
我们的Java应用消耗了大部分CPU,进而导致CPU us值过高。
2)问题的原因:
-
主要原因:执行线程无任何挂起动作,且一直执行,导致CPU没有机会去调度其它的线程,造成线程饿死的现象(通常是线程在执行无阻塞动作造成的)。
-
其它原因:频繁的GC;程序循环次数太多、正则或者纯粹的计算等造成CPU us过高;
3)解决办法
①对于线程一直处于运行状态的情况,常用的一种优化方法是,首先找到具体消耗CPU的线程所执行的代码;然后,对于这种线程的动作增加Thread.sleep,来释放CPU的执行权,降低CPU的消耗。(但是,这种修改方式是以损失单次执行性能为代价的,但是由于降低了CPU的消耗,对于多线程应用而言,反而会提高总体的平均性能。)
寻找具体消耗CPU的线程的过程:首先,通过top命令找到CPU严重消耗的线程及其ID,将此线程ID转换为十六进制的值。然后,通过kill-3[javapid]或者jstack的方式dump出应用的java线程信息,通过之前转化出的十六进制的值找到对应的nid的线程。
②对于GC频繁的原因,就要通过JVM调优或者程序调优,来降低GC的执行次数。
③对于循环次数太多、正则或者纯粹的计算等的原因,则要结合业务需要来进行调优。
4)调优时需要的指令
①top指令
top指令可以查看CPU的整体消耗情况:也就是多个CPU所占的百分比总和;
进入top视图后按1可以,按核来显示CPU消耗情况:
在top视图中按shift+h可以显示,每个线程的CPU消耗情况:
②pidstat工具
输入pidstat可以查看当前进程的CPU消耗情况:其中,CPU表示当前进程所使用的CPU个数。
输入pidstat -p[PID] -t 1 5 可以查看某个进程中线程的CPU消耗情况:TID为线程ID
③jstack工具:Java堆栈跟踪工具
jstack( Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者 javacore文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的 目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂 起等,都是导致线程长时间停顿的常见原因。
通过下图中nid确定是该线程:
jstack命令格式:
2、CPU sy值过高
1)问题
Linux花费了大量时间在进行线程切换,导致内核线程处理所占的CPU百分比增高。
2)问题的原因
主要原因: 启动的线程比较多,而且这些线程多数都处于不断的阻塞(锁等待、IO等待等)和执行状态的变化过程中,这就会导致操作系统要不断的进行切换执行的线程,产生大量的上下文切换。
其它原因:
①线程之间的锁竞争激烈:会造成线程状态要经常切换。
②应用中有较多的网络IO操作或者确实需要一些锁竞争机制(例如数据库连接池),为了能够支撑高的并发量,需要更多的线程来支撑。 可以采用协程的方式来支撑更高的并发量。
3)解决办法
①针对启动线程比较多的问题:常用的优化方法是在线程数设置合理的情况下,尽量减少线程的数量,同时使用线程池避免不断的创建线程。
②针对锁竞争激烈的问题:尽可能降低线程之间的锁竞争
③协程来支撑更高的并发量:
解决的是大并发时,原生线程浪费的情况。
创建并启动一个Thread对象就意味着运行了一个原生线程,当这个线程有任何的阻塞动作(例如同步文件IO、同步网络IO、锁等待、Thread.sleep等)时,这个线程就会被挂起,但仍然占据着线程的资源。
找出线程不断切换状态的原因的过程:
采用kill -3[javapid]或者jstack -l[javapid]的方式dump出Java应用程序的线程信息,查看线程的状态信息和锁信息,找出等到状态或锁竞争过多的线程。
4)调优指令
①vmstat
可以查看CPU的消耗情况:比如CPU sy:
②sar
查看CPU sy等
2、 IO层
2.1 文件IO的消耗
1)问题
Java应用的IO消耗高(CPU的iowait 所占的百分比比较多),并且存在读取文件情况,考虑文件IO消耗高的原因。
2)问题的原因
①主要原因: 有多个线程在写大量的数据到同一文件,导致文件很快变得很大,进而导致写入速度越来越慢,并且造成各线程激烈争抢文件锁。
②磁盘本省的处理速度比较慢
③文件系统慢
3)解决思路
①异步写文件
将写文件的同步动作改为异步动作,就可以避免由于写文件慢而性能下降太多,比如写日志使用log4j提供的AsyncAppender。
②批量读写
批量操作来大幅度提升文件IO操作的性能,避免频繁的读写操作对IO的消耗。
③限流
就是将文件IO的消耗控制在一个可接受的范围,比如控制一段时间内的文件IO频率。
例如,在记录日志时,采用log.error(errorInfo,throwable)
的方式,我们会采用一个简单的策略来统计一段时间log.error
的执行频率,当超过某个我们可接受的频率时,一段时间内就不再写log,或者塞入一个队列后缓慢的写。
④限制文件大小
因为操作太大的文件会造成文件IO效率低,所以我们可以对每个输出的文件做大小的限制,在超出最大值之后可以生成一个新文件,再去读写。类似,log4j中RollingFileAppender的maxFileSize属性的作用。
Linux中提升文件IO速度的做法之一:
Linux在操作文件时,将数据放入文件缓存区,直到内存不够或者系统要释放内存给用户进程使用,这种情况下,如果我们的物理内存够用,通常在Linux上只有写文件和第一次读取文件时会产生真正的文件IO。
寻找造成文件IO消耗高的代码的过程:
首先,通过pidstat直接找到文件IO操作多的线程;之后,结合jstack找到对应的代码。
4)调优指令
①pidstat :跟踪线程文件IO的消耗
输入pidstat -d -t -p[pid] 1 100
的命令可以查看线程的IO消耗状况。(必须在2.6.20以上的版本的内核中执行)
②iostat :
只能查看整个系统的文件IO消耗情况。
-
输入iostat命令,可以查看各个设备的IO历史状况:
输入iostat -x xvda 3 5
查看IO消耗情况:
await:表示平均每次IO的等待时间;avgqu-sz:表示等待请求的队列的平均长度。svctm:表示平均每次设备执行IO操作的时间;
当iowait所占的百分比比较大时,就表明IO的消耗比较高了。
2.2 网络IO消耗情况
1)问题
网络IO消耗比较严重。(其实由于在Java实现网络通信时,通常要将对象序列化为字节流,进行发送,或读取字节流,再反序列化为对象。而这个过程要消耗JVM堆内存,且JVM堆内存大小通常有限,因此Java应用一般不会造成网络IO消耗严重)
2)问题的原因
网络IO消耗严重的主要原因就是同时需要发送或者接收的包太多。
3)解决方法
常用的调优方法就是进行限流,限制发送网络paket的频率,在我们对于网络IO可接受的情况下发送packet。
4)调优指令
①sar
输入sar -n FULL 1 2
,以1秒为频率来输出两次网络IO的消耗情况:
3、内存层
内存的分布
对于我们的Java应用来说,对于内存的主要消耗主要是在JVM堆内存上,物理内存和SWAP区则消耗的比较少。
3.1 物理内存和swap消耗
1)问题原因
Java应用中,对于物理内存和swap的消耗主要是JVM内存设置过大、创建的Java线程过多或者通过Direct ByteBuffer往物理内存中放置了过多的对象造成。
2)解决方法
合理调整JVM内存占据总内存的比例;控制创建的线程数;
3)调优指令
①top
top可以查看进程所消耗的内存量,但是top中看到的Java进程的内存消耗是包括了JVM已分配的内存加上Java应用所消耗的JVM以外的物理内存,这会导致top中看到Java进程所消耗的内存大小可能超过-Xmx加上-XX:MaxPermSize设置的内存大小。
因此,纯粹根据top是很难判断出Java进程消耗的内存中有多少属于JVM的,有多少属于消耗JVM外的内存。
②pidstat
通过pidstat也可以查看进程所消耗的内存量。
命令格式:pidstat -r -p[pid][interval][times]
,比如输入pidstat -r -p 2013 1 100
,可以查看该进程所占用的物理内存和虚拟内存的大小:
3.2 JVM内存消耗
Java应用中大多数都是对于JVM heap区的消耗。
1)问题
JVM内存消耗过多会导致GC执行频繁,CPU消耗增加,应用线程的执行速度严重下降,还可能造成OOM错误,最终导致Java进程退出。
2)解决方法
JVM方面调优:
-
主要是一些JVM内存管理方面的调优,主要目的就是降低GC所导致的应用暂停时间。
①代大小的调优(Java heap中各分代的大小调节)
几个重要的参数:
-Xms,-Xmx:控制JVM heap所使用的空间大小。
-Xmn:新生代空间大小
-XX:SurvivoRatio:调节Eden、S0、S1三个区域的比率
-XX:MaxTenuringThreshold:新生代存活周期,控制对象在经历多少次Minor GC之后转入老年代
a)避免新生代大小设置过小
新生代设置过小可能产生的问题:
一是minor GC的次数会非常频繁;二是容易触发Full GC,原因是minor GC之后的对象有可能直接进入老年代,如果进入老年代的对象超过了老年代的剩余空间,就会触发Full GC。
调整原则:
在不能调大JVM heap的情况下,尽可能增大新生代空间(找一个合适的比例,新生代也不可以设置过大),尽量让对象在minor GC阶段被回收掉;在能调大JVM heap的情况下,可以增加新生代空间大小的时,增加JVM heap的大小。保证老年代空间可用。
b)避免新生代大小设置过大
新生代设置过大可能产生的问题:
一是可能导致Full GC频繁执行,原因是老年代变小了,比较容易填满老年代;
二是minor GC的耗时会大幅增加。
调整原则:
大多数场景下新生代的大小都应设置的比老年代小,通常推荐的比例是新生代占JVM heap区大小的33%左右。
c)避免Survivor区过小或过大
调大SurvivorRatio:Eden区会变大,minor GC的触发频率会降低,但问题是Survivor区变小了,minor GC后容易填满Survivor区进而直接转入老年代。
调小SurvivorRatio:Eden区变小了,minor GC的触发频率增高了,但好的是Survivor区变大了,避免更多的minor GC知州存活的对象进入老年代。
因此,根据实际情况合理调节SurvivorRatio的比例,合理控制Eden和Survivor区的大小。
d)合理设置新生代存活周期
JVM参数上-XX:MaxTenuringThreshold这个值的默认值是15,这个值可以控制我们新生代的对象经过多少次minor GC之后进入老年代,因此,我们也可以根据实际情况来调整-XX:MaxTenuringThreshold的值。
e)调整JVM heap的大小
在操作系统等硬件条件允许的情况下,可以适当增大JVM heap的大小。
②GC策略调优
尽可能采用GC暂停时间短的GC策略,比如可以把串行GC优化为CMS的并发GC,尽量减小GC给应用造成的暂停时间。
③程序调优
a.释放不必要的引用
问题原因:代码中持有了不需要的对象引用,导致这些对象无法被GC回收,从而占据JVM heap内存,在成JVM内存不必要的消耗。
因此,我们要去主动释放这些对象引用。典型例子比如复用线程情况下使用的ThreadLocal,由于线程复用,ThreadLocal中存放的对象如未做主动释放的话则不会被GC。要使用ThreadLocal.set把对象清除。
b.使用对象缓存池
采用对象缓存池可以大幅度提升性能,减小JVM heap区的消耗,避免创建对象所耗费的时间及频繁GC所造成的消耗。
c.采用合理的缓存失效算法
因为放入太多对象在缓存池中,容易造成内存的严重消耗;同时由于缓存池一直对这些对象持有引用,从而会造成Ful GC增多,因此要合理控制缓存池的大小。
我们可以利用一些经典的缓存失效算法来清除缓存池中的大小,例如FIFO、LRU、LFU等。进而来控制缓存池中对象的数量,避免缓存池中的对象数量无限上涨。
基于LinkedHashMap简单实现支持FIFO和LRU策略的cachepool:
d.合理使用SoftReference和WeakReference
我们可以基于SoftReference和WeakReference对于占据内存但又不是必须存在的对象进行缓存。
SoftReference对象会在内存不够用时被回收,而WeakReference的对象会在Full GC的时候回收,这两种方法可以一定程度上减少JVM heap区内存的消耗。
3)调优指令
①直接输出GC日志
根据GC日志来分析GC状况
输出到控制台:-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCApplicationStoppedTime,分别输出GC简要信息,GC详细信息,GC的时间信息以及GC造成的应用暂停时间。
②JMap :Java内存映像工具
JDK中自带的一个分析JVM内存状况的工具,位于JDK的bin目录下。使用Jmap可以查看目前JVM中各个代的内存状况、JVM中对象的内存占用情况,以及导出整个JVM中的内存信息。
用于生成堆转储快照(一般称为heapdump文件或者dump文件)
查看JVM各个代的内存状况;在linux上执行jmap -heap[pid]
命令
JVM中对象的内存占用情况;输入jmap -histo[pid]
可以查看JVM堆中对象的详细占用情况,
其中[C表示char类型的对象在JVM中总共有243707个实例,占用了5016….的大小。
-
导出整个JVM中的内存消息
命令:jamp -dump:format=b,file=文件名 [pid]
可以导出整个JVM堆的内存信息。
③JHat :JVM堆转储快照分析工具
可以分析jmap生成的堆转储快照。可以分析JVM heap中对象的内存占用情况、引用关系
命令:jhat -J -Xmx1024M[file]
不过除非没有别的分析工具可用,这个工具用的非常少。可以用VisualVM代替。
④JStat 虚拟机统计信息监视工具
jstat用于监视虚拟机各种运行状态信息的命令行工具。不仅可以分析GC状况,还可以分析编译的状况、类加载状况等。
⑤JVisualVM
基于此工具可以查看内存的消耗情况、线程的执行状况及程序中消耗CPU、内存的动作。
可以安装VisualGC插件来分析GC趋势、内存消耗详细状况:
4、 资源消耗不多但是,程序执行慢
1)问题原因
①锁竞争激烈
锁竞争激烈会造成程序执行慢。典型例子比如:数据库连接池,通常数据库连接池提供的连接数是有限的。
②未充分使用硬件资源
③数据量增长
比如当数据库中单表的数据从100万个上涨到1亿个后,数据库的读写速度将大幅下降。
2)解决办法
-
针对锁竞争激烈的解决办法是尽量降低线程间的竞争:
①使用并发包中类
并发包中的类多数都采用lock-free、nonblocking算法,减少了多线程情况下资源的锁竞争,所以对于线程间要共享操作的资源而言,尽量使用并发包中的类来实现。
②使用Treiber算法
③使用Michael-Scott非阻塞队列算法
④尽可能少用锁 (将锁最小化)
尽可能让锁仅在需要的地方出现,通常没必要对整个方法加锁,而只对控制的资源加锁操作。
⑤拆分锁
拆分锁就是把独占锁分为多把锁,常见的有读写锁及锁拆分及类似ConcurrentHashMap中默认拆分为16把锁的办法。
⑥去除读写操作操作的互斥锁
-
针对为充分使用硬件资源
①在CPU资源消耗可以接受,且不会因为线程增加带来激烈竞争额场景下,可以适当对处理过程进行分解,增加线程数来并行处理。
②在内存资源消耗可接受、GC频率及系统结构可接受请况下,应充分使用内存来缓存数据,提升系统性能。
调优分析过程:
1)首先考虑对JDK的版本进行升级,看看升级虚拟机是不是可以解决一些问题。
2)编译时间和类加载时间的优化
首先,如果我们觉得编译代码是安全可靠的话,我们可以不需要在类加载的时候进行字节码验证,可以通过参数
-Xverify:none
禁止字节满验证过程进行优化。另外,我们可以通过多层编译的方式(C1和C2结合),来优化我们的代码编译效果。
3)调整内存设置控制GC频率
利用之前优化GC频率的方法来调整GC时间。
新生代回收次数,老年代回收次数;