深入理解JVM—寻找性能瓶颈并进行优化

资源消耗过多时,外部系统性能必然会变差

一、CPU标高问题

当CPU消耗严重时,主要体现在us、sy、wa或hi的值变高,wa的值是IO等待造成的;hi的值变高主要为硬件中断造成的,例如网卡接收数据频繁的状况。

对于Java应用而言,CPU消耗严重主要体现在us、sy两个值上,来分别看看Java应用在这两个值高的情况下应如何寻找对应造成瓶颈的代码。

先用top命令查看相关指标,找到哪个进程,然后top -hp pid或者 pidstat -t -p pid 查看进程的具体线程使用CPU情况,jstack -l [javapid]的方式dump出Java应用程序的线程信息。现在也大多使用arthas(线上java诊断工具)挂Java进程看哪个线程,进行在线定位分析。

如命令dashboard模拟图形界面,显示出堆内存情况,线程占比CPU情况,thread -b pid 查看线程是否死锁...

1.CPU us值高:

Java应用造成us高的原因主要是线程一直处于可运行(Runnable)状态,无任何挂起动作,导致CPU没有机会去调度执行其他的线程,造成线程饿死。常用优化方法就是对这种线程动作增加Thread.sleep,来释放CPU的执行权,降低CPU的消耗。这种修改方式是以损失单次执行性能为代价的,但由于降低了CPU的消耗,对于多线程的应用而言,反而提高了总体的平均性能。

另外一个可能也会造成us高的原因是频繁的GC。如每次请求都需要分配较多内存,当访问量高的时候就将导致不断地进行GC,系统响应速度下降,进而造成堆积的请求更多,消耗的内存更严重,最严重的时候有可能会导致系统不断进行full GC,对于频繁GC的状况要通过分析JVM内存的消耗、JVM调优,看回收情况,降低GC的执行次数。

还有一种经典的场景是状态的扫描,例如某线程要等其他线程改变了值后可继续执行。对于这种场景,最佳的方式是改为采用wait/notify机制。

对于其他类似循环次数太多、正则、计算等造成的CPU us过高的状况,则要结合业务需求来进行调优。

2.CPU sy值高时:

表示Linux花费了更多的时间在进行线程切换,需要找出线程要不断切换状态的原因。

找到线程,查看线程的状态信息以及锁信息,找出等待状态或锁竞争过多的线程

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

最简单的优化方法就是减少线程数,需合理的设置线程数。

造成CPU sy高还有一个重要的原因是线程之间锁竞争激烈,造成了线程状态经常要切换,尽可能降低线程间的锁竞争也是常见的优化方法。锁竞争降低后,线程的状态切换的次数也就会下降,sy值会相应下降。

对于分布式Java应用而言,还有一种典型现象是应用中有较多的网络IO操作或确实需要一些锁竞争机制(例如数据库连接池),但为了能够支撑高的并发量,在Java应用中又只能

借助启动更多的线程来支撑。在这样的情况下当并发量增长到一定程度后,可能会造成CPU sy高的现象,对于这种现象,可采用协程(Corountine)来支撑更高的并发量,避免并发量上涨后造成CPU sy消耗严重、系统load迅速上涨和系统性能下降。

   

二、IO问题

  1. 文件IO消耗高

在使用iostat查看IO的消耗情况时,首先要关注的是CPU中的iowait%所占的百分比,当iowait占据了主要百分比时,就表示要关注IO方面的消耗状况,这时可以再通过iostat -x这样的方式来详细地查看具体状况。

当文件IO消耗过高时,对于Java应用最重要的是找到造成文件IO消耗高的代码,寻找的最佳方法为通过pidstat直接找到文件IO操作多的线程。之后结合jstack找到对应的Java代码,如没有pidstat,也可直接根据jstack得到的线程信息来分析其中文件IO操作较多的线程。

Java应用造成文件IO消耗严重主要是原因多个线程需要进行大量内容写入(例如频繁的日志写入)的动作,导致文件变得很大,从而写入速度越来越慢,并造成各线程激烈争抢文件锁,或者文件过大造成文件IO效率低。

常用的调优方式

异步写文件

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

批量读写

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

③限流

