Java进阶之性能优化

作者:盲目的拾荒者

博客:https://blog.csdn.net/niugang0920

衡量系统现状

       调优前首先要做的是衡量系统现状,这包括目前系统的请求次数、响应时间资源消耗等信息,列如:A系统目前95%的请求影响时间在1秒内。

  • 先粗粒度的划分
  • 再细粒度的寻找具体的点

设定调优目标

       根据用户所能接受的响应速度回系统所拥有的机器以及所支撑的用户量指定出来。

寻找性能瓶颈

       根据现状信息衡量目前系统资源的瓶颈,对于于Java应用而言,寻找性能瓶颈的方法通常为首先分析资源的消耗,结合java一些工具查找程序中造成资源消耗过多的代码.

寻找消耗资源的代码

CPU

us(用户进程处理所占的百分比)

a.线程一直处于运行状态,通常是这些线程在执行无阻塞代、循环、计算量大等动作造成。

找到pid 通过jstack定位

b.GC频繁(如:每次请求都需要分配较多的内存,当访问量高的时候就需要不断的GC系统响应速度下降,造成堆积的请求更多,消耗内存更多,最严重的时候可能导致系统不断的进行FullFC,这个可以通过JVM工具)

sy(内核线程处理所占的百分比)

线程上下文----线程多&IO等待多等(启动线程比较多,且这些线程多数都处于不断的阻塞【如锁等待,Io等待】和执行状态的变化中)

注意:对于多个或多核cpu,top则会显示所占用的百分比总,所以可能会超过100% 如查看每个核的消耗情况 ,可在进入top视图按1,就会按核显示消耗情况

注意:对于java应用而言,CP消耗严重主要体现在us,sy两个值。

文件IO

并发读写多、文件大

扩展:对于java应用最重要的是找到造成文件io消耗高的代码,寻找最佳方法为通过pidstat直接找到文件iO操作多的线程,结合jstack找到对应的java代码

扩展:在Lunix中,要跟踪线程的文件IO的消耗,主要通过pidstat输入如:pidstat -d -t -p [pid] 1 100 查看线程的IO消耗情况

扩展:java应用造成IO消耗严重的主要是多个线程需要大量内容写入(列如:频繁的日志写入)的动作。或磁盘设备本身处理速度慢;或文件系统慢

内存

       首先分析消耗的是JVM外的物理内存还是JVM heap区。如为JVM外的物理内存,则要分析程序中线程的数量以及Diret ByteBuffer的使用情况;如为JVM heap区,则结合JDK提供的工具或外部工具来分析程序中突击对象的内存占用状况。

a.JVM堆内存创建了过多对象、持有了不要的引用

扩展:Java应用对于内存的消耗主要在JVM堆内存上,在正式环境中多数Java应用都会将-Xms和-Xmx设为相同的值,避免运行期不断申请内存。

扩展:对于JVM内存消耗分析通过jmap、jstat、mat、visualvm。JVM内存消耗过多会导致GC执行频繁,cpu消耗增加,应用 线程的执行速度严重下降甚至OOM,最终导致java进程的退出。

b.JVM堆以外的内存创建了太多的线程、使用了ByteBuffer但未释放

扩展:对于JVM堆以外的内存方面的消耗,最值得关注的是swap的消耗以及物理内存的消耗。Linux中通过vmstat、sar、top、pidstat等方式来查看swap和物理内存的消耗状态。

扩展:通过top可查看进程所消耗的内存量,不过top中看到的java进程的消耗内存是包括了JVM已分配的内存加上Java应用所消耗 的JVM以外的内存,这会导致top中看到Java进程所消耗的内存大小可能超过-Xmx加上-XX:MaxPermSize设置的内存大小。所以可认为Java进程中显示出来的内存消耗值即为JVM -Xmx的值+消耗的JVM外的内存值。

总结:最佳的内存消耗分析方法是结合top和pidstat,以及JVM的内存分析工具来共同分析内存的消耗情况。

网络IO

读写网络操作太频繁

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

寻找执行慢的原因和代码

扩展:资源消耗不多,但程序执行仍然慢,这种情况多出现于访问量不大的情况。

a.锁竞争激烈 典型的就是连接池(线程等待)

b.未充分使用硬件资源

c.数据量大

性能调优

JVM调优

扩展:JVM调优主要是内存管理方面的调优,包括各个代大小、GC策略。由于GC动作会挂起应用线程,严重影响性能,这些调优至关重要。所以内存管理调优的方法其目标就是尽量降低GC所导致的应用暂停时间。

