性能调优
性能指标
性能指标主要是吞吐量、响应时间、QPS、TPS 等、并发用户数等,而这些性能指标又依赖于系统服务器的资源,如 CPU、内存、磁盘 IO、网络 IO 等,对于这些指标数据的收集,通常可以根据Java本身的工具或指令进行查询
几个重要的指标:
- 停顿时间(响应时间):提交请求和返回该请求的响应之间使用的时间,比如垃圾回收中 STW 的时间
- 吞吐量:对单位时间内完成的工作量(请求)的量度(可以对比 GC 的性能指标)
- 并发数:同一时刻,对服务器有实际交互的请求数
- QPS:Queries Per Second,每秒处理的查询量
- TPS:Transactions Per Second,每秒产生的事务数
- 内存占用:Java 堆区所占的内存大小
优化步骤
对于一个系统要部署上线时,则一定会对 JVM 进行调整,不经过任何调整直接上线,容易出现线上系统频繁 FullGC 造成系统卡顿、CPU 使用频率过高、系统无反应等问题
-
性能监控:通过运行日志、堆栈信息、线程快照等信息监控是否出现 GC 频繁、OOM、内存泄漏、死锁、响应时间过长等情况
-
性能分析:
- 打印 GC 日志,通过 GCviewer 或者 http://gceasy.io 来分析异常信息
-
运用命令行工具、jstack、jmap、jinfo 等
-
dump 出堆文件,使用内存分析工具分析文件
-
使用阿里 Arthas、jconsole、JVisualVM 来实时查看 JVM 状态
-
jstack 查看堆栈信息
-
性能调优:
- 适当增加内存,根据业务背景选择垃圾回收器
-
优化代码,控制内存使用
-
增加机器,分散节点压力
-
合理设置线程池线程数量
-
使用中间件提高程序效率,比如缓存、消息队列等
参数调优
对于 JVM 调优,主要就是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型
-
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值
-Xms:设置堆的初始化大小 -Xmx:设置堆的最大大小
-
设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置,则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小,来减少 YGC 发生的次数,虽然次数减少了,但 Eden 区满的时候,由于占用的空间较大,导致释放缓慢,此时 STW 的时间较长,因此需要按照程序情况去调优
-XX:SurvivorRatio
-
年轻代和老年代默认比例为 1:2,可以通过调整二者空间大小比率来设置两者的大小。
-XX:newSize 设置年轻代的初始大小 -XX:MaxNewSize 设置年轻代的最大大小, 初始大小和最大大小两个值通常相同
-
线程堆栈的设置:每个线程默认会开启 1M 的堆栈,用于存放栈帧、调用参数、局部变量等,但一般 256K 就够用,通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统
-Xss 对每个线程stack大小的调整,-Xss128k
-
一般一天超过一次 FullGC 就是有问题,首先通过工具查看是否出现内存泄露,如果出现内存泄露则调整代码,没有的话则调整 JVM 参数
-
系统 CPU 持续飙高的话,首先先排查代码问题,如果代码没问题,则咨询运维或者云服务器供应商,通常服务器重启或者服务器迁移即可解决
-
如果数据查询性能很低下的话,如果系统并发量并没有多少,则应更加关注数据库的相关问题
-
如果服务器配置还不错,JDK8 开始尽量使用 G1 或者新生代和老年代组合使用并行垃圾回收器
命令行篇
jps
jps(Java Process Statu):显示指定系统内所有的 HotSpot 虚拟机进程(查看虚拟机进程信息),可用于查询正在运行的虚拟机进程,进程的本地虚拟机 ID 与操作系统的进程 ID 是一致的,是唯一的
使用语法:jps [options] [hostid]
options 参数:
-
-q:仅仅显示 LVMID(local virtual machine id),即本地虚拟机唯一 id,不显示主类的名称等
-
-l:输出应用程序主类的全类名或如果进程执行的是 jar 包,则输出 jar 完整路径
-
-m:输出虚拟机进程启动时传递给主类 main()的参数
-
-v:列出虚拟机进程启动时的JVM参数,比如 -Xms20m -Xmx50m是启动程序指定的 jvm 参数
ostid 参数:RMI注册表中注册的主机名,如果想要远程监控主机上的 java 程序,需要安装 jstatd
jstat
jstat(JVM Statistics Monitoring Tool):用于监视 JVM 各种运行状态信息的命令行工具,可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,在没有 GUI 的图形界面,只提供了纯文本控制台环境的服务器上,它是运行期定位虚拟机性能问题的首选工具,常用于检测垃圾回收问题以及内存泄漏问题
使用语法:jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
查看命令相关参数:jstat-h 或 jstat-help
-
vmid 是进程 id 号
-
option 参数:
类装载相关:
- -class:显示 ClassLoader 的相关信息,类的装载、卸载数量、总空间、类装载所消耗的时间等
垃圾回收相关:
-
-gc:显示与GC相关的堆信息,年轻代、老年代、永久代等的容量、已用空间、GC时间合计等信息
-
-gccapacity:显示内容与 -gc 基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间
-
-gcutil:显示内容与 -gc 基本相同,但输出主要关注已使用空间占总空间的百分比
-
-gccause:与 -gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因
-
-gcnew:显示新生代 GC 状况
-
-gcnewcapacity:显示内容与 -gcnew 基本相同,输出主要关注使用到的最大、最小空间
-
-geold:显示老年代 GC 状况
-
-gcoldcapacity:显示内容与 -gcold 基本相同,输出主要关注使用到的最大、最小空间
-
-gcpermcapacity:显示永久代使用到的最大、最小空间
JIT 相关:
-
-compiler:显示 JIT 编译器编译过的方法、耗时等信息
-
-printcompilation:输出已经被 JIT 编译的方法
jinfo
jinfo(Configuration Info for Java):查看虚拟机配置参数信息,也可用于调整虚拟机的配置参数,开发人员可以很方便地找到 Java 虚拟机参数的当前值
使用语法:jinfo [options] pid
options 参数:
- no option:输出全部的参数和系统属性
- -flag name:输出对应名称的参数
- -flag [±]name:开启或者关闭对应名称的参数 只有被标记为manageable的参数才可以被动态修改
- -flag name=value:设定对应名称的参数
- -flags:输出全部的参数
- -sysprops:输出系统属性
jmap
jmap(JVM Memory Map):获取 dump 文件,还可以获取目标 Java 进程的内存相关信息,包括 Java 堆各区域的使用情况、堆中对象的统计信息、类加载信息等
使用语法:
-
jmap [options] <pid>
-
jmap [options] <executable <core>
-
jmap [options] [server_id@] <remote server IP or hostname>
option 参数:
- -dump:生成 dump 文件(Java堆转储快照,二进制文件),-dump:live 只保存堆中的存活对象
- -heap:输出整个堆空间的详细信息,包括 GC 的使用、堆配置信息,以及内存的使用信息等
- -histo:输出堆空间中对象的统计信息,包括类、实例数量和合计容量,-histo:live 只统计堆中的存活对象
- -J :传递参数给 jmap 启动的 jvm
- -finalizerinfo:显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象,仅 linux/solaris 平台有效
- -permstat:以 ClassLoader 为统计口径输出永久代的内存状态信息,仅 linux/solaris 平台有效
- -F:当虚拟机进程对 -dump 选项没有任何响应时,强制执行生成 dump 文件,仅 linux/solaris 平台有效
jhat
jhat(JVM Heap Analysis Tool):Sun JDK 提供的 jhat 命令与 jmap 命令搭配使用,用于分析 jmap 生成的 heap dump 文件(堆转储快照),jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 文件的分析结果后,用户可以在浏览器中查看分析结果
使用语法:jhat <options> <dumpfile>
options 参数:
- -stack false|true:关闭|打开对象分配调用栈跟踪
- -refs false|true:关闭|打开对象引用跟踪
- -port port-number:设置 jhat HTTP Server 的端口号,默认 7000
- -exclude exclude-file:执行对象查询时需要排除的数据成员
- -baseline exclude-file:指定一个基准堆转储
- -debug int:设置 debug 级别
- -version:启动后显示版本信息就退出
- -J :传入启动参数,比如 -J-Xmx512m
说明:jhat 命令在 JDK9、JDK10 中已经被删除,官方建议用 VisualVM 代替
jstack
jstack(JVM Stack Trace):用于生成虚拟机指定进程当前时刻的线程快照(虚拟机堆栈跟踪),线程快照就是当前虚拟机内指定进程的每一条线程正在执行的方法堆栈的集合
线程快照的作用:可用于定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等问题,用 jstack 显示各个线程调用的堆栈情况
使用语法:jstack [options] pid
options 参数:
- -F:当正常输出的请求不被响应时,强制输出线程堆栈
- -l:除堆栈外,显示关于锁的附加信息
- -m:如果调用本地方法的话,可以显示 C/C++ 的堆栈
在 thread dump 中的几种状态:
-
死锁:Deadlock
-
等待资源:Waiting on condition
-
等待获取监视器:Waiting on monitor entry
-
阻塞:Blocked
-
执行中:Runnable
-
暂停:Suspended
-
对象等待中:Object.wait() 或 TIMED_WAITING
-
停止:Parked
jcmd
jcmd 是一个多功能命令行工具,可以用来实现前面除了 jstat 之外所有命令的功能,比如 dump、内存使用、查看 Java 进程、导出线程信息、执行 GC、JVM 运行时间等
jcmd -l:列出所有的JVM进程
jcmd 进程号 help:针对指定的进程,列出支持的所有具体命令
- Thread.print:可以替换 jstack 指令
-
GC.class_histogram:可以替换 jmap 中的 -histo 操作
-
GC.heap_dump:可以替换 jmap 中的 -dump 操作
-
GC.run:可以查看GC的执行情况
-
VM.uptime:可以查看程序的总执行时间,可以替换 jstat 指令中的 -t 操作
-
VM.system_properties:可以替换 jinfo -sysprops 进程 id
-
VM.flags:可以获取 JVM 的配置参数信息
jstatd
jstatd 是一个 RMI 服务端程序,相当于代理服务器,建立本地计算机与远程监控工具的通信,jstatd 服务器将本机的 Java 应用程序信息传递到远程计算机
远程主机信息收集,前面的指令只涉及到监控本机的 Java 应用程序,而在这些工具中,一些监控工具也支持对远程计算机的监控(如 jps、jstat),为了启用远程监控,则需要配合使用 jstatd 工具。
GUI工具
工具的使用此处不再多言,推荐一个写的非常好的文章,JVM 调优部分的笔记全部参考此文章编写
视频链接:https://www.bilibili.com/video/BV1PJ411n7xZ?p=304
文章链接:https://www.yuque.com/u21195183/jvm/lv1zot
运行参数
参数选项
添加 JVM 参数选项:进入 Run/Debug Configurations → VM options 设置参数
-
标准参数选项:
java [-options] class [args...]
或java [-options] -jar jarfile [args...]
命令:
-? -help
可以输出此命令的相关选项C:\Users\Seazean>java -version java version "1.8.0_221" Java(TM) SE Runtime Environment (build 1.8.0_221-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode) # mixed mode 字样,代表当前系统使用的是混合模式
Hotspot JVM 有两种模式,分别是 Server 和 Client,分别通过 -server 和- client 设置模式:
-
32 位系统上,默认使用 Client 类型的 JVM,要使用 Server 模式,机器配置至少有 2 个以上的内核和 2G 以上的物理内存,Client 模式适用于对内存要求较小的桌面应用程序,默认使用 Serial 串行垃圾收集器
-
64 位系统上,只支持 Server 模式的 JVM,适用于需要大内存的应用程序,默认使用并行垃圾收集器
-
-
-X 参数选项:
-Xmixed 混合模式执行 (默认) -Xint 仅解释模式执行 -Xbootclasspath:<用;分隔的目录和zip/jar文件>设置搜索路径以引导类和资源 -Xbootclasspath/a:<用;分隔的目录和zip/jar文件>附加在引导类路径末尾 -Xbootclasspath/p:<用;分隔的目录和zip/jar文件>置于引导类路径之前 -Xdiag 显示附加诊断消息 -Xnoclassgc 禁用类垃圾收集 -Xincgc 启用增量垃圾收集 -Xloggc:<file> 将 GC 状态记录在文件中 (带时间戳) -Xbatch 禁用后台编译 -Xprof 输出 cpu 配置文件数据 -Xfuture 启用最严格的检查, 预期将来的默认值 -Xrs 减少 Java/VM 对操作系统信号的使用 (请参阅文档) -Xcheck:jni 对 JNI 函数执行其他检查 -Xshare:off 不尝试使用共享类数据 -Xshare:auto 在可能的情况下使用共享类数据 (默认) -Xshare:on 要求使用共享类数据, 否则将失败。 -XshowSettings 显示所有设置并继续 -XshowSettings:all 显示所有设置并继续 -XshowSettings:vm 显示所有与 vm 相关的设置并继续 -XshowSettings:properties 显示所有属性设置并继续 -XshowSettings:locale 显示所有与区域设置相关的设置并继续
-
-XX 参数选项:
#Boolean类型格式 -XX:+<option> 启用option属性 -XX:-<option> 禁用option属性 #非Boolean类型格式 -XX:<option>=<number> 设置option数值,可以带单位如k/K/m/M/g/G -XX:<option>=<string> 设置option字符值
程序运行中:
# 设置Boolean类型参数
jinfo -flag [+|-]<name> <pid>
# 设置非Boolean类型参数
jinfo -flag <name>=<value> <pid>
打印参数
-XX:+PrintCommandLineFlags 程序运行时JVM默认设置或用户手动设置的XX选项
-XX:+PrintFlagsInitial 打印所有XX选项的默认值
-XX:+PrintFlagsFinal 打印所有XX选项的实际值
-XX:+PrintVMOptions 打印JVM的参数
内存参数
# 栈
-Xss128k <==> -XX:ThreadStackSize=128k 设置线程栈的大小为128K
# 堆
-Xms2048m <==> -XX:InitialHeapSize=2048m 设置JVM初始堆内存为2048M(默认为物理内存的1/64)
-Xmx2048m <==> -XX:MaxHeapSize=2048m 设置JVM最大堆内存为2048M(默认为物理内存的1/4)
-Xmn2g <==> -XX:NewSize=2g 设置年轻代大小为2G
-XX:SurvivorRatio=8 设置Eden区与Survivor区的比值,默认为8
-XX:NewRatio=2 设置老年代与年轻代的比例,默认为2
-XX:+UseAdaptiveSizePolicy 设置大小比例自适应,默认开启
-XX:PretenureSizeThreadshold=1024 设置让大于此阈值的对象直接分配在老年代,只对Serial、ParNew收集器有效
-XX:MaxTenuringThreshold=15 设置新生代晋升老年代的年龄限制,默认为15
-XX:TargetSurvivorRatio 设置MinorGC结束后Survivor区占用空间的期望比例
# 方法区
-XX:MetaspaceSize / -XX:PermSize=256m 设置元空间/永久代初始值为256M
-XX:MaxMetaspaceSize / -XX:MaxPermSize=256m 设置元空间/永久代最大值为256M
-XX:+UseCompressedOops 使用压缩对象
-XX:+UseCompressedClassPointers 使用压缩类指针
-XX:CompressedClassSpaceSize 设置Klass Metaspace的大小,默认1G
# 直接内存
-XX:MaxDirectMemorySize 指定DirectMemory容量,默认等于Java堆最大值
说明:参数前面是+
号说明是开启,如果是-
号说明是关闭
OOM参数
-XX:+HeapDumpOnOutMemoryError 内存出现OOM时生成Heap转储文件,两者互斥
-XX:+HeapDumpBeforeFullGC 出现FullGC时生成Heap转储文件,两者互斥
-XX:HeapDumpPath=<path> 指定heap转储文件的存储路径,默认当前目录
-XX:OnOutOfMemoryError=<path> 指定可行性程序或脚本的路径,当发生OOM时执行脚本
日志参数
-XX:+PrintGC <==> -verbose:gc 打印简要日志信息
-XX:+PrintGCDetails 打印详细日志信息
-XX:+PrintGCTimeStamps 打印程序启动到GC发生的时间,搭配-XX:+PrintGCDetails使用
-XX:+PrintGCDateStamps 打印GC发生时的时间戳,搭配-XX:+PrintGCDetails使用
-XX:+PrintHeapAtGC 打印GC前后的堆信息,如下图
-Xloggc:<file> 输出GC导指定路径下的文件中
-XX:+TraceClassLoading 监控类的加载
-XX:+PrintTenuringDistribution 打印JVM在每次MinorGC后当前使用的Survivor中对象的年龄分布
-XX:+PrintGCApplicationStoppedTime 打印GC时线程的停顿时间
-XX:+PrintGCApplicationConcurrentTime 打印垃圾收集之前应用未中断的执行时间
-XX:+PrintReferenceGC 打印回收了多少种不同引用类型的引用
-XX:+UseGCLogFileRotation 启用GC日志文件的自动转储
-XX:NumberOfGCLogFiles=1 设置GC日志文件的循环数目
-XX:GCLogFileSize=1M 设置GC日志文件的大小
其他参数
-XX:+DisableExplicitGC 禁用hotspot执行System.gc(),默认禁用
-XX:+DoEscapeAnalysis 开启逃逸分析
-XX:+UseBiasedLocking 开启偏向锁
-XX:+UseLargePages 开启使用大页面
-XX:+PrintTLAB 打印TLAB的使用情况
-XX:TLABSize 设置TLAB大小
-XX:ReservedCodeCacheSize=<n>[g|m|k]、-XX:InitialCodeCacheSize=<n>[g|m|k] 指定代码缓存大小
-XX:+UseCodeCacheFlushing 放弃一些被编译的代码,避免代码缓存被占满时JVM切换到interpreted-only的情况
代码获取
Java 提供了 java.lang.management 包用于监视和管理 Java 虚拟机和 Java 运行时中的其他组件,允许本地或远程监控和管理运行的 Java 虚拟机。ManagementFactory 类较为常用,Runtime 类可获取内存、CPU 核数等相关的数据,通过使用这些方法,可以监控应用服务器的堆内存使用情况,设置一些阈值进行报警等处理
public class MemoryMonitor {
public static void main(String[] args) {
MemoryMXBean memorymbean = ManagementFactory.getMemoryMXBean();
MemoryUsage usage = memorymbean.getHeapMemoryUsage();
System.out.println("INIT HEAP: " + usage.getInit() / 1024 / 1024 + "m");
System.out.println("MAX HEAP: " + usage.getMax() / 1024 / 1024 + "m");
System.out.println("USE HEAP: " + usage.getUsed() / 1024 / 1024 + "m");
System.out.println("\nFull Information:");
System.out.println("Heap Memory Usage: " + memorymbean.getHeapMemoryUsage());
System.out.println("Non-Heap Memory Usage: " + memorymbean.getNonHeapMemoryUsage());
System.out.println("====通过java来获取相关系统状态====");
System.out.println("当前堆内存大小totalMemory " + (int) Runtime.getRuntime().totalMemory() / 1024 / 1024 + "m");// 当前堆内存大小
System.out.println("空闲堆内存大小freeMemory " + (int) Runtime.getRuntime().freeMemory() / 1024 / 1024 + "m");// 空闲堆内存大小
System.out.println("最大可用总堆内存maxMemory " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "m");// 最大可用总堆内存大小
}
}
日志分析
日志分类
HotSpot VM 的 GC 按照回收区域分为两类:一种是部分收集(Partial GC),一种是整堆收集(Full GC)
-
部分收集(Partial GC):不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代(Eden/S0、S1)的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集,只有 CMS GC 会有单独收集老年代的行为
-
混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集,只有 G1 GC 会有这种行为
-
整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。
Minor GC/Young GC 日志:
[GC (Allocation Failure) [PSYoungGen: 31744K->2192K (36864K) ] 31744K->2200K (121856K), 0.0139308 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
Full GC 日志:
[Full GC (Metadata GC Threshold) [PSYoungGen: 5104K->0K (132096K) ] [Par01dGen: 416K->5453K (50176K) ]5520K->5453K (182272K), [Metaspace: 20637K->20637K (1067008K) ], 0.0245883 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
日志解析
通过日志看垃圾收集器:
-
Serial 收集器:新生代显示
[DefNew
,即Default New Generation
-
ParNew 收集器:新生代显示
[ParNew
,即Parallel New Generation
-
Parallel Scavenge 收集器:新生代显示
[PSYoungGen
,JDK1.7 使用的 PSYoungGen -
Parallel Old 收集器:老年代显示
[ParoldGen
-
G1 收集器:显示
garbage-first heap
通过日志看 GC 原因:
-
Allocation Failure:表明本次引起 GC 的原因是因为新生代中没有足够的区域存放需要分配的数据
-
Metadata GCThreshold:Metaspace 区不足
-
FErgonomics:JVM 自适应调整导致的 GC
-
System:调用了 System.gc() 方法
通过日志看 GC 前后情况:GC 前内存占用 → GC 后内存占用(该区域内存总大小)
[PSYoungGen: 5986K->696K (8704K)] 5986K->704K (9216K)
-
中括号内:GC 回收前年轻代堆大小 → 回收后大小(年轻代堆总大小)
-
括号外:GC 回收前年轻代和老年代大小 → 回收后大小(年轻代和老年代总大小)
- Minor GC 堆内存总容量 = 9/10 年轻代 + 老年代,Survivor 区只计算 from 部分,而 JVM 默认年轻代中 Eden 区和 Survivor 区的比例关系:Eden:S0:S1=8:1:1
通过日志看 GC 时间:GC 日志中有三个时间 user、sys、real
-
user:进程执行用户态代码(核心之外)所使用的时间,这是执行此进程所使用的实际 CPU 时间,其他进程和此进程阻塞的时间并不包括在内,在垃圾收集的情况下,表示 GC 线程执行所使用的 CPU 总时间。
-
sys:进程在内核态消耗的 CPU 时间,即在内核执行系统调用或等待系统事件所使用的 CPU 时间
-
real:程序从开始到结束所用的时钟时间,这个时间包括其他进程使用的时间片和进程阻塞的时间(比如等待 I/O 完成),对于并行 GC,这个数字应该接近(用户时间+系统时间)除以垃圾收集器使用的线程数
由于多核的原因,一般的 GC 事件中,real time 小于 sys time+user time,因为是多个线程并发的去做 GC。如果 real>sys+user 的话,则说明 IO 负载非常重或 CPU 不够用
分析工具
GCEasy 是一款在线的 GC 日志分析器,可以通过 GC 日志分析进行内存泄露检测、GC 暂停原因分析、JVM 配置建议优化等功能,大多数功能是免费的
- 官网地址:https://gceasy.io/
GCViewer 是一款离线的 GC 日志分析器,用于可视化 Java VM 选项 -verbose:gc 和 .NET 生成的数据 -Xloggc:,还可以计算与垃圾回收相关的性能指标(吞吐量、累积的暂停、最长的暂停等),当通过更改世代大小或设置初始堆大小来调整特定应用程序的垃圾回收时,此功能非常有用
-
源码下载:https://github.com/chewiebug/GCViewer
-
运行版本下载:https://github.com/chewiebug/GCViewer/wiki/Changelog
参考文章:https://www.yuque.com/u21195183/jvm/ukmb3k