深入理解JVM之性能调优

性能调优

  1. 寻找性能瓶颈

资源主要消耗在CPU、文件 IO、网络IO以及内存方面,机器的资源是有限的,当某资源消耗过多时,通常会造成系统的响应速度慢。

CPU消耗分析

  • 上下文切换:每个CPU(或多核CPU中的每核CPU)在同一时间只能执行一个线程‘,Linux采用的是抢占式调度。即为每个线程分配一定的执行时间,当到达执行时间、线程中有IO阻塞或高优先级线程要执行时,Linux将切换执行的线程,在切换时要存储目前线程的执行状态,并恢复要执行的线程的状态,这个过程就称为上下文切换。对于Java应用,典型的是在进行文件IO操作、网络IO操作、锁等待或线程Sleep时,当前线程会进入阻塞或休眠状态,从而触发上下文切换,上下文切换过多会造成内核占据较多的CPU使用,使得应用的响应速度下降。

  • 运行队列:每个CPU核都维护了一个可运行的线程队列,例如一个4核的CPU,Java应用中启动了8个线程,且这8个线程都处于可运行状态,那么在分配平均的情况下每个CPU中的运行队列里就会有两个线程。

  • 利用率:CPU利用率为CPU在用户进程、内核、中断处理、IO等待以及空闲五个部分使用百分比,这五个值是用来分析CPU消耗情况的关键指标

查看指令

在Linux中,可通过top或pidstat方式来查看进程中现成的CPU消耗状况。

CPU消耗指标

CPU消耗主要体现在us、sy两个值上。

  • us值过高时,表示运行的应用消耗了大部分的CPU。Java应用造成us高的主要原因是线程一直处于可运行(Runnable)状态,通常是这些线程在执行无阻塞、循环、正则或纯粹的计算等动作造成的。另外一种原因是频繁的GC

  • sy值过高时,表示linux花费了更多的时间在进行线程切换。Java应用造成这种现象的主要原因是启动的线程比较多,且这些线程多数都处于不断的阻塞(例如锁等待、IO等待状态)和执行状态的变化过程中,这会导致操作系统要不断的切换执行的线程,产生大量的上下文切换

文件IO消耗分析

在Linux中,要跟踪线程的文件IO的消耗,主要方法是通过pidstat来查找。

  • pidstat:输入如pidstat -d -t-p [pid] 1100类似的命令即可查看线程的IO消耗状况

  • iostat:直接输入iostat命令,可查看各个设备的IO历史状况

网络IO消耗分析

在linux中可采用sar分析网络IO的消耗状况。

由于没办法分析具体每个线程所消耗的网络IO,因此当网络IO消耗高时,对于Java应用而言只能对线程进行 dump,查找产生了大量网络IO操作的线程。这些线程的特征是读取或写入网络流,在用Java实现网络通信时,通常要将对象序列化为字节流,进行发送,或读取字节流,并反序列化为对象。这个过程要消耗JVM堆内存,JVMJVM堆的内存大小通常是有限的,因此Java应用一般不会造成网络IO消耗严重

内存消耗分析

在Linux中可通过vmstat、sar、top、pidstat等方式来查看swap和物理内存的消耗状况

Java应用只有在创建线程和使用Direct ByteBuffer时才会操作JVM堆外的内存JVM,在内存消耗方面最值得关注的是JVM内存的消耗状况。JVM内存消耗过多会导致GC执行频繁,CPU消耗增加,应用线程的执行速度严重下降。

对于JVM堆以外的内存方面的消耗,最为值得关注的是swap的消耗以及物理内存的消耗,这两方面的消耗都可基于os提供的命令来查看。

程序执行慢原因分析

