Java性能调优

2021/8/7 20:08:08

本文主要是介绍Java性能调优,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前言

  本文结合《Java性能权威指南》一书进行总结,用于Java代码性能调优实战。
  改善性能涉及的3种不同活动:性能监控、性能分析、性能调优

  • 性能监控:非侵入式收集或查看应用运行性能数据的活动。
  • 性能分析:侵入式方式收集性能数据的活动,会影响应用吞吐量或响应性。
  • 性能调优:改善应用响应性或吞吐量而更改参数、源代码或属性配置的活动。

性能分析的两种方法

  • 自顶向下:着眼于软件栈顶层的应用,从上往下寻找优化机会和问题。是最常用的性能调优方法,涉及代码修改也用这招。
  • 自底向上:在无法更改应用代码的情况下,如生产环境部署、应用迁移等,使用这种方法。通常需要收集和监控最底层CPU的性能统计数据,包括执行特定任务所需的CPU指令数(通常称为路径长度,Path Length)、应用在一定负载下运行时的CPU高速缓存未命中率。

操作系统的性能监控

CPU使用率

  大多数操作系统的CPU使用率分为用户态CPU使用率系统态CPU使用率

  • 用户态CPU使用率:指应用执行操作系统调用的时间占总CPU时间的百分比。
  • 系统态CPU使用率:指应用执行操作系统调用的时间占总CPU时间的百分比。

系统态CPU使用率高意味着共享资源有竞争或者I/O设备之间有大量的交互。原本用于执行操作系统内核调用的CPU周期也可以用来执行应用代码。所以理想情况下,应用达到最高性能和扩展性时,系统态CPU使用率为0%。所以,提高应用性能和扩展性的一个目标是尽可能降低系统态CPU使用率。

Linux监控CPU使用率

  Linux下监控CPU有如下几种方式:

  • 1、图形化工具GNOME System Monitor
  • 2、图形化工具xosview
  • 3、命令行(vmstat、prstat、top)
    在这里插入图片描述

  vmstat命令显示如上,us表示用户态CPU使用率,sy表示系统态CPU使用率。id表示CPU空闲率或可用率,即id=100-us-sy

CPU调度程序运行队列

  监控CPU调度程序运行队列对于分辨系统是否满负荷具有重要意义。如果在很长一段时间里,运行队列的长度超过了虚拟处理器的个数的一倍,就需要关注但不需要立刻采取行动。如果在很长一段时间里,运行队列的长度达到虚拟处理器的3~4倍或者更高,则需要立刻引起注意或采取行动。
  解决运行队列长的办法有2种:
   1、增加CPU以分担负载或减少处理器的负载量
   2、分析系统中运行的应用,改进CPU使用率。如减少GC的频度、采用完成同样任务但CPU指令更少的算法

Linux监控CPU调度程序运行队列

  Linux上可以使用vmstat命令来监控运行队列长度。procs列的r子列的数字就代表运行队列中轻量级进程的实际数量。
在这里插入图片描述

内存使用率

  当应用运行所需的的内存超过可用物理内存是,会发生页面交换。为应对这种情况,通常要为系统配置swap空间,swap空间会在一个独立的磁盘分区上,应用物理内存耗尽时,操作系统会将应用最少运行的部分置换到swap,需要访问时再置换回内存。这种置换活动会影响应用的响应性和吞吐量。
  JVM垃圾收集器在系统页面交换时性能也很差。因为需要把置换出去的部分拿回到内存进行扫描,同时可能会使STW时间变长。

Linux监控内存使用率

  监控页面交换可用的命令:vmstat、top,或者查看/proc/meminfo文件。
  使用vmstat显示如下,si表示内存页面换入的量so表示页面内存换出的量。free列表示可用空闲内存,当页面交换时,能看到有变化。
在这里插入图片描述

Linux监控锁竞争

  使用pidstat命令可以监控锁竞争,pidstat -w输出结果中cswch/s是让步式上下文切换,并不是所有上下文切换。

让步式切换消耗CPU时钟周期的百分比=让步式切换数*80 000/CPU每秒时钟周期

  如下,pidstat -w每5s监控java进程9391。服务器处理器为3.0GHz双核CPU,每秒时钟周期为3 000 000 000。按上述公式,每个虚拟处理器上下文切换为3500/2=1750,耗费的时钟周期为1750*80 000=140 000 000,所以上下文切换浪费的时钟周期百分比为140 000 000 /3 000 000 000 = 4.7%。符合一般性准则(让步时钟周期占用3% ~ 5%或者更多),说明该Java应用正面临锁竞争。
