目录

对于调优这个事情来讲,通常就是三个过程:html

  • 性能监控:问题没有发生,你并不知道你须要调优什么?此时须要一些系统、应用的监控工具来发现问题。
  • 性能分析:问题已经发生,可是你并不知道问题到底出在哪里。此时就须要使用工具、经验对系统、应用进行瓶颈分析,以求定位到问题缘由。
  • 性能调优:通过上一步的分析定位到了问题所在,须要对问题进行解决,使用代码、配置等手段进行优化。

Java调优也不外乎这三步。java

此外,本文所讲的性能分析、调优等是抛开如下因素的:ios

  • 系统底层环境:硬件、操做系统等
  • 数据结构和算法的使用
  • 外部系统如数据库、缓存的使用

调优准备

调优是须要作好准备工做的,毕竟每个应用的业务目标都不尽相同,性能瓶颈也不会总在同一个点上。在业务应用层面,咱们须要:git

  • 须要了解系统的整体架构,明确压力方向。好比系统的哪个接口、模块是使用率最高的,面临高并发的挑战。
  • 须要构建测试环境来测试应用的性能,使用ab、loadrunner、jmeter均可以。
  • 对关键业务数据量进行分析,这里主要指的是对一些数据的量化分析,如数据库一天的数据量有多少;缓存的数据量有多大等
  • 了解系统的响应速度、吞吐量、TPS、QPS等指标需求,好比秒杀系统对响应速度和QPS的要求是很是高的。
  • 了解系统相关软件的版本、模式和参数等,有时候限于应用依赖服务的版本、模式等,性能也会受到必定的影响。

此外,咱们还须要了解Java相关的一些知识:github

  1. Java内存相关:这一部分能够参见谈谈Java内存管理一文
  2. 对Java代码进行基准性能测试:可使用JMH来进行,[译]使用JMH进行微基准测试:不要猜,要测试!
  3. HotSpot VM相关知识:http://www.oracle.com/technetwork/cn/java/javase/tech/index-jsp-136373-zhs.html
  4. jdk自带各类java工具:http://www.rowkey.me/blog/2016/11/03/jdk-tools/

性能分析

在系统层面可以影响应用性能的通常包括三个因素:CPU、内存和IO,能够从这三方面进行程序的性能瓶颈分析。web

CPU分析

当程序响应变慢的时候,首先使用top、vmstat、ps等命令查看系统的cpu使用率是否有异常,从而能够判断出是不是cpu繁忙形成的性能问题。其中,主要经过us(用户进程所占的%)这个数据来看异常的进程信息。当us接近100%甚至更高时,能够肯定是cpu繁忙形成的响应缓慢。通常说来,cpu繁忙的缘由有如下几个:正则表达式

  • 线程中有无限空循环、无阻塞、正则匹配或者单纯的计算
  • 发生了频繁的gc
  • 多线程的上下文切换

肯定好cpu使用率最高的进程以后就可使用jstack来打印出异常进程的堆栈信息:redis

jstack [pid]算法

jstack

接下来须要注意的一点是,Linux下全部线程最终仍是以轻量级进程的形式存在系统中的,而使用jstack只能打印出进程的信息,这些信息里面包含了此进程下面全部线程(轻量级进程-LWP)的堆栈信息。所以,进一步的须要肯定是哪个线程耗费了大量cpu,此时可使用top -p [processId]来查看,也能够直接经过ps -Le来显示全部进程,包括LWP的资源耗费信息。最后,经过在jstack的输出文件中查找对应的lwp的id便可以定位到相应的堆栈信息。其中须要注意的是线程的状态:RUNNABLE、WAITING等。对于Runnable的进程须要注意是否有耗费cpu的计算。对于Waiting的线程通常是锁的等待操做。数据库

也可使用jstat来查看对应进程的gc信息,以判断是不是gc形成了cpu繁忙。

jstat -gcutil [pid]

jstat

还能够经过vmstat,经过观察内核状态的上下文切换(cs)次数,来判断是不是上下文切换形成的cpu繁忙。

vmstat 1 5

jstat

此外,有时候可能会由jit引发一些cpu飚高的情形,如大量方法编译等。这里可使用-XX:+PrintCompilation这个参数输出jit编译状况,以排查jit编译引发的cpu问题。

内存分析

