JAVA性能优化思路探究

844 阅读11分钟

1、背景介绍

一个系统的上线除了常规的功能性测试外,还需要经过严格的性能测试,满足预期的性能指标(常见的有响应时间,tps等),才允许上生产环境。

广义的性能测试一般还包含负载测试(用于测试系统的容量:即系统在保证一定响应时间的情况下能够允许多少并发用户的访问),压力测试(用于测试系统的稳定性:即在保证一定压力的情况下,查看测试系统的稳定性),并发测试(即测试系统多并发能力:即模拟多用户访问同一应用的测试,用于发现并发问题,比如线程锁,资源竞争,数据库死锁等)等。

通过性能测试,可以帮助我们尽快发现系统的瓶颈。如果发现未能满足预期的业务目标,则需要进行性能调优。性能调优的需求,有时候来自于原型的验证,有时候来自于生产上实际的问题,不管哪一类的性能调优,我们一般按照性能监控,性能分析,性能优化这几个步骤进行。以下章节会对每个步骤进行详细分析。

2、性能监控

性能监控是性能调优的第一步,主要目的在于了解当前系统运行的状态,了解当前服务器资源使用情况,JVM的内存使用,线程使用等情况,以便于第一时间找到瓶颈点。

2.1 查看服务器配置

为了更好评估服务器性能,首先应了解当前宿主服务器的配置情况。以下主要是针对linux服务器给出的常见的查看命令。

2.1.1 CPU配置

对于CPU,比较关心的是CPU的总逻辑核数,可以直接使用mpstat查看。

可以使用cat /proc/cpuinfo查看CPU的型号:

2.1.2 内存配置

使用free命令进行查看,可以看到总的内存,以及使用的情况

2.1.3 磁盘配置

使用fdisk -l可以查看到所有的磁盘配置情况,使用df -TH可以看到当前磁盘的目录挂载情况

有时候,需要确认当前磁盘是否为SSD盘,判断cat /sys/block//queue/rotational的返回值(其中为你的硬盘设备名称,例如sda等等),如果返回1则表示磁盘可旋转,那么就是HDD了;反之,如果返回0,则表示磁盘不可以旋转,那么就有可能是SSD了。如下图所示,sda是SSD盘。

2.1.4 网络配置

使用ifconfig命令,可以看到网卡的配置情况。有时候,需要查看当前网卡是千M还是万M,可以通过ethtool查看speed可以判断。如下图所示,eth4是千M网卡。

2.2 服务器监控

为了能实时了解系统运行时,资源的占用情况,我们就需要对服务器的系统资源进行监控,以下列出常见的命令以及常用的监控事项。

2.2.1 CPU监控

使用vmstat命令,vmstat 2代表每2秒统计一次

重点观察

Ø Procs中r值,它代表调度程序运行队列的长度,如果该值长时间大于CPU逻辑核数1倍以上,需要关注,超过3-4倍需要马上采取行动

Ø System中in(中断),cs(上下文切换)如果两值较大,说明系统内核消耗CPU较多

Ø Cpu列中,如果us(用户态)占比长期大于50%时,就需要考虑优化算法。根据经验us+sy占比参考值为80%

可以使用pidstat -w -I -p pid 2,监控应用的锁竞争情况

让步式上下文切换(cswch)时钟周期占用3% ~ 5%,说明Java应用面临锁竞争,抢占式上下文切换率(nvcswch)高,说明预备运行的线程数多于可用的虚拟处理器数。

2.2.2 内存监控

也可以使用上述的vmstat查看内存页面交换,

重点观察free,si,so这几列,如果free变小,而且si,so在变化,说明存在内存不足,跟磁盘swap,有发生页面交换的情况,需要考虑加大内存。

2.2.3 网络监控

使用第三方软件iptraf,它提供了可视化的页面,通过它可以实时监控网络流量情况。

2.2.4 磁盘

使用iostat进行监控

cpu属性值说明:

Ø 如果%iowait的值过高,表示硬盘存在I/O瓶颈,%idle值高,表示CPU较空闲,如果%idle值高但系统响应慢时,有可能是CPU等待分配内存,此时应加大内存容量。%idle值如果持续低于10,那么系统的CPU处理能力相对较低,表明系统中最需要解决的资源是CPU。

disk属性值说明:

Ø 如果%util接近100%,说明产生的I/O请求太多,I/O系统已经满负荷,该磁盘可能存在瓶颈。如果svctm比较接近await,说明I/O几乎没有等待时间;如果await远大于svctm,说明I/O队列太长,io响应太慢,则需要进行必要优化。如果avgqu-sz比较大,也表示有当量io在等待。

2.3 JVM监控

使用jdk中自带的jvisualvm工具,在要连接的远程java进程,启动时增加jmx的配置,如下:

这样jvisualvm就可以通过ip+1111端口侦听远程JVM的情况了。

2.4 连接池监控

2.4.1 查看数据库连接池数量

使用netstat –an | grep ‘db ip’ | wc –l命令,可以看到与数据库创建的连接池,看这个值跟设置的数据库连接池的最小值,以及最大值的关系。如果始终通过最大值,需要考虑调整连接的最大值。

2.4.2 查看工作线程数

方法1:使用jvisualvm工具远程监控来查看

方法2:使用命令查看

2.5 Oracle监控

2.5.1 查看oracle配置

Ø 使用oracle的用户登录oracle的服务器(su - oracle)

Ø 启动sqlplus命令行模式(sqlplus / as sysdba)

Ø 查看配置(Show parameter sga;)

2.5.2 性能监控

Ø 使用sqlplus命令行模式

Ø 开始时启动快照命令,停止时再执行一遍快速命令

备注:快照命令(exec DBMS_WORKLOAD_REPOSITORY.CREATE_SNAPSHOT();)

Ø 快照执行完后,取报告(@?/rdbms/admin/awrrpt)

Ø 分析报告(重点关注top 5 time events)

3 性能分析

3.1 JVM分析

3.1.1 堆分析

为了不影响线上的性能,可以使用堆转储,命令如下:

jmap -dump:live,format=b,file=heap_dump.hprofpid

然后可以将生成的.hprof文件导入mat,或者jvisualvm进行分析,可以了解哪些对象正在消耗内存。同时对于识别由创建太多某一特定对象所引发的内存问题,软件提供的直方图方法快速且方便。

3.1.2 垃圾回收分析

Jvm启动时,可以设置-Xloggc,-XX:PrintGCDetails等参数,开启gc日志收集。也可以使用jstat进行监控分析,比如jstat–gcutil pid 2用于每隔2秒打印当前Java堆及GC情况。

3.1.3 线程分析

使用jdk自带的JMC和jstack工具,可以查看堵塞的线程。JMC内部集成的JFR可以很方便的检索出引发线程堵塞的事件。而jstack在一定程度上可以检查线程是堵塞在什么资源上。以下给出jstack定位思路:

4 性能优化

在深度优化系统前,应该先弄清为何CPU的使用率低。优化代码的目的是提升而不是降低更短时间内的CPU使用率。

4.1 JVM启动参数优化

4.1.1 原生内存的优化

对原生内存的优化,包含使用压缩的OOP(jvm启动参数上增加-XX:+UseCompressedOops)以及调整大内存分页(同时修改linux配置以及jvm启动参数-XX:LargePageSizeInBytes)等,都可以提升性能。

4.1.2 垃圾回收机制的优化

Ø 合理设置堆的大小,以及合理设置好代空间的划分:设置太小容易频繁GC,而设置太大,GC时停顿时间太长。同时为了避免可能使用到虚拟内存,内存页交换导致更慢,至少保留1G的物理内存。

Ø 如何选择各分区大小应该依赖应用程序中对象生命周期的分布情况:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。