在这里插入图片描述

Linux监控网络I/O使用率

  使用Linux版nicstat,需要自行编译安装。
  nicstat语法:

        nicstat [-hnsz] [-i interface[,...]] | [interval [count]]

-h:显示帮助信息
-n:仅显示非本地接口
-s:显示概要信息
-z:跳过0值
-i interface:网络接口设备名
interval:报告输出的频率(秒级)
count:报告的采样数

磁盘I/O使用率

  对于有有磁盘操作的应用,查找性能问题应该监控磁盘I/O。Linux可用iostat -xm监控磁盘I/O使用率和系统态CPU使用率。

JVM性能

JVM生命周期

  启动HotSpot VM(JVM)的组件是启动器,JVM有若干个启动器。启动器启动JVM执行步骤:

  • 1、解析命令选项
  • 2、设置堆的大小和JIT编译器。如果启动命令行没有明确设置堆的大小和JIT编译器,启动器会通过自动优化进行设置。
  • 3、设定环境变量,classpath等
  • 4、如果命令行有-jar选项,启动器从指定的JAR的manifest中查找Main-Class,否则从命令行读取Main-Class
  • 5、使用标准Java本地接口(JNI)方法JNI_CreateJavaVM在新创建的线程中创建HotSpot VM
  • 6、创建并初始化好HotSpot VM后,加载Main-Class,同时得到main方法的参数
  • 7、HotSpot VM通过JNI方法CallStaticVoidMethod调用main方法,并将命令行选项传给它

应用程序对垃圾收集器的影响

  应用程序对垃圾收集器的影响主要由3个方面:

  • 1、内存分配。当分代的空间达到某个限额的时候,就会发生垃圾收集,结果导致应用内存分配速率越来越高,垃圾收集触发就越频繁。
  • 2、存活数据的多少。HotSpot VM中垃圾收集器的工作量与每个分代存活的数据多少成比例。Java堆中存活的对象越多,收集器需要做的工作越多。
  • 3、老年代中的引用更新。如果老年代中的引用发生更新,就会创建一个Old-To-Young的引用,这可能导致在预清除或重新标记阶段就产生一个需要遍历的对象。

重要的垃圾收集数据

  重要的垃圾回收数据包括:

  • 当前使用的垃圾收集器
  • Java堆的大小
  • 新生代和老年代的大小
  • 永久代的大小
  • Minor GC的持续时间
  • Minor GC的频率
  • Minor GC的空间回收量
  • Full GC的持续时间
  • Full GC的频率
  • 每个并发垃圾收集周期内的空间回收量
  • 垃圾收集前后Java堆的占用量
  • 垃圾收集前后新生代和老年代的占用量
  • 垃圾收集前后永久代的占用量
  • 是否老年代或永久代的占用触发Full GC
  • 应用是否显式调用了System.gc()

GC时间戳查看

  -XX:+PrintGCTimeStamps:输出自JVM启动以来到垃圾收集之间流逝的秒数
  -XX:+PrintGCDetails:输出自JVM启动以来的秒数
  -Xloggc:<filename>:将垃圾收集的统计数据直接输出到文件,用于离线分析

应用停止时间和应用并发时间

  -XX:+PrintGCApplicationConcurrentTime:应用并发时间
  -XX:+PrintGCApplicationSttoppedTime:应用停止时间
  利用这两个时间。可以报告应用在安全点操作之间的运行时间,有助于理解和量化延迟对JVM的影响,也可以用来辨别是JVM安全点操作还是应用程序引入的延迟。

显式垃圾收集

  显式垃圾收集容易识别,垃圾收集日志中会有特定文字(System)显式,说明System.gc()所引起

离线分析垃圾收集数据

  离线分析工具:GCHisto
在这里插入图片描述

  • Overhead%:垃圾收集开销,表示垃圾收集调优的程度。一般来说,并发垃圾收集的开销应该小于10%。Java堆越大,降低来及收集开销的机会越大,对于给定Java堆大小,需要通过JVM调优才能达到最小开销。
  • Max(ms):最长停顿时间(Maximum Pause Time),可以用来评估最差情况下垃圾收集的延时是否满足需求