对Java应用来讲,内存主要是由堆外内存和堆内内存组成。

  1. 堆外内存

    堆外内存主要是JNI、Deflater/Inflater、DirectByteBuffer(nio中会用到)使用的。对于这种堆外内存的分析,仍是须要先经过vmstat、sar、top、pidstat(这里的sar,pidstat以及iostat都是sysstat软件套件的一部分,须要单独安装)等查看swap和物理内存的消耗情况再作判断的。此外,对于JNI、Deflater这种调用能够经过Google-preftools来追踪资源使用情况。

  2. 堆内内存

    此部份内存为Java应用主要的内存区域。一般与这部份内存性能相关的有:

    • 建立的对象:这个是存储在堆中的,须要控制好对象的数量和大小,尤为是大的对象很容易进入老年代
    • 全局集合:全局集合一般是生命周期比较长的,所以须要特别注意全局集合的使用
    • 缓存:缓存选用的数据结构不一样,会很大程序影响内存的大小和gc
    • ClassLoader:主要是动态加载类容易形成永久代内存不足
    • 多线程:线程分配会占用本地内存,过多的线程也会形成内存不足

    以上使用不当很容易形成:

    • 频繁GC -> Stop the world,使你的应用响应变慢
    • OOM,直接形成内存溢出错误使得程序退出。OOM又能够分为如下几种:
      • Heap space:堆内存不足
      • PermGen space:永久代内存不足
      • Native thread:本地线程没有足够内存可分配

    排查堆内存问题的经常使用工具是jmap,是jdk自带的。一些经常使用用法以下:

    • 查看jvm内存使用情况:jmap -heap 
    • 查看jvm内存存活的对象:jmap -histo:live 
    • 把heap里全部对象都dump下来,不管对象是死是活:jmap -dump:format=b,file=xxx.hprof 
    • 先作一次full GC,再dump,只包含仍然存活的对象信息:jmap -dump:format=b,live,file=xxx.hprof 

    此外,无论是使用jmap仍是在OOM时产生的dump文件,可使用Eclipse的MAT(MEMORY ANALYZER TOOL)来分析,能够看到具体的堆栈和内存中对象的信息。固然jdk自带的jhat也可以查看dump文件,会启动web端口供开发者使用浏览器浏览堆内对象的信息。

IO分析

一般与应用性能相关的包括:文件IO和网络IO。

  1. 文件IO

    可使用系统工具pidstat、iostat、vmstat来查看io的情况。这里能够看一张使用vmstat的结果图。

    这里主要注意bi和bo这两个值,分别表示块设备每秒接收的块数量和块设备每秒发送的块数量,由此能够断定io繁忙情况。进一步的能够经过使用strace工具定位对文件io的系统调用。一般,形成文件io性能差的缘由不外乎:

    • 大量的随机读写
    • 设备慢
    • 文件太大
  2. 网络IO

    查看网络io情况,通常使用的是netstat工具。能够查看全部链接的情况、数目、端口信息等。例如:当time_wait或者close_wait链接过多时,会影响应用的相应速度。

    netstat -anp

    此外,还可使用tcpdump来具体分析网络io的数据。固然,tcpdump出的文件直接打开是一堆二进制的数据,可使用wireshark阅读具体的链接以及其中数据的内容。

    tcpdump -i eth0 -w tmp.cap -tnn dst port 8080 #监听8080端口的网络请求并打印日志到tmp.cap中

    还能够经过查看/proc/interrupts来获取当前系统使用的中断的状况。

    各个列依次是:

    irq的序号, 在各自cpu上发生中断的次数,可编程中断控制器,设备名称(request_irq的dev_name字段)

    经过查看网卡设备的终端状况能够判断网络io的情况。

其余分析工具

