(点击上方公众号,可快速关注)
泉源 :飒然Hang
链接:www.rowkey.me/blog/2016/11/02/java-profile/
对于调优这个事变 来说 ,一样平常 就是三个过程:
性能监控:题目 没有发生,你并不知道你必要 调优什么?此时必要 一些体系 、应用的监控工具来发现题目 。
性能分析:题目 已经发生,但是你并不知道题目 到底出在那边 。此时就必要 利用 工具、履历 对体系 、应用举行 瓶颈分析,以求定位到题目 缘故起因 。
性能调优:颠末 上一步的分析定位到了题目 地点 ,必要 对题目 举行 办理 ,利用 代码、设置 等本领 举行 优化。
Java调优也不外乎这三步 。
别的 ,本文所讲的性能分析 、调优等是抛开以下因素的:
体系 底层环境 :硬件、操纵 体系 等
数据布局 和算法的利用
外部体系 如数据库、缓存的利用
调优预备
调优是必要 做好预备 工作的 ,毕竟 每一个应用的业务目标 都不尽雷同 ,性能瓶颈也不会总在同一个点上。在业务应用层面,我们必要 :
必要 相识 体系 的总体架构 ,明白 压力方向。比如 体系 的哪一个接口 、模块是利用 率最高的,面对 高并发的挑衅 。
必要 构建测试环境 来测试应用的性能,利用 ab、loadrunner、jmeter都可以。
对关键业务数据量举行 分析 ,这里重要 指的是对一些数据的量化分析,如数据库一天的数据量有多少;缓存的数据量有多大等
相识 体系 的相应 速率 、吞吐量、TPS、QPS等指标需求,比如 秒杀体系 对相应 速率 和QPS的要求黑白 常高的。
相识 体系 相干 软件的版本 、模式和参数等 ,偶然 间 限于应用依靠 服务的版本、模式等,性能也会受到肯定 的影响 。
别的 ,我们还必要 相识 Java相干 的一些知识:
Java内存相干 :这一部分 可以拜见 谈谈Java内存管理一文
对Java代码举行 基准性能测试:可以利用 JMH来举行 ,[译]利用 JMH举行 微基准测试:不要猜 ,要测试!。
HotSpot VM相干 知识:https://www.oracle.com/technetwork/cn/java/javase/tech/index-jsp-136373-zhs.html
jdk自带各种java工具:https://www.rowkey.me/blog/2016/11/03/jdk-tools/
性能分析
在体系 层面可以或许 影相应 用性能的一样平常 包罗 三个因素:CPU、内存和IO,可以从这三方面举行 程序的性能瓶颈分析。
CPU分析
当程序相应 变慢的时间 ,起首 利用 top 、vmstat、ps等下令 查察 体系 的cpu利用 率是否有非常 ,从而可以判定 出是否是cpu繁忙造成的性能题目 。此中 ,重要 通过us(用户进程 所占的%)这个数据来看非常 的进程 信息。当us靠近 100%乃至 更高时,可以确定是cpu繁忙造成的相应 迟钝 。一样平常 说来 ,cpu繁忙的缘故起因 有以下几个:
线程中有无穷 空循环、无壅闭 、正则匹配大概 单纯的盘算
发生了频仍 的gc
多线程的上下文切换
确定好cpu利用 率最高的进程 之后就可以利用 jstack来打印出非常 进程 的堆栈信息:
jstack [pid]
接下来必要 留意 的一点是,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]
还可以通过vmstat,通过观察内核状态的上下文切换(cs)次数 ,来判定 是否是上下文切换造成的cpu繁忙 。
vmstat 1 5
别的 ,偶然 间 大概 会由jit引起一些cpu飚高的情况 ,如大量方法编译等。这里可以利用 -XX:+PrintCompilation这个参数输出jit编译环境 ,以排查jit编译引起的cpu题目 。
内存分析
对Java应用来说,内存重要 是由堆外内存和堆内内存构成 。
1. 堆外内存堆外内存重要 是JNI、Deflater/Inflater、DirectByteBuffer(nio中会用到)利用 的。对于这种堆外内存的分析,还是 必要 先通过vmstat 、sar、top、pidstat等查察 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。
文件IO可以利用 体系 工具pidstat、iostat、vmstat来查察 io的状态 。这里可以看一张利用 vmstat的结果 图。
这里重要 留意 bi和bo这两个值 ,分别表现 块装备 每秒吸取 的块数量 和块装备 每秒发送的块数量 ,由此可以判定 io繁忙状态 。进一步的可以通过利用 strace工具定位对文件io的体系 调用 。通常,造成文件io性能差的缘故起因 不外乎:
大量的随机读写
装备 慢
文件太大
网络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入门到纯熟 小工完全指南。
JwebapJwebap是一款JavaEE性能检测框架,基于asm加强 字节码实现。支持:http哀求 、jdbc毗连 、method的调用轨迹跟踪以及次数 、耗时的统计 。由此可以获取最耗时的哀求 、方法,并可以查察 jdbc毗连 的次数、是否关闭等。但此项目是2006年的一个项目 ,已经将近 10年没有更新。根据笔者利用 ,已经不支持jdk7编译的应用 。假如 要利用 ,发起 基于原项目二次开辟 ,同时也可以参加 对redis毗连 的轨迹跟踪。固然 ,基于字节码加强 的原理,也可以实现本身 的JavaEE性能监测框架。
上图来自笔者公司二次开辟 过的jwebap ,已经支持jdk8和redis毗连 追踪。
useful-s这里有一个本人参加 的开源的项目:https://github.com/superhj1987/useful-s,封装了很多 常用的性能分析下令 ,比如 上文讲的打印繁忙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等 ,别的 Discuptor和Actor在符合 的场景也可以利用 。
内存调优
内存的调优重要 就是对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编译友爱 么?
别的 ,jdk7、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参数时 ,最好是先通过以上下令 查察 对应参数的默认值再确定是否必要 设置。也最好不要设置 你搞不清用途的参数,毕竟 默认值的设置是有它的公道 之处的 。
动态设置参数当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]
其他的参数设置雷同 。
2. -verbose:gc 与 -XX:+PrintGCDetails很多 gc保举 设置都同时设置了这两个参数,着实 ,只要打开了-XX:+PrintGCDetails ,前面的选项也会同时打开,无须重复设置。
3. -XX:+DisableExplicitGC这个参数的作用就是使得system.gc变为空调用,很多 保举 设置内里 都是发起 开启的 。但是,假如 你用到了NIO大概 其他利用 到堆外内存的环境 ,利用 此选项会造成oom。可以用XX:+ExplicitGCInvokesConcurrent或XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses(共同 CMS利用 ,使得system.gc触发一次并发gc)代替 。别的 ,尚有 一个比力 故意 思的地方 。假如 你不设置此选项的话 ,当你利用 了RMI的时间 ,会周期性地来一次full gc。这个征象 是由于分布式gc造成的,为RMI服务。具体 的可见此链接内容中与dgc相干 的:https://docs.oracle.com/javase/6/docs/technotes/guides/rmi/sunrmiproperties.html
4. MaxDirectMemorySize此参数是设置的堆外内存的上限值。当不设置的时间 为-1 ,此值为-Xmx减去一个survivor space的预留巨细 。
5. 由于遗留缘故起因 ,作用雷同 的参数
-Xss 与 -XX:ThreadStackSize
-Xmn 与 -XX:NewSize,别的 这里必要 留意 的是设置了-Xmn的话 ,NewRatio就没作用了。
6. -XX:MaxTenuringThreshold利用 工具查察 此值默认值为15,但是选择了CMS的时间 ,此值会变成 4。当此值设置为0时 ,全部 eden里的活对象在履历 第一次minor GC的时间 就会直接提拔 到old gen,survivor space直接就没用 。
7. -XX:HeapDumpPath利用 此参数可以指定-XX:+HeapDumpBeforeFullGC、-XX:+HeapDumpAfterFullGC、-XX:+HeapDumpOnOutOfMemoryError触发heap dump文件的存储位置。
参考资料
Java HotSpot? Virtual Machine Performance Enhancements
Java HotSpot Virtual Machine Garbage Collection Tuning Guide
[HotSpot VM] JVM调优的 ”标准 参数”的各种陷阱
关注「ImportNew」
看更多 Java 技能 精选文章
↓↓↓