监控JIT编译器

  当你想找出哪些方法被优化,或某些情况下的逆转化或重新转化时,监控JIT编译就有用了。
  使用-XX:+PrintComplication参数可以为每次编译生成一行日志,对JIT编译器进行监控。
在这里插入图片描述

类加载

  JVM会把所有类的元数据信息都加载到永久代。

  • 当永久代满时,触发Full GC。
  • 当需要加载其他类而空间不足时,未使用的类就会从永久代中卸载
  • -XX:PermSize-XX:MaxPermSize参数可调整永久代大小。为避免Full GC扩大或缩小永久代的可分配空间,两个参数可以设置为相同值
  • 永久代并发垃圾收集周期不是STW,只能和CMS一起使用

Java应用监控

  应用监控常用方法是查看日志。有些应用内建MBean,通过Java SE监控管理API进行监控。
  jstack命令可赚取线程信息,快速定位Java应用中的锁竞争。如下图,线程信息中粗体字waiting 通lock<0x22e88b10>(a Queue),表示正在等待锁,通过观察,该锁目前有线程ReadThead-33持有。
在这里插入图片描述

Java性能分析工具

  • Oracle Studio Performance Analyzer
  • NetBeans Profiler

Java性能分析技巧

  Java性能优化可以归纳为以下几类:

  • 使用更高效的算法
  • 减少锁竞争
  • 为算法生成更有效率的代码

Java代码可能出现的性能问题

  • 1、FileOutputStream。FileOutputStream.write(int)消耗的系统态CPU过高,理想情况下程序使用的系统态CPU应为0%。处理方式:使用BufferedFileOutputStream对写入的数据进行缓存;同时创建BufferedFileOutputStream时尽量指定一个初始化缓存大小(默认8192)
  • 2、同步HashMap或HashTable。Collections.synchronizedMap()创建了一个同步映像,大量线程在尝试并发访问这个同步HashMap时程序会产生锁竞争。大量让步式线程上下文切换是潜在大量锁竞争的征兆。解决办法:使用ConcurrentHashMap替代。某例子运行结果表明,原先使用同步HashMap最多只能利用8%的系统CPU,改进后可达到100%,让步式香菜上下文从几千降到100以下,每秒操作数提升至原来2倍
  • 3、java.util.Random.next(int)方法。next方法里面使用到AtomicLong类的compareAndSet(ling,long)方法(CAS操作),大量线程争抢的情况下,会导致compareAndSet返回flase而一直循环操作。解决办法:每个线程使用生成自己的Random对象,存放到TheadLocal中。相对使用static Random,使用static TheadLocal<Random>后,每秒操作数是原来的25倍多
  • 4、volatile。volatile修饰的变量必须遵守一定的指令顺序(happens before原则)。为了确保CPU缓存即时更新,即在各个线程之间保持同步,出现volatilet关键字的地方都会加入一条CPU指令:内存屏障,一旦volatile变量值发生改变就会触发CPU缓存更新。频繁更新volatile变量可能导致性能问题。解决办法:减少对volatile变量的写操作,或者对应用程序进行重构避免使用volatile(不能直接删除,以免破坏原有程序的正确性)
  • 5、StringBuilder或StringBuffer。底层使用char[]数组进行存储,当扩大到超过底层数据的存储能力时,需要分配新的数组(容量为老数组的2倍)。解决办法:创建对象时显示指定容量大小
  • 6、Collection类的某些具体实现底层数据存储基于数组。随着元素数量的增加,调整大小的代价很大。解决办法:利用性能分析器收集堆或内存的性能数据,分析需要存储的元素数量大小,设置对应的初始化容量

JVM调优入门

JVM调优应关注的点

  对性能需求的这种分类为系统需求。系统需求关注应用程序运行时的特定方面:吞吐量、响应时间、内存消耗、启动时间、可用性、可管理性,等等。
  JVM调优流程如下:
在这里插入图片描述