上面分别针对CPU、内存以及IO讲了一些系统/JDK自带的分析工具。除此以外,还有一些综合分析工具或者框架能够更加方便咱们对Java应用性能的排查、分析、定位等。

  • VisualVM

    这个工具应该是Java开发者们很是熟悉的一款java应用监测工具,原理是经过jmx接口来链接jvm进程,从而可以看到jvm上的线程、内存、类等信息。 若是想进一步查看gc状况,能够安装visual gc插件。此外,visualvm也有btrace的插件,能够可视化直观的编写btrace代码并查看输出日志。 与VisualVm相似的,jconsole也是经过jmx查看远程jvm信息的一款工具,更进一步的,经过它还能够显示具体的线程堆栈信息以及内存中各个年代的占用状况,也支持直接远程执行MBEAN。固然,visualvm经过安装jconsole插件也能够拥有这些功能。但因为这俩工具都是须要ui界面的,所以通常都是经过本地远程链接服务器jvm进程。服务器环境下,通常并不用此种方式。

  • Java Mission Control(jmc)

    此工具是jdk7 u40开始自带的,原来是JRockit上的工具,是一款采样型的集诊断、分析和监控与一体的很是强大的工具。https://docs.oracle.com/javacomponents/jmc-5-5/jmc-user-guide/toc.htm

  • Btrace

    这里不得不提的是btrace这个神器,它使用java attach api+ java agent + instrument api可以实现jvm的动态追踪。在不重启应用的状况下能够加入拦截类的方法以打印日志等。具体的用法能够参考Btrace入门到熟练小工彻底指南

  • Jwebap

    Jwebap是一款JavaEE性能检测框架,基于asm加强字节码实现。支持:http请求、jdbc链接、method的调用轨迹跟踪以及次数、耗时的统计。由此能够获取最耗时的请求、方法,并能够查看jdbc链接的次数、是否关闭等。但此项目是2006年的一个项目,已经将近10年没有更新。根据笔者使用,已经不支持jdk7编译的应用。若是要使用,建议基于原项目二次开发,同时也能够加入对redis链接的轨迹跟踪。固然,基于字节码加强的原理,也能够实现本身的JavaEE性能监测框架。

    上图来自笔者公司二次开发过的jwebap,已经支持jdk8和redis链接追踪。

  • useful-scripts

    这里有一个本人参与的开源的项目:https://github.com/superhj1987/useful-scripts,封装了不少经常使用的性能分析命令,好比上文讲的打印繁忙java线程堆栈信息,只须要执行一个脚本便可。

性能调优

与性能分析相对应,性能调优一样分为三部分。

CPU调优

  • 不要存在一直运行的线程(无限while循环),可使用sleep休眠一段时间。这种状况广泛存在于一些pull方式消费数据的场景下,当一次pull没有拿到数据的时候建议sleep一下,再作下一次pull。
  • 轮询的时候可使用wait/notify机制
  • 避免循环、正则表达式匹配、计算过多,包括使用String的format、split、replace方法(可使用apache的commons-lang里的StringUtils对应的方法),使用正则去判断邮箱格式(有时候会形成死循环)、序列/反序列化等。
  • 结合jvm和代码,避免产生频繁的gc,尤为是full GC。

此外,使用多线程的时候,还须要注意如下几点:

  • 使用线程池,减小线程数以及线程的切换
  • 多线程对于锁的竞争能够考虑减少锁的粒度(使用ReetrantLock)、拆分锁(相似ConcurrentHashMap分bucket上锁), 或者使用CAS、ThreadLocal、不可变对象等无锁技术。此外,多线程代码的编写最好使用jdk提供的并发包、Executors框架以及ForkJoin等,此外DiscuptorActor在合适的场景也可使用。

内存调优

内存的调优主要就是对jvm的调优。

  • 合理设置各个代的大小。避免新生代设置太小(不够用,常常minor gc并进入老年代)以及过大(会产生碎片),一样也要避免Survivor设置过大和太小。
  • 选择合适的GC策略。须要根据不一样的场景选择合适的gc策略。这里须要说的是,cms并不是全能的。除非特别须要再设置,毕竟cms的新生代回收策略parnew并不是最快的,且cms会产生碎片。此外,G1直到jdk8的出现也并无获得普遍应用,并不建议使用。
  • jvm启动参数配置-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:[log_path],以记录gc日志,便于排查问题。

其中,对于第一点,具体的还有一点建议:

  • 年轻代大小选择:响应时间优先的应用,尽量设大,直到接近系统的最低响应时间限制(根据实际状况选择)。在此种状况下,年轻代收集发生gc的频率是最小的。同时,也可以减小到达年老代的对象。吞吐量优先的应用,也尽量的设置大,由于对响应时间没有要求,垃圾收集能够并行进行,建议适合8CPU以上的应用使用。
  • 年老代大小选择:响应时间优先的应用,年老代通常都是使用并发收集器,因此其大小须要当心设置,通常要考虑并发会话率和会话持续时间等一些参数。若是堆设置小了,会形成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;若是堆大了,则须要较长的收集时间。最优化的方案,通常须要参考如下数据得到:
    • 并发垃圾收集信息
    • 持久代并发收集次数
    • 传统GC信息
    • 花在年轻代和年老代回收上的时间比例

    通常吞吐量优先的应用都应该有一个很大的年轻代和一个较小的年老代。这样能够尽量回收掉大部分短时间对象,减小中期的对象,而年老代存放长期存活对象。

