在经过工具获得异常指标,初步定位瓶颈点后,若是进一步进行确认和调优?咱们在这里提供一些可实践、可借鉴、可参考的性能调优「套路」,即:如何在众多异常性能指标中,找出最核心的那一个,进而定位性能瓶颈点,最后进行性能调优。如下会按照代码、CPU、内存、网络、磁盘等方向进行组织,针对对某一各优化点,会有系统的「套路」总结,便于思路的迁移实践。java
1、应用代码相关ios
遇到性能问题,首先应该作的是检查否与业务代码相关——不是经过阅读代码解决问题,而是经过日志或代码,排除掉一些与业务代码相关的低级错误。性能优化的最佳位置,是应用内部。git
譬如,查看业务日志,检查日志内容里是否有大量的报错产生,应用层、框架层的一些性能问题,大多数都能从日志里找到端倪(日志级别设置不合理,致使线上疯狂打日志);再者,检查代码的主要逻辑,如 for 循环的不合理使用、NPE、正则表达式、数学计算等常见的一些问题,均可以经过简单地修改代码修复问题。github
别动辄就把性能优化和缓存、异步化、JVM 调优等名词挂钩,复杂问题可能会有简单解,「二八原则」在性能优化的领域里里依然有效。固然了,了解一些基本的「代码经常使用踩坑点」,能够加速咱们问题分析思路的过程,从 CPU、内存、JVM 等分析到的一些瓶颈点优化思路,也有可能在代码这里体现出来。正则表达式
下面是一些高频的,容易形成性能问题的编码要点。数据库
1)正则表达式很是消耗 CPU(如贪婪模式可能会引发回溯),慎用字符串的 split()、replaceAll() 等方法;正则表达式表达式必定预编译。数组
2)String.intern() 在低版本(Java 1.6 以及以前)的 JDK 上使用,可能会形成方法区(永久代)内存溢出。在高版本 JDK 中,若是 string pool 设置过小而缓存的字符串过多,也会形成较大的性能开销。缓存
3)输出异常日志的时候,若是堆栈信息是明确的,能够取消输出详细堆栈,异常堆栈的构造是有成本的。注意:同一位置抛出大量重复的堆栈信息,JIT 会将其优化后成,直接抛出一个事先编译好的、类型匹配的异常,异常堆栈信息就看不到了。安全
4)避免引用类型和基础类型之间无谓的拆装箱操做,请尽可能保持一致,自动装箱发生太频繁,会很是严重消耗性能。性能优化
5)Stream API 的选择。复杂和并行操做,推荐使用 Stream API,能够简化代码,同时发挥来发挥出 CPU 多核的优点,若是是简单操做或者 CPU 是单核,推荐使用显式迭代。
6)根据业务场景,经过 ThreadPoolExecutor 手动建立线程池,结合任务的不一样,指定线程数量和队列大小,规避资源耗尽的风险,统一命名后的线程也便于后续问题排查。
7)根据业务场景,合理选择并发容器。如选择 Map 类型的容器时,若是对数据要求有强一致性,可以使用 Hashtable 或者 「Map + 锁」 ;读远大于写,使用 CopyOnWriteArrayList;存取数据量小、对数据没有强一致性的要求、变动不频繁的,使用 ConcurrentHashMap;存取数据量大、读写频繁、对数据没有强一致性的要求,使用 ConcurrentSkipListMap。
8)锁的优化思路有:减小锁的粒度、循环中使用锁粗化、减小锁的持有时间(读写锁的选择)等。同时,也考虑使用一些 JDK 优化后的并发类,如对一致性要求不高的统计场景中,使用 LongAdder 替代 AtomicLong 进行计数,使用 ThreadLocalRandom 替代 Random 类等。
代码层的优化除了上面这些,还有不少就不一一列出了。咱们能够观察到,在这些要点里,有一些共性的优化思路,是能够抽取出来的,譬如
a. 空间换时间:使用内存或者磁盘,换取更宝贵的CPU 或者网络,如缓存的使用
b. 时间换空间:经过牺牲部分 CPU,节省内存或者网络资源,如把一次大的网络传输变成屡次;
c. 其余诸如并行化、异步化、池化技术等。
2、CPU 相关
前面讲到过,咱们更应该关注 CPU 负载,CPU 利用率高通常不是问题,CPU 负载 是判断系统计算资源是否健康的关键依据。
3、CPU 利用率高&&平均负载高
这种状况常见于 CPU 密集型的应用,大量的线程处于可运行状态,I/O 不多,常见的大量消耗 CPU 资源的应用场景有:
a. 正则操做
b. 数学运算
c. 序列化/反序列化
d. 反射操做
e. 死循环或者不合理的大量循环
f. 基础/第三方组件缺陷
排查高 CPU 占用的通常思路:经过 jstack 屡次(> 5次)打印线程栈,通常能够定位到消耗 CPU 较多的线程堆栈。或者经过 Profiling 的方式(基于事件采样或者埋点),获得应用在一段时间内的 on-CPU 火焰图,也能较快定位问题。
还有一种可能的状况,此时应用存在频繁的 GC (包括 Young GC、Old GC、Full GC),这也会致使 CPU 利用率和负载都升高。排查思路:使用 jstat -gcutil 持续输出当前应用的 GC 统计次数和时间。频繁 GC 致使的负载升高,通常还伴随着可用内存不足,可用 free 或者 top 等命令查看下当前机器的可用内存大小。
CPU 利用率太高,是否有多是 CPU 自己性能瓶颈致使的呢?也是有可能的。能够进一步经过 vmstat 查看详细的 CPU 利用率。用户态 CPU 利用率(us)较高,说明用户态进程占用了较多的 CPU,若是这个值长期大于50%,应该着重排查应用自己的性能问题。内核态 CPU 利用率(sy)较高,说明内核态占用了较多的 CPU,因此应该着重排查内核线程或者系统调用的性能问题。若是 us + sy 的值大于 80%,说明 CPU 可能不足
4、CPU 利用率低&&平均负载高
若是CPU利用率不高,说明咱们的应用并无忙于计算,而是在干其余的事。CPU 利用率低而平均负载高,常见于 I/O 密集型进程,这很容易理解,毕竟平均负载就是 R 状态进程和 D 状态进程的和,除掉了第一种,就只剩下 D 状态进程了(产生 D 状态的缘由通常是由于在等待 I/O,例如磁盘 I/O、网络 I/O 等)。
排查&&验证思路:使用 vmstat 1 定时输出系统资源使用,观察 %wa(iowait) 列的值,该列标识了磁盘 I/O 等待时间在 CPU 时间片中的百分比,若是这个值超过30%,说明磁盘 I/O 等待严重,这多是大量的磁盘随机访问或直接的磁盘访问(没有使用系统缓存)形成的,也可能磁盘自己存在瓶颈,能够结合 iostat 或 dstat 的输出加以验证,如 %wa(iowait) 升高同时观察到磁盘的读请求很大,说明多是磁盘读致使的问题。
此外,耗时较长的网络请求(即网络 I/O)也会致使 CPU 平均负载升高,如 MySQL 慢查询、使用 RPC 接口获取接口数据等。这种状况的排查通常须要结合应用自己的上下游依赖关系以及中间件埋点的 trace 日志,进行综合分析。
5、CPU 上下文切换次数变高
先用 vmstat 查看系统的上下文切换次数,而后经过 pidstat 观察进程的自愿上下文切换(cswch)和非自愿上下文切换(nvcswch)状况。自愿上下文切换,是由于应用内部线程状态发生转换所致,譬如调用 sleep()、join()、wait()等方法,或使用了 Lock 或 synchronized 锁结构;非自愿上下文切换,是由于线程因为被分配的时间片用完或因为执行优先级被调度器调度所致。
若是自愿上下文切换次数较高,意味着 CPU 存在资源获取等待,好比说,I/O、内存等系统资源不足等。若是是非自愿上下文切换次数较高,可能的缘由是应用内线程数过多,致使 CPU 时间片竞争激烈,频频被系统强制调度,此时能够结合 jstack 统计的线程数和线程状态分布加以佐证。
6、内存相关
前面提到,内存分为系统内存和进程内存(含 Java 应用进程),通常咱们遇到的内存问题,绝大多数都会落在进程内存上,系统资源形成的瓶颈占比较小。对于 Java 进程,它自带的内存管理自动化地解决了两个问题:如何给对象分配内存以及如何回收分配给对象的内存,其核心是垃圾回收机制。
垃圾回收虽然能够有效地防止内存泄露、保证内存的有效使用,但也并非万能的,不合理的参数配置和代码逻辑,依然会带来一系列的内存问题。此外,早期的垃圾回收器,在功能性和回收效率上也不是很好,过多的 GC 参数设置很是依赖开发人员的调优经验。好比,对于最大堆内存的不恰当设置,可能会引起堆溢出或者堆震荡等一系列问题。
下面看看几个常见的内存问题分析思路。
一、系统内存不足
Java 应用通常都有单机或者集群的内存水位监控,若是单机的内存利用率大于 95%,或者集群的内存利用率大于80%,就说明可能存在潜在的内存问题(注:这里的内存水位是系统内存)。
除了一些较极端的状况,通常系统内存不足,大几率是由 Java 应用引发的。使用 top 命令时,咱们能够看到 Java 应用进程的实际内存占用,其中 RES 表示进程的常驻内存使用,VIRT 表示进程的虚拟内存占用,内存大小的关系为:VIRT > RES > Java 应用实际使用的堆大小。除了堆内存,Java 进程总体的内存占用,还有方法区/元空间、JIT 缓存等,主要组成以下:Java 应用内存占用 = Heap(堆区)+ Code Cache(代码缓存区) + Metaspace(元空间)+ Symbol tables(符号表)+ Thread stacks(线程栈区)+ Direct buffers(堆外内存)+ JVM structures(其余的一些 JVM 自身占用)+ Mapped files(内存映射文件)+ Native Libraries(本地库)+ ...
Java 进程的内存占用,可使用 jstat -gc 命令查看,输出的指标中能够获得当前堆内存各分区、元空间的使用状况。堆外内存的统计和使用状况,能够利用 NMT(Native Memory Tracking,HotSpot VM Java8 引入)获取。线程栈使用的内存空间很容易被忽略,虽然线程栈内存采用的是懒加载的模式,不会直接使用 +Xss 的大小来分配内存,可是过多的线程也会致使没必要要的内存占用,可使用 jstackmem 这个脚本统计总体的线程占用。
二、系统内存不足的排查思路:
a. 首先使用 free 查看当前内存的可用空间大小,而后使用 vmstat 查看具体的内存使用状况及内存增加趋势,这个阶段通常能定位占用内存最多的进程;
b. 分析缓存 / 缓冲区的内存使用。若是这个数值在一段时间变化不大,能够忽略。若是观察到缓存 / 缓冲区的大小在持续升高,则可使用 pcstat、cachetop、slabtop 等工具,分析缓存 / 缓冲区的具体占用;
c. 排除掉缓存 / 缓冲区对系统内存的影响后,若是发现内存还在不断增加,说明颇有可能存在内存泄漏。
7、Java 内存溢出
内存溢出是指应用新建一个对象实例时,所需的内存空间大于堆的可用空间。内存溢出的种类较多,通常会在报错日志里看到 OutOfMemoryError 关键字。
常见内存溢出种类及分析思路以下:
1)java.lang.OutOfMemoryError: Java heap space。缘由:堆中(新生代和老年代)没法继续分配对象了、某些对象的引用长期被持有没有被释放,垃圾回收器没法回收、使用了大量的 Finalizer 对象,这些对象并不在 GC 的回收周期内等。通常堆溢出都是因为内存泄漏引发的,若是确认没有内存泄漏,能够适当经过增大堆内存。
2)java.lang.OutOfMemoryError:GC overhead limit exceeded。缘由:垃圾回收器超过98%的时间用来垃圾回收,但回收不到2%的堆内存,通常是由于存在内存泄漏或堆空间太小。
3)java.lang.OutOfMemoryError: Metaspace或java.lang.OutOfMemoryError: PermGen space。排查思路:检查是否有动态的类加载但没有及时卸载,是否有大量的字符串常量池化,永久代/元空间是否设置太小等。
4)java.lang.OutOfMemoryError : unable to create new native Thread。缘由:虚拟机在拓展栈空间时,没法申请到足够的内存空间。可适当下降每一个线程栈的大小以及应用总体的线程个数。此外,系统里整体的进程/线程建立总数也受到系统空闲内存和操做系统的限制,请仔细检查。注:这种栈溢出,和 StackOverflowError 不一样,后者是因为方法调用层次太深,分配的栈内存不够新建栈帧致使。
此外,还有 Swap 分区溢出、本地方法栈溢出、数组分配溢出等 OutOfMemoryError 类型,因为不是很常见,就不一一介绍了。
8、Java 内存泄漏
Java 内存泄漏能够说是开发人员的噩梦,内存泄漏与内存溢出不一样则,后者简单粗暴,现场也比较好找。内存泄漏的表现是:应用运行一段时间后,内存利用率愈来愈高,响应愈来愈慢,直到最终出现进程「假死」。
Java 内存泄漏可能会形成系统可用内存不足、进程假死、OOM 等,排查思路却不外乎下面两种:
a. 经过 jmap 按期输出堆内对象统计,定位数量和大小持续增加的对象;
b. 使用 Profiler 工具对应用进行 Profiling,寻找内存分配热点。
此外,在堆内存持续增加时,建议 dump 一份堆内存的快照,后面能够基于快照作一些分析。快照虽然是瞬时值,但也是有必定的意义的。
9、垃圾回收相关
GC(垃圾回收,下同)的各项指标,是衡量 Java 进程内存使用是否健康的重要标尺。垃圾回收最核心指标:GC Pause(包括 MinorGC 和 MajorGC) 的频率和次数,以及每次回收的内存详情,前者能够经过 jstat 工具直接获得,后者须要分析 GC 日志。须要注意的是,jstat 输出列中的 FGC/FGCT 表示的是一次老年代垃圾回收中,出现 GC Pause (即 Stop-the-World)的次数,譬如对于 CMS 垃圾回收器,每次老年代垃圾回收这个值会增长2(初始标记和从新标记着两个 Stop-the-World 的阶段,这个统计值会是 2。
何时须要进行 GC 调优?这取决于应用的具体状况,譬如对响应时间的要求、对吞吐量的要求、系统资源限制等。一些经验:GC 频率和耗时大幅上升、GC Pause 平均耗时超过 500ms、Full GC 执行频率小于1分钟等,若是 GC 知足上述的一些特征,说明须要进行 GC 调优了。
因为垃圾回收器种类繁多,针对不一样的应用,调优策略也有所区别,所以下面介绍几种通用的的 GC 调优策略。
1)选择合适的 GC 回收器。根据应用对延迟、吞吐的要求,结合各垃圾回收器的特色,合理选用。推荐使用 G1 替换 CMS 垃圾回收器,G1 的性能是在逐步优化的,在 8GB 内存及如下的机器上,其各方面的表现也在遇上甚至有超越之势。G1 调参较方便,而 CMS 垃圾回收器参数太过复杂、容易形成空间碎片化、对 CPU 消耗较高等弊端,也使其目前处于废弃状态。Java 11 里新引入的 ZGC 垃圾回收器,基本可用作到全阶段并发标记和回收,值得期待。
2)合理的堆内存大小设置。堆大小不要设置过大,建议不要超过系统内存的 75%,避免出现系统内存耗尽。最大堆大小和初始化堆的大小保持一致,避免堆震荡。新生代的大小设置比较关键,咱们调整 GC 的频率和耗时,不少时候就是在调整新生代的大小,包括新生代和老年代的占比、新生代中 Eden 区和 Survivor 区的比例等,这些比例的设置还须要考虑各代中对象的晋升年龄,整个过程须要考虑的东西仍是比较多的。若是使用 G1 垃圾回收器,新生代大小这一块须要考虑的东西就少不少了,自适应的策略会决定每一次的回收集合(CSet)。新生代的调整是 GC 调优的核心,很是依赖经验,可是通常来讲,Young GC 频率高,意味着新生代过小(或 Eden 区和 Survivor 配置不合理),Young GC 时间长,意味着新生代过大,这两个方向大致不差。
3)下降 Full GC 的频率。若是出现了频繁的 Full GC 或者 老年代 GC,颇有多是存在内存泄漏,致使对象被长期持有,经过 dump 内存快照进行分析,通常能较快地定位问题。除此以外,新生代和老年代的比例不合适,致使对象频频被直接分配到老年代,也有可能会形成 Full GC,这个时候须要结合业务代码和内存快照综合分析。
此外,经过配置 GC 参数,能够帮助咱们获取不少 GC 调优所需的关键信息,如配置-XX:+PrintGCApplicationStoppedTime-XX:+PrintSafepointStatistics-XX:+PrintTenuringDistribution,分别能够获取 GC Pause 分布、安全点耗时统计、对象晋升年龄分布的信息,加上 -XX:+PrintFlagsFinal 可让咱们了解最终生效的 GC 参数等。
10、磁盘I/O和网络I/O
磁盘 I/O 问题排查思路:
a. 使用工具输出磁盘相关的输出的指标,经常使用的有 %wa(iowait)、%util,根据输判断磁盘 I/O 是否存在异常,譬如 %util 这个指标较高,说明有较重的 I/O 行为;
b. 使用 pidstat 定位到具体进程,关注下读或写的数据大小和速率;
c. 使用 lsof + 进程号,可查看该异常进程打开的文件列表(含目录、块设备、动态库、网络套接字等),结合业务代码,通常可定位到 I/O 的来源,若是须要具体分析,还可使用 perf 等工具进行 trace 定位 I/O 源头。
须要注意的是,%wa(iowait)的升高不表明必定意味着磁盘 I/O 存在瓶颈,这是数值表明 CPU 上 I/O 操做的时间占用的百分比,若是应用进程的在这段时间内的主要活动就是 I/O,那么也是正常的。
网络 I/O 存在瓶颈,可能的缘由以下:
a. 一次传输的对象过大,可能会致使请求响应慢,同时 GC 频繁;
b. 网络 I/O 模型选择不合理,致使应用总体 QPS 较低,响应时间长;
c. RPC 调用的线程池设置不合理。可以使用 jstack 统计线程数的分布,若是处于 TIMED_WAITING 或 WAITING 状态的线程较多,则须要重点关注。举例:数据库链接池不够用,体如今线程栈上就是不少线程在竞争一把链接池的锁;
d. RPC 调用超时时间设置不合理,形成请求失败较多;
Java 应用的线程堆栈快照很是有用,除了上面提到的用于排查线程池配置不合理的问题,其余的一些场景,如 CPU 飙高、应用响应较慢等,均可以先从线程堆栈入手。
11、有用的一行命令
给出若干在定位性能问题的命令,用于快速定位
1)查看系统当前网络链接数
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
2)查看堆内对象的分布 Top 50(定位内存泄漏)
jmap –histo:live $pid | sort-n -r -k2 | head-n 50
3)按照 CPU/内存的使用状况列出前10 的进程
#内存 ps axo %mem,pid,euser,cmd | sort -nr | head -10 #CPU ps -aeo pcpu,user,pid,cmd | sort -nr | head -10
4)显示系统总体的 CPU利用率和闲置率
grep "cpu " /proc/stat | awk -F ' ' '{total = $2 + $3 + $4 + $5} END {print "idle \t used\n" $5*100/total "% " $2*100/total "%"}'
5)按线程状态统计线程数(增强版)
jstack $pid | grep java.lang.Thread.State:|sort|uniq -c | awk '{sum+=$1; split($0,a,":");gsub(/^[ \t]+|[ \t]+$/, "", a[2]);printf "%s: %s\n", a[2], $1}; END {printf "TOTAL: %s",sum}';
6)查看最消耗 CPU 的 Top10 线程机器堆栈信息
推荐使用 show-busy-java-threads 脚本,该脚本可用于快速排查 Java 的 CPU 性能问题(top us值太高),自动查出运行的 Java 进程中消耗 CPU 多的线程,并打印出其线程栈,从而肯定致使性能问题的方法调用,该脚本已经用于阿里线上运维环境。连接地址:https://github.com/oldratlee/useful-scripts/。
7)火焰图生成(须要安装 perf、perf-map-agent、FlameGraph 这三个项目):
# 1. 收集应用运行时的堆栈和符号表信息(采样时间30秒,每秒99个事件); sudo perf record -F 99 -p $pid -g -- sleep 30; ./jmaps # 2. 使用 perf script 生成分析结果,生成的 flamegraph.svg 文件就是火焰图。 sudo perf script | ./pkgsplit-perf.pl | grep java | ./flamegraph.pl > flamegraph.svg
8)按照 Swap 分区的使用状况列出前 10 的进程
for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head -10
9)JVM 内存使用及垃圾回收状态统计
#显示最后一次或当前正在发生的垃圾收集的诱发缘由 jstat -gccause $pid #显示各个代的容量及使用状况 jstat -gccapacity $pid #显示新生代容量及使用状况 jstat -gcnewcapacity $pid #显示老年代容量 jstat -gcoldcapacity $pid #显示垃圾收集信息(间隔1秒持续输出) jstat -gcutil $pid 1000
10)其余的一些平常命令
# 快速杀死全部的 java 进程 ps aux | grep java | awk '{ print $2 }' | xargs kill -9 # 查找/目录下占用磁盘空间最大的top10文件 find / -type f -print0 | xargs -0 du -h | sort -rh | head -n 10
参考资料:
[1]https://github.com/superhj1987/awesome-scripts?[2]https://github.com/jvm-profiling-tools/perf-map-agent?[3]https://github.com/brendangregg/FlameGraph?[4] https://github.com/apangin/jstackmem/blob/master/jstackmem.py