垃圾收集器的调优

  JVM提供的收集器:Serial收集器、Throughput收集器、Concurrent Mark-Sweep收集器、G1收集器。
  垃圾收集性能的3个属性:
   1、吞吐量:指不考虑垃圾收集硬气的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标
   2、延迟:由于垃圾收集引起的停顿时间
   3、内存占用:垃圾收集器流畅运行所需要的的内存数量

  垃圾收集器调优3个基本原则:
   1、Minor GC回收原则:每次Minor GC都尽可能多地收集垃圾对象
   2、GC内存最大化原则:处理吞吐量和延迟问题时,垃圾处理器能用的内存越大,垃圾收集的效果越好,应用程序运行也越流畅
   3、GC调优3选2原则:在吞吐量、延迟、内存占用3个属性中任意选择两个进行垃圾收集器的调优。(其中一个属性性能的提高几乎是以另一个或两个的性能属性的损失为代价。现实情况下,极少出现3个属性同等重要的情况)

  JVM调优时使用的辅助命令参数:

-XX:PrintGCTimeStamps打印从JVM启动到GC开始所经历的时间
-XX:PrintGCDateStamps输出YYYY-MM-DDTHH-MM-SS.mmm-TZ格式的垃圾收集时间
-XX:PrintGCDetails打印垃圾收集器相关数据
-Xloggc:< filename >将GC日志记录到< filename >文件中

在这里插入图片描述

堆中活跃数据大小

  堆中活跃数据大小,是应用程序运行稳定时,Full GC后老年代和永久代占用的空间大小。
  为更好度量活跃数据大小,最好在多次Full GC后查看堆的占用情况,同时要确保发生Full GC时应用程序处于稳定状态。正常情况下,应用不会频繁发生Full GC,可以通过jmap命令人工触发。命令:jmap -histo:live Java进程号,进程号可以通过jps命令获得。
  多次Full GC后,计算出Java堆活跃数据占用以及GC时间的平均值。根据活跃数据代销定义初始化Java堆大小,考虑Full GC的影响,推荐基于最差延迟进行估算。
  Java堆初始值通用法则:
   通用法则一:将初始值-Xms和最大值-Mmx设置为活跃数据大小的3~4倍
   通用法则二:永久代的初始值-XX:PermSize和最大值-XX:MaxPermSize应该是活跃数据的1.2~1.5倍或更大
   补充法则:新生代空间应为老年代空间活跃数据的1~1.5倍
  综上,如果Java堆的初始值及最大值为活跃数据大小的3~4倍、新生代为活跃数据大小的1 ~ 1.5倍,那么老年代应设置为活跃数据大小的2 ~ 3倍。
在这里插入图片描述

关于CMS的几个使用注意项

1、从Throughput收集器迁移到CMS收集器时,需要遵守一个通用原则:将老年代空间增大20%~30%,这样才能更有效地运行CMS收集器
2、使CMS调优具有挑战性的几方面因素:对象从新生代提升到老年代的速率;并行老年代垃圾收集线程回收空间的速率;由于CMS收集器回收位于对象之间的垃圾对象而造成老年代空间的碎片化
3、指导原则:CMS包括Minor GC所带来的开销应小于10%;如果观察到CMS垃圾收集的开销在3%或者更少,说明通过调优吞吐量性能提升的空间极其有限

逃逸分析

  HotSpot Vm逃逸分析可以通过-XX:+DoEscapeAnalysis命令行选项开启。
  借助逃逸分析,HotSpot VM的JIT编译器可以应用以下的优化技术:
   1、对象展开。一种在可能直接回收的空间而非Java堆上分配对象字段的技术
   2、标量替换。一种减少内存访问的优化技术
   3、栈上分配。一种在线程的栈帧上而非Java堆上分配对象的优化技术
   4、消除同步。如果线程分配的对象不会发生逃逸,且该线程持有了该对象上的锁,由于其他线程不会访问该对象,这个锁可以通过JIT编译器移除
   5、消除垃圾收集的读/写屏障。如果线程分配的对象不发生逃逸,该对象只能从线程本地的根节点访问,因此在其他对象中存储其他地址时不需要执行读或写屏障

JIT相关知识点

  • -XX:+PrintComplication选项通知JVM为每次它优化或逆优化的函数输出一条日志
  • 通过静态分析、运行时检测以及一种轻量级的性能分析,能够定位出代码中未被执行过的部分,因为这部分代码被JIT编译器识别为死代码
  • JIT能将目标方法展开到调用方法中,降低方法调用的开销,这种方式称为内联。通过-XX:+PrintInlining启动参数,可以看到哪些方法被编译器进行了内联优化


这篇关于Java性能调优的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程