有些情况是资源消耗不多,但程序执行仍然慢,这种现象多出现于访问量不是非常大的情况下,主要原因有以下三种:锁竞争激烈、未充分使用硬件资源和数据量增长

  1. 锁竞争激烈:例如数据库连接池提供的连接数是10个,而此时有50个线程要进行数据库操作,那么其中就会有40个线程处于等待状态。

  1. 未充分使用硬件资源:例如机器上有双核CPU,但程序中都是单线程串行的操作,并没有充分发挥硬件资源的作用。

  1. 数据量增长:例如当数据库中单表的数据从100万个上涨到1亿个后,数据库的读写速度将大幅度下降,相应的操作此表的程序的执行速度也就下降

  1. JVM调优

代大小的调优

  1. 避免新生代设置过小

  1. 避免新生代设置过小

  1. 避免Survivor区过小或过大

  1. 合理设置新生代存活周期

GC策略的调优

根据不同场景选择合适的GC策略

  1. 程序调优

CPU消耗严重的解决方法

  1. CPU us高的解决方法

CPU us高的原因主要是执行线程无任何挂起动作,且一直执行,导致CPU没有机会去调度其他的线程,造成线程饿死的现象。对于这种情况,一种常见的优化方法是对这种线程的动作增加Thread.sleep,以释放CPU的执行权,降低CPU的消耗。

  1. CPU sy高的解决方法

CPU sy高的原因主要是线程的运行状态要经常切换,对于这种情况,最简单的优化方法是减少线程数

文件IO消耗严重的解决方法

从程序角度而言,造成文件IO消耗严重的原因主要是多个线程在写大量的数据到同一文件,导致文件很快变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁。对于这类情况,常用的调优方法有以下几种:

  • 异步写文件

将写文件的同步动作改为异步动作,避免应用由于写文件慢而性能下降太多,例如写日志,可以使用 log4j提供的AsyncAppender 。

  • 批量读写

频繁的读写操作对IO消耗会很严重,批量操作将大幅度提升IO操作的性能。

  • 限流

频繁读写的另外一个调优方式是限流,从而将文件IO消耗控制到一个能接受的范围

网络IO消耗严重的解决方法

对于内存消耗严重的情况:

  1. 释放不必要的引用

内存消耗严重的情况中最典型的一种现象是代码中持有了不需要的对象引用,造成这些对象无法被GC,从而占据了JVM堆内存。

  1. 使用对象缓存池

创建对象的实例要耗费一定的CPU以及内存,使用对象缓存池一定程度上可降低JVM Heap内存的使用。在内存消耗严重的情况下,采用对象缓存池可大幅度提升性能,避免创建对象所耗费的时间及频繁GC造成的消耗。

  1. 采用合理的缓存失效算法

采用对象缓存池会降低内存的消耗,但如果放入太对的对象在缓存池中,反而会造成内存的严重消耗。同时由于缓存池一直对这些对象持有引用,从而会造成Full GC增多,所以要合理控制缓存池大小。

使用缓存失效算法可以清除缓存池中的对象,以便于控制缓存池大小,从而使新的对象能够加入到缓存池当中

  1. 合理使用SoftReference和 WeakReference

对于占据内存但又不是必须存在的对象,可以以基于SoftReference或WeakReference的方式来进行缓存

对于资源消耗不多,但程序执行慢的情况

锁竞争激烈:

  1. 使用并发包中的类

  1. 使用Treiber

  1. 使用Michaerl-Scott非阻塞队列算法

  1. 尽可能少用锁

  1. 拆分锁

拆分锁即把独占锁拆分为多把锁,拆分锁很大程度上能提高读写的速度,但需要注意的是在采用拆分锁后,全局性质的操作会变得比较复杂

  1. 去除读写操作的互斥锁

在修改时加锁,并复制对象进行修改,修改完毕后切换对象的引用,而读取时则不加锁,这种方式被称为CopyOnWrite。CopyOnWrite的好处是可以明显提升读的性能,对于读多写少的应用非常适合,但由于写操作时每次都要复制一份对象,会造成更多的内存消耗

未充分使用硬件资源

未充分使用CPU

未充分使用内存

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值