代大小调优

扩展:minorGC(新生代 Eden Space内存回收)会远快于FullGC(老年代内存回收)。各个代的大小设置直接决定了minorGC和Full GC的触发时间,在代大小的调优上,最关键的参数为:-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringTheshold。

扩展:-Xms和-Xmx通常设为相同值,避免运行时要不断扩展JVM内存空间,这个值决定了JVM heap所能使用的最大空间。

扩展:-Xmn决定了新生代空间的大小,新生代中的Eden、s0和s1三个区域的 比率可通过-XX:SurvivorRatio控制,SurvivorRatio默认为8,如-Xmn=10M 则eden为8M,s0 s1分别为1M。

扩展:-XX:MaxTenuringTheshold控制对象在经历多次minorGC后才转入就生代。

a. 避免新生代设置过小

原因:当新生代设置过小,会产生两种比较明显的现象,一是minorGC的次数更加频繁。二是有可能导致minor gc对象直接进行入就生代,此时进入旧生代的对象占据了旧生代剩余空间,则出发FullGC(System.gc则触发FullGC).

方法:在不能调大JVM Heap情况下,尽可能放大新生代空间,尽量让对象在minorGC阶段被回收。但旧生代也不可过大。在能调大JVM Heap的情况下,则可以增加新生代空间大小增加JVM Heap大小。以保证旧生代空间够

通过jstat观察GC状况。

b.避免新生代设置过大

原因:一是旧生代变小了,有可能导致频繁的FullGC。二是 minorGC耗时大幅增加。

方法:大多数场景下新生代都应设置的比旧生代小,推荐比例是新生代占JVM heap大小的33%左右。

c.避免SurvivorRatio区过小或过大

在串行GC时,默认情况下Eden、s0、s1的大小比例为8:1:1。调大SurvivorRatio意味着Eden区域变大,minorGc的触发次数会降低,但此时Survivor区域空间变小,如有超过Survivor空间大小的对象在minorGC后仍没有被回收,则直接进入旧生代。调小SurvivorRatio则意味着Eden区域变小,minorGC的触发次数会增加,Survivor区域变大 ,意味着可存储更多在minorGC后仍存活的对象,避免其进入旧生代。

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

新生代存活周期决定了新生代的对象经过多少次MinorGC后进入旧生代,默认为15.

GC策略的调优

扩展:程序中大多数对象的存活时间都是较短的,少部分对象是长期存活的。基于这个分析,JVM堆新生代和旧生代中对象存活时间特征提供了不同的GC实现。

串行GC性能太差,因此在实际场景中使用的主要是并行和并发GC.

程序调优

CPU消耗严重的解决方法

a.CPU us高的解决

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

b.CPU sy高的解决

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

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

造成文件IO消耗严重的原因主要是多个线程在写大量的数据到同一文件,导致文件很快变得很大,从而写入速度越来越慢。

a.异步写文件

将同步写文件改为异步写文件,列如写日志,可以使用log4j提供的AsyncAppender

b.批量读写

c.限制文件大小

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

造成网络IO消耗严重的主要是同时需要发送或接收的包太多了。调优方法为进行限流,限流通常是限制发送数据包的频率。

对于内存消耗严重的情况

在内存消耗方面,最明显的在于消耗过多的JVM Heap内存,造成GC频繁。

a.释放不要的引用

资源消耗不多,仍然执行慢的情况

对于分布式Java应用而言,造成的主要原因通常有锁竞争激烈和未充分发挥硬件资源

a.锁竞争激烈

线程多了后,锁竞争的状况比较明显,这时线程很容易处于等待锁的状态。锁是影响性能的重要因素,但是为了保证资源的一致性,多线程应用中锁的使用时不可避免的,只能尽量降低线程间的锁竞争。

方法:使用并发包中的类、尽可能少用锁

b.未充分使用硬件资源

这种情况也是性能降低的应用中经常出现的,主要体现在未充分使用cpu和内存

未充分使用cpu

    未充分使用CPU的原始主要是在并行处理的场景中未使用足够的线程。

未充分使用内存

    未充分使用内存的场景非常多,如数据的缓存、耗时资源的缓存(如:数据库连接的创建、网络连接的创建)、页面片段的缓存等,毕竟内存的读取肯定远快于硬盘、网络的读取。

以上内容主要来自读《分布式Java应用基础与实践》

                                               

                                                                             JAVA程序猿成长之路

                                                       分享学习资源,学习方法,记录程序员生活。

猜你喜欢

转载自blog.csdn.net/niugang0920/article/details/83031756