1.低效的算法、数据结构
2.锁竞争
3.低效的代码
然后文章分门别类得列举了一些java应用中典型的性能问题和调优tip,主要讲解了以下几类问题的原因分析和排查解决思路:
一.系统态cpu资源消耗过高
cpu消耗中有两项主要的消耗是用户态和系统态资源消耗。系统态cpu消耗是指内核对系统资源进行调度时消耗的cpu资源,而用户态则是应用程序进程消耗的cpu资源,比如我们的java应用代码运行的cpu消耗就是用户态消耗。当系统态资源消耗高时,用户态能够能够使用的cpu资源自然就变少了。所以当系统态资源消耗过高时,应用就需要考虑进行性能调优了。java应用满负载运行时建议用户态和系统态的比例为65%~70%/30%~35%。
在linux下使用top命令查看cpu消耗,top命令使用的一些tips:
第一行给出当前服务器时间,启动时间,当前登录用户,以及系统负载情况。需要注意的是linux的系统负载是以1分钟、5分钟和15分钟内的平均值 来衡量的。
第二行列出系统进程情况,总共97个进程,1个进程处于运行状态,96个空闲,0个停止,另外有0个僵尸进程。僵尸进程指的是子进程退出后父进程并没有处理子进程的退出信号,导致子进程变为僵尸进程。
第三行给出当前CPU的工作情况,%us(user)指的是cpu用在用户态程序上的时间;%sy(sys)指的是cpu用在内核态程序上的时间;%ni(nice)指的是用在nice优先级调整过的用户态程序上的时间;%id(idle)指的是cpu空闲时间;%wa(iowait)指的是 cpu等待系统io的时间;%hi指的是cpu处理硬件中断的时间;%si指的是cpu处理软中断的时间;%st(steal)用于有虚拟cpu的情况, 用来指示被虚拟机偷掉的cpu时间。通常idle值可以反映一个系统cpu的闲忙程度。
第四行和第五行给出真实内存的使用情况,包括内存总量,使用量,空闲量,以及交换分区的总量,使用量和空闲量。此外关于buffers和 cached的区别需要说明一下,buffers指的是块设备的读写缓冲区,cached指的是文件系统本身的页面缓存。它们都是linux操作系统底层 的机制,目的就是为了加速对磁盘的访问。
第六行往后是进程列表,常见的这几列的意义分别为:PID(进程号), USER(运行用户),PR(优先级),NI(任务nice值),VIRT(虚拟内存用量),RES(物理内存用量),SHR(共享内存用量),S(进程 状态),%CPU(CPU占用比),%MEM(内存占用比),TIME+(累计CPU占用时间)。
除了这些信息之外,top还提供了很多命令能帮我更好的解读这些信息,例如按”M”键可以按内存用量进行排序;按”P”可以按CPU使用量进行排 序,这样一来对于分析系统瓶颈很有帮助;
Top命令使用的几个小技巧:在top视图上按1,会按核来显示;按大写P,按cpu使用率排序;按大写M,按照内存使用率排序;按shift+h,按照线程查看cpu的消耗状况
《java performance》一书中介绍了一个由于io操作频繁导致的用户态消耗低而系统态资源消耗相对过高的例子,他提供的解决办法是把原先直接使用FileOutPutStream进行文件写操作的程序改成了使用BufferedOutputStream进行操作,从而降低了系统态资源的消耗。这个例子会导致cpu系统态资源消耗高的原因是:io操作频繁会导致iowait较高,iowait高了之后多线程情况下上下文切换(上下文切换的概念:每个cpu同一时间只能执行一个线程,linux采用的是抢占式调度。当一个线程的时间片用完后要切换执行的线程,这时要存储目前线程的状态,并恢复要执行线程的状态。当发生线程切换时cpu存储和恢复线程状态的行为即上下文切换。上下文切换消耗的是cpu的系统态资源)也随之变高,从而导致cpu系统态消耗高。其实这类问题根本的解决思路是找到消耗系统态资源较高的问题的原因,尽可能减少导致这个问题发生的cpu操作的发生频率。
二.锁竞争激烈
三.基于数组的数据结构的大小重分配
在java中有很多数据结构的底层实现都是依赖于数组的,比如String底层依赖char数组,ArrayList和HashMap等容器底层也是类似。当这些数据结构中的数据不停得增长,到一定程度时,就需要对底层的数组进行重分配,重新申请一个新的数组,并把原数组的数据拷贝到新数组中去。这会带来一些问题:数组分配带来的cpu指令、内存消耗、老数组带来的垃圾内存、以及垃圾内存带来的垃圾回收时的cpu资源消耗、底层数组变化导致的cpu cache miss、重分配数组导致得数据结构锁定等。
第一种情况主要讲StringBuffer和StringBuilder等以char数组为底层的数据结构和类似的以数组为底层的容器的重分配大小对性能带来的影响。当上述的两种数据结构包含的字符数足够大时,这些对象通常都会进行重置大小,通常是把底层持有数据的数组扩大两倍。还是书中先前的例子,在频繁得使用了StringBuilder作为数据结构后。应用的内存dump显示char数组占用的数据占到了大量的内存消耗。这类问题的解决办法可以是不采用StringBuilder的默认构造函数,因为默认使用的是一个16位大小的char数组,一般情况都会不够用,所以可以设置一个更大的值,减少char数组频繁变更导致得重新分配大小。
四.局部变量全局化导致的并发下效率降低问题(使用了Random.nextInt使用AtomicLong的CAS方法来获取随机数导致热方法的例子)
文中使用了Random类的对象做为全局对象导致热方法的例子。在多线程环境下,每个线程都使用同一个Random类的对象来调用nextInt方法来获取随机数,由于Random.next中使用了AtomicLong的CAS方法来获取随机数,在Random对象为全局的情况下,多线程共享Random对象时,由于没有线程同步,会频繁导致CAS失败,进而Random对象需要频繁得进行CAS操作才能正确获得随机数,Random.next的代码如下:
可以看到,当AtomicLong的compareAndSet方法在多线程情况下失败频率增大时,会出现频繁多余得对compareAndSet方法的调用。
这里介绍一下CAS:CAS的原理是在读取数据时获得一份数据的拷贝,当需要修改数据时比较拷贝和源地址的数据是否一致,即检查数据有没有被修改过,判断数据没有被修改过才进行修改。保证数据的一致性。这个原理和乐观锁相近。乐观锁是在修改数据库记录时读取版本号,每次修改记录会修改版本号,但是修改时会检查当前获得的版本号是否和数据库中的锁一致,如果一致再进行修改。
上面的这个问题是通过把Random类的对象修改做为线程变量来解决的。这就避免了多个线程同时进行CAS操作以及由此带来的CAS失败。
我认为,这类问题的本质是对于一些工具类型的api,如果可能在多线程下使用,要考虑多线程使用下的同步问题,如果确实存在并发可能带来的问题,可以考虑以空间换取时间的策略,把全局变量变成局部变量,或者线程变量(java通过ThreadLocalapi可以支持)。