频繁读写的另外一个调优方式是限流,从而将文件IO消耗控制到一个能接受的范围,例如通常在记录日志时会采用如下方式:

如以上方式不做任何处理,在大量出现异常时,会出现所有的线程都在执行log.error(...),此时可采取的一个简单策略为统计一段时间内log.error的执行频率。当超过这个频率时,一段时间内不再写log,或塞入一个队列后缓慢地写。

限制文件大小

操作太大的文件也是造成文件IO效率低的一个原因,因此对于每个输出的文件,都应做大小的限制,在超出最大值后可生成一个新的文件,类似log4j中RollingFileAppender的maxFileSize属性的作用。

除了以上这些外,还有就是尽可能采用缓冲区等方式来读取文件内容,避免不断与操作系统交互。

  1. 网络IO消耗分析

在linux上采用sar分析网络IO的消耗状况

sar -n full 1 2:执行后以一秒为频率,总共输出两次网络IO的消耗情况。

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

从程序角度而言,造成网络IO消耗严重的原因主要是同时需要发送或接收的包太多,读写网络操作太频繁。对于这类情况,常用的调优方法为进行限流,限流通常是限制发送packet的频率,从而在网络IO消耗可接受的情况下来发送packet。

三、JVM内存消耗分析

在Java程序出现内存消耗过多、GC频繁,CPU消耗增加,应用线程的执行速度严重下降,甚至造成OutOfMemoryError,最终导致Java进程退出。

要首先分析其所耗费的是JVM外的物理内存还是JVM heap区:

如为JVM外的物理内存,则要分析程序中线程的数(创建太多线程)以及Direct ByteBuffer的使用情况(使用了ByteBuffer但未释放);

如为JVM heap区,则要结合JDK提供的工具或外部的工具来分析程序中具体对象的内存占用状况。

  1. JVM堆以外的内存

目前的Java应用只有在创建线程和使用Direct ByteBuffer时才会操作JVM堆外的内存JVM

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

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

vmstat:展示出内存的swpd(虚拟内存所使用的部分)、free(空闲的物理内存)、buff(用于缓冲的内存)、cache(用于缓存的内存)以及swap下的si和so。

由于Java应用是单进程应用,只要JVM的内存设置不是过大,是不会操作到swap区域的。物理内存消耗过高可能是由于JVM内存设置过大、创建的Java线程过多或通过Direct ByteBuffer往物理内存中放置了过多的对象造成的。

sar:查看内存的消耗情况,与vmstat相比,sar可以查询历史状况,来更准确地分析趋势状况,但两者都不能分析进程所占用的内存量。

top:查看进程所消耗的内存量

pidstat:也查看进程所消耗的内存量 pidstat -r -p pid

最佳的内存消耗分析方法是结合top或pidstat,+JVM的内存分析工具来共同分析内存的消耗状况。

2.JVM堆内存

除了少数如上几种操作JVM堆外内存的方法外,大多数都是对于JVM Heap区的消耗:

如创建了过多对象,持有一些不必要的引用导致内存溢出..

  Java对象内存分配以及回收,得出Java应用对于内存的消耗主要是在JVM堆内存上,在正式环境中,多数Java应用都会将-Xms和-Xmx设为相同的值,避免运行期要不断申请内存。

对JVM堆内存消耗较多的应用进行优化,提升应用性能,常用的内存管理优化方法如下,为了降低GC所导致的应用暂停时间STW。

代大小的调优

在不采用Gl(G1不区分minor GC和Full GC)的情况下,通常minor GC会远快于Full GC,各个代的大小设置直接决定了minor GC和Full GC触发的时机,在代大小的调优上,合理调节最关键的参数:

1.Xms和-Xmx:通常设置为相同的值,避免运行时要不断地扩展JVM内存空间,这个值决定了JVMHeap所能使用的最大空间。在内存不够用时可适量增大此值,能调整到多大取决于操作系统位数和CPU能力。

2.-Xmn:决定了新生代空间的大小,同时决定了多少比例的对象有机会在minor GC阶段被回收,但此值相应的也决定了旧生代的大小。新生代越大,通常意味着多数对象能够在minor GC阶段被回收掉,但同时意味着旧生代的空间会变小,可能会造成更频繁的FullGC,甚至是OutOfMemoryError。