此外,较小堆引发的碎片问题:由于年老代的并发收集器使用标记、清除算法,因此不会对堆进行压缩。当收集器回收时,会把相邻的空间进行合并,这样能够分配给较大的对象。可是,当堆空间较小时,运行一段时间之后,就会出现“碎片”,若是并发收集器找不到足够的空间,那么并发收集器将会中止,而后使用传统的标记、清除方式进行回收。若是出现“碎片”,可能须要进行以下配置:-XX:+UseCMSCompactAtFullCollection,使用并发收集器时,开启对年老代的压缩。同时使用-XX:CMSFullGCsBeforeCompaction=xx设置多少次Full GC后,对年老代进行压缩。

其他对于jvm的优化问题可见后面JVM参数进阶一节。

代码上,也须要注意:

  • 避免保存重复的String对象,同时也须要当心String.subString()与String.intern()的使用
  • 尽可能不要使用finalizer
  • 释放没必要要的引用:ThreadLocal使用完记得释放以防止内存泄漏,各类stream使用完也记得close。
  • 使用对象池避免无节制建立对象,形成频繁gc。但不要随便使用对象池,除非像链接池、线程池这种初始化/建立资源消耗较大的场景,
  • 缓存失效算法,能够考虑使用SoftReference、WeakReference保存缓存对象
  • 谨慎热部署/加载的使用,尤为是动态加载类等
  • 不要用Log4j输出文件名、行号,由于Log4j经过打印线程堆栈实现,生成大量String。此外,使用log4j时,建议此种经典用法,先判断对应级别的日志是否打开,再作操做,不然也会生成大量String。

    if (logger.isInfoEnabled()) {
          logger.info(msg);
      }

IO调优

文件IO上须要注意:

  • 考虑使用异步写入代替同步写入,能够借鉴redis的aof机制。
  • 利用缓存,减小随机读
  • 尽可能批量写入,减小io次数和寻址
  • 使用数据库代替文件存储

网络IO上须要注意:

  • 和文件IO相似,使用异步IO、多路复用IO/事件驱动IO代替同步阻塞IO
  • 批量进行网络IO,减小IO次数
  • 使用缓存,减小对网络数据的读取
  • 使用协程: Quasar

其余优化建议

  • 算法、逻辑上是程序性能的首要,遇到性能问题,应该首先优化程序的逻辑处理
  • 优先考虑使用返回值而不是异常表示错误
  • 查看本身的代码是否对内联是友好的: 你的Java代码对JIT编译友好么?

此外,jdk七、8在jvm的性能上作了一些加强:

  • 经过-XX:+TieredCompilation开启JDK7的多层编译(tiered compilation)支持。多层编译结合了客户端C1编译器和服务端C2编译器的优势(客户端编译可以快速启动和及时优化,服务器端编译能够提供更多的高级优化),是一个很是高效利用资源的切面方案。在开始时先进行低层次的编译,同时收集信息,在后期再进一步进行高层次的编译进行高级优化。须要注意的一点:这个参数会消耗比较多的内存资源,由于同一个方法被编译了屡次,存在多份native内存拷贝,建议把code cache调大一点儿(-XX:+ReservedCodeCacheSize,InitialCodeCacheSize)。不然有可能因为code cache不足,jit编译的时候不停的尝试清理code cache,丢弃无用方法,消耗大量资源在jit线程上。
  • Compressed Oops:压缩指针在jdk7中的server模式下已经默认开启。
  • Zero-Based Compressed Ordinary Object Pointers:当使用了上述的压缩指针时,在64位jvm上,会要求操做系统保留从一个虚拟地址0开始的内存。若是操做系统支持这种请求,那么就开启了Zero-Based Compressed Oops。这样可使得无须在java堆的基地址添加任何地址补充便可把一个32位对象的偏移解码成64位指针。
  • 逃逸分析(Escape Analysis): Server模式的编译器会根据代码的状况,来判断相关对象的逃逸类型,从而决定是否在堆中分配空间,是否进行标量替换(在栈上分配原子类型局部变量)。此外,也能够根据调用状况来决定是否自动消除同步控制,如StringBuffer。这个特性从Java SE 6u23开始就默认开启。
  • NUMA Collector Enhancements:这个重要针对的是The Parallel Scavenger垃圾回收器。使其可以利用NUMA (Non Uniform Memory Access,即每个处理器核心都有本地内存,可以低延迟、高带宽访问) 架构的机器的优点来更快的进行gc。能够经过-XX:+UseNUMA开启支持。

此外,网上还有不少过期的建议,不要再盲目跟随:

  • 变量用完设置为null,加快内存回收,这种用法大部分状况下并无意义。一种状况除外:若是有个Java方法没有被JIT编译但里面仍然有代码会执行比较长时间,那么在那段会执行长时间的代码前显式将不须要的引用类型局部变量置null是可取的。具体的能够见R大的解释:https://www.zhihu.com/question/48059457/answer/113538171
  • 方法参数设置为final,这种用法也没有太大的意义,尤为在jdk8中引入了effective final,会自动识别final变量。