Ø 稳定与震荡的堆大小:将-Xms和-Xmx的大小一致,对垃圾回收有利。

4.1.3 大对象分配优化

Ø 大对象尽量分配在TLAB,如果大量发生在TLAB外,需要考虑调整TLAB参数,或者减少分配对象的大小。可以通过-XX:PrintTLAB标志查看结果。

Ø 大对象划入老年代:将大对象直接分配到老年代,保持新生代对象的结构的完整性,以提高GC效率,以通过 -XX:PretenureSizeThreshold设置进入老年代的阀值。

4.2 java编程优化

因为实际的编程中,涉及性能优化的点比较多,以下只是列举一些常见的优化项供参考。

4.2.1 线程池优化

Ø 根据当前服务器CPU的数量合理设置最大线程数,最小线程数,线程池任务队列大小。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。

Ø 建议使用有界队列,有界队列能增加系统的稳定性和预警能力。

Ø 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。

Ø 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。设置线程的优先级。

4.2.2 其它编程细节

Ø 尽量减少内存的使用,减少对象的大小,设置类型时考虑最小原则,去掉不用的属性,以及没有用到的实例变量。

Ø 通过使用对象池以及线程局部变量的方式来加强对象的重复。对象重用跟GC是有点矛盾,所以主要考虑对象初始化时成本比较高的情况(即初始化时间比较长)。

Ø 对于保存实际不需要在多个线程间共享的同步对象,同时又在不同的实际中进行传递的对象,可以考虑使用线程局部变量,减少同步竞争。

Ø 在一些场景下,优化使用java8并行流的模式

4.3 数据库优化

4.3.1 使用预编译

使用preparedStatement方式,重用预处理语句池可以极大提升性能,同时也要避免出现大量大型对象池化而引起的GC方面的问题。

4.3.2 使用连接池

引入hikari连接池,在启动时就配置好

Ø 创建连接的代价很大,通过JDBC连接池获取连接可省去创建连接时间。同时需要合理设置连接池的大小。

Ø 合理设值:比如设置检索时的批量值,设置最优的预取值,设置ResultSet的批量值,可以提高检索的性能。

Ø 事务的优化:事务的提交以及事务相关的锁机制都会影响系统的性能,需要考虑合理设置事务隔离的级别,以及批量提交的策略等。

5 性能实战经验汇总

5.1 清算并发性能上不去

5.1.1 问题的现象

使用java8的并行流计算时,发现并发的性能上不去,并且性能会随着时间推移,不断的下降

5.1.2 优化点

Ø 引入hikari连接池,将单笔延时降到5ms

Ø 关闭日志

Ø 将sql改成预编译的模式

Ø Oracle服务器的把oracle的内存提高

5.2 Hsiar跟中台的连接上,存在很多FIN_WAIT2连接

5.3 Server_name是否可以随便配置

6 小结

6.1 性能工具箱

6.1.1 压测工具jmeter

Jmter是开源的压测工具,也易于上手。它的使用就不介绍了,这里主要讲一些注意的事项:

Ø 它的实时绘图依赖于服务器端的响应,如果压测机与服务器时间不同步的话,会出现展示图断层现象,为了得到更准确的性能曲线,建议使用命令行的方式。

Ø 有时候发现压测性能上不去,有可能的原因在于客户端。主要考虑的因素:客户端的CPU不足以支持所需数量的客户端线程,或者客户端需要花大量时间处理响应后才能发送请求。

6.1.2 JVM相关

除了上文提到的jdk自带的工具外,还有IBM提供的MemoryAnalyzer,以及商用软件jprofile都很强大。

6.1.3 数据库相关

对于数据库的性能监控,可以使用Spotlight,它有基于mysql以及oracle的不同版本支持。

6.1.4 网络相关

常用Linux自带的Tcpdump命令导出抓包,然后使用wireshark进行分析,很强大。

本文主要是针对java性能调优的一般思路的总结,以及分享一些在性能优化上常用的最佳实践。希望对大家有帮助。