3.新生代中Eden、S0和Sl三个区域的比率可通过-XX:SurvivorRatio来控制。

Eden区越大通常也就意味着minor GC发生的频率越低。但有可能会造成Survivor区太小,导致对象在经过minor GC后直接就进旧生代了,从而更频繁的触发Full GC,这取决于当Eden区满的时候其中存活对象的比例。

4.-XX:MaxTenuringThreshold控制对象在经历多少次Minor GC后才转入旧生代,通常又将此值称为新生代存活周期,此参数只有在串行GC时有效,其他GC方式时则由Sun JDK自行决定。

在清楚掌握minor GC、Full GC的触发时机以及代大小的调整后,结合应用的状况(例如创建出的对象都可很快被回收掉、缓存对象多等)通常就可较好设置代的大小,减少GC所占用的时间。在调整后可结合jstat、VisualVM等查看GC的变化是否达到了调优的目的。

GC策略的调优

策略 1:将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。

策略 2:大对象进入老年代,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代 -XX:PretenureSizeThreshold 可以设置直接进入老年代的对象大小。

策略 3:合理设置进入老年代对象的年龄,-XX:MaxTenuringThreshold 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。

策略 4:设置稳定的堆大小,堆大小设置有两个参数:-Xms 初始化堆大小,-Xmx 最大堆大小。

策略5:如果满足下面的指标,则一般不需要进行 GC 优化:

MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。

释放不必要的引用

内存消耗严重的情况中最典型的一种现象是代码中持有了不需要的对象引用,造成这些对象无法被GC,从而占据了JVM堆内存。这种情况最典型的一个例子是在复用线程的情况下使用ThreadLocal,.由于线程复用,ThreadLocal中存放的对象如未做主动释放的话则不会被GC。

对于这种情况,要注意在线程内的动作执行完毕时执行ThreadLocal.set把对象清除,避免持有不必要的对象引用。

使用对象缓冲池

创建对象的实例要耗费一定的CPU以及内存,使用对象缓存池一定程度上可降低JVM Heap内存的使用。

在内存消耗严重的情况下,采用对象缓存池可大幅度提升性能,避免创建对象所耗费的时间及频繁GC造成的消耗。

资源消耗不多,程序执行仍然很慢

有些情况是资源消耗不多,但程序执行仍然慢,这种现象多出现于访问量不是非常大的情况下,造成这种现象的原因主要有以下三种:

1.锁竞争激烈

锁竞争激烈直接就会造成程序执行慢,例如一个典型的例子是数据库连接池,通常数据库连接池提供的连接数都是有限的。假设提供的是10个,那么就意味着同时能够进行数据库操作的就只有10个线程,而如果此时有50个线程要进行数据库操作,那就会造成另外的40个线程处于等待状态,这种情况下对于4核类型的机器而言,CPU的消耗并不会高,但程序的执行仍然会较慢。

优化方法:为保证资源的一致性,多线程使用锁不可避免,只能尽量降低线程间的锁竞争,常用方法:

①对于线程间要共享操作的资源,尽量使用并发包中的类实现,此类大多采用了 lock-free、nonblocking算法,减少了多线程情况下资源的锁竞争。

②拆分锁:把独占锁拆分为多把锁,提高速率,但全局性质操作变得复杂。

③CopyOnWrite:修改时加锁并复杂对象进行修改,修改完毕后切换对象的引用,而读取时不加锁,提升读的性能。

2.未充分使用硬件资源

例如机器上有双核CPU,但程序中都是单线程串行的操作,并没有充分发挥硬件资源的作用,优化方法:来充分而不过分使用CPU和内存,提升程序的执行速度。

3.数据量增长

数据量增长通常也是造成程序执行慢的典型原因,例如当数据库中单表的数据从100万个上涨到1亿个后,数据库的读写速度将大幅度下降,相应的操作此表的程序的执行速度也就下降了。

优化方法:要记录程序执行的整个过程的时间消耗或使用Profiler等商业工具跟踪代码执行速度从而找到执行耗时比率最大的代码进行优化处理。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欣欣uvo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值