JVM参数进阶

jvm的参数设置一直是比较理不清的地方,不少时候都搞不清都有哪些参数能够配置,参数是什么意思,为何要这么配置等。这里主要针对这些作一些常识性的说明以及对一些容易让人进入陷阱的参数作一些解释。

如下全部都是针对Oracle/Sun JDK 6来说

  1. 启动参数默认值

    Java有不少的启动参数,并且不少版本都并不同。可是如今网上充斥着各类资料,若是不加辨别的所有使用,不少是没有效果或者原本就是默认值的。通常的,咱们能够经过使用java -XX:+PrintFlagsInitial来查看全部能够设置的参数以及其默认值。也能够在程序启动的时候加入-XX:+PrintCommandLineFlags来查看与默认值不相同的启动参数。若是想查看全部启动参数(包括和默认值相同的),可使用-XX:+PrintFlagsFinal。

    输出里“=”表示使用的是初始默认值,而“:=”表示使用的不是初始默认值,多是命令行传进来的参数、配置文件里的参数或者是ergonomics自动选择了别的值。

    此外,还可使用jinfo命令显示启动的参数。

    • jinfo -flags [pid] #查看目前启动使用的有效参数
    • jinfo -flag [flagName] [pid] #查看对应参数的值

    这里须要指出的是,当你配置jvm参数时,最好是先经过以上命令查看对应参数的默认值再肯定是否须要设置。也最好不要配置你搞不清用途的参数,毕竟默认值的设置是有它的合理之处的。

  2. 动态设置参数

    当Java应用启动后,定位到了是GC形成的性能问题,可是你启动的时候并无加入打印gc的参数,不少时候的作法就是从新加参数而后重启应用。但这样会形成必定时间的服务不可用。最佳的作法是可以在不重启应用的状况下,动态设置参数。使用jinfo能够作到这一点(本质上仍是基于jmx的)。

    jinfo -flag [+/-][flagName] [pid] #启用/禁止某个参数
     jinfo -flag [flagName=value] [pid] #设置某个参数

    对于上述的gc的状况,就可使用如下命令打开heap dump并设置dump路径。

    jinfo -flag +HeapDumpBeforeFullGC [pid] 
     jinfo -flag +HeapDumpAfterFullGC [pid]
     jinfo -flag HeapDumpPath=/home/dump/dir [pid]

    一样的也能够动态关闭。

    jinfo -flag -HeapDumpBeforeFullGC [pid] 
     jinfo -flag -HeapDumpAfterFullGC [pid]

    其余的参数设置相似。

  3. -verbose:gc 与 -XX:+PrintGCDetails

    不少gc推荐设置都同时设置了这两个参数,其实,只要打开了-XX:+PrintGCDetails,前面的选项也会同时打开,无须重复设置。

  4. -XX:+DisableExplicitGC

    这个参数的做用就是使得system.gc变为空调用,不少推荐设置里面都是建议开启的。可是,若是你用到了NIO或者其余使用到堆外内存的状况,使用此选项会形成oom。能够用XX:+ExplicitGCInvokesConcurrent或XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses(配合CMS使用,使得system.gc触发一次并发gc)代替。

    此外,还有一个比较有意思的地方。若是你不设置此选项的话,当你使用了RMI的时候,会周期性地来一次full gc。这个现象是因为分布式gc形成的,为RMI服务。具体的可见此连接内容中与dgc相关的:http://docs.oracle.com/javase/6/docs/technotes/guides/rmi/sunrmiproperties.html

  5. MaxDirectMemorySize

    此参数是设置的堆外内存的上限值。当不设置的时候为-1,此值为-Xmx减去一个survivor space的预留大小。

  6. 因为遗留缘由,做用相同的参数

    • -Xss 与 -XX:ThreadStackSize
    • -Xmn 与 -XX:NewSize,此外这里须要注意的是设置了-Xmn的话,NewRatio就没做用了。
  7. -XX:MaxTenuringThreshold

    使用工具查看此值默认值为15,可是选择了CMS的时候,此值会变成4。当此值设置为0时,全部eden里的活对象在经历第一次minor GC的时候就会直接晋升到old gen,survivor space直接就没用。

  8. -XX:HeapDumpPath

    使用此参数能够指定-XX:+HeapDumpBeforeFullGC、-XX:+HeapDumpAfterFullGC、-XX:+HeapDumpOnOutOfMemoryError触发heap dump文件的存储位置。