GC调优
GC调优的主要目标是避免由垃圾回收引起程序性能下降。
GC调优的核心指标
- 垃圾回收吞吐量:执行用户代码时间/(执行用户代码时间 + GC时间)
- 延迟:GC延迟 + 业务执行时间
- 内存使用量
GC调优步骤
- 发现问题:通过监控工具。
- 诊断问题:通过分析工具。
- 修复问题:调整JVM参数或修复源代码。
- 测试验证
GC调优发现问题的工具
- jstat:jdk自带的可以提供垃圾回收的信息。 jstat -gc 进程ID 每次统计的间隔(毫秒) 统计次数。
- Visualvm插件:Visual GC插件,实时监控Java进程的堆内存结构、堆内存变化趋势以及垃圾回收时间的变化趋势。适合开发环境,对程序有影响,生产环境无权限。
- Prometheus + Grafana:非常详细,环境复杂,运维搭建。
分析GC日志
- GCViewer:日志转为可视化图表 java -jar gcviewer_1.3.4.jar 日志文件.log
https://github.com/chewiebug/GCViewer
- GCEasy:在线的可视化工具图表
常见的GC模式
- 正常情况
- 缓存对象过多
- 内存泄漏
- 持续的FullGC:请求量激增,生产更多对象,gc无法跟上对象创建速率。
- 元空间不足:堆中内存足够
GC调优的手段
- 优化基础JVM参数:基础JVM参数的设置不当,会导致频繁FULLGC的产生。
- 减少对象产生:大多数场景下的FULLGC是由于对象产生速度过快导致的。
- 更换垃圾回收器:选择适合当前业务场景的垃圾回收器,减少延迟、提高吞吐量。
- 优化垃圾回收器参数:
优化基础JVM参数
堆、栈、元空间、垃圾回收器、堆内存快照、日志
- -Xmx 和 --Xms 最大堆内存、初始堆内存。根据最大并发量估算服务器的配置,然后再减去系统和其他程序所需的内存。建议一样大,减少申请内存次数。
- -XX:MaxMetaspaceSize 和 -XX:MetaspaceSize 最大元空间大小、第一次FULLGC的阈值(之后JVM自行计算)。
- -Xss 虚拟机栈大小。默认大小取决于操作系统和体系结构。Linux x86 1m。合理值256k~1m。
- 不建议手动设置。
- -Xmn 年轻代的大小,默认值为整个堆的1/3。G1会动态调整年轻代大小。
- -XX:SurvivorRatio 伊甸园区和幸存者区的大小比例,默认值为8。
- -XX:MaxTenuringThreshold 最大晋升阈值,年龄大于此值,会进入老年代。JVM有动态年龄判断机制:将年龄从小到大的对象占据的空间加起来,如果大于survivor区域的50%,把等于或大于该年龄的对象,放入到老年代。
- 其他参数
- -XX:+DisableExplicitGC 禁止在代码中使用System.gc(),因为可能会引起FULLGC。
- -XX:+HeapDumpOnOutOfMemoryError 发生OOM错误时,自动生成hprof内存快照文件。
- -XX:HeapDumpPath= 指定hprof文件的输出路径。
- 打印GC日志:8及之前: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路径
- 打印GC日志:9及之后: -Xlog:gc*:file=文件路径
JVM参数模板
Java
-Xms1g
-Xmx1g
-Xss256k
-XX:MaxMetaspaceSize=512m
-XX:+DisableExplicitGC-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/logs/my-service.hprof-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:文件路径
实战
保存hprof文件,Heap Hero分析文件
性能调优
问题:top命令CPU占用率高;单个服务处理时间特别长;程序启动后正常,一段时间后无法处理任何请求。
线程转储
线程转储(Thread Dump)提供了对所有运行中的线程当前状态的快照。
bash
jstack 进程id
jstack 进程id > 文件名 # 导出线程栈文件
线程转储的可视化在线分析平台: 1、 https://jstack.review/ 2、 https://fastthread.io/
CPU占用率高
- 通过top命令找到进程和线程。
- 使用jstack打印线程快照。
- 找到线程快照正在执行的方法,并优化性能。
bash
top -c #找到进程id
top -p 进程id #再摁H可以查看进程的线程
jstack 进程id #查看栈信息
printf '%x\n' #通过16进制,在文件中查看线程信息(文件中线程id是16进制)
# 找到栈信息中源代码位置
关注状态为RUNNABLE的线程。一些线程执行本地方法时并不会消耗CPU,只是在等待。但 JVM 仍然会将它们标识成"RUNNABLE"状态。
接口响应时间长
方法1:通过arthas的trace和watch命令,监控方法的执行耗时和参数、返回值等信息,定位性能瓶颈,并优化性能。
trace 类名 方法名
--skipJDKMethod false
输出JDK核心包中的方法及耗时。- '#cost > 毫秒值' 。
--n 数值
,最多显示该数值条。stop
结束监控,重置arthas增强的对象。
方法2:通过arthas的profile火焰图功能,找到火焰图中顶部较平的方法,一般就是性能问题产生的根源,并优化性能。
- 使用了for循环向ArrayList中添加数据。ArrayList扩容需要copyof复制老到新的数组。
线程不可用、死锁
死锁或时间长:通过jstack、visualvm、fastthread.io 等工具,找到线程死锁的原因,解决死锁问题。
使用线程快照生成工具就可以看到死锁的根源。文件中搜索deadlock
解决思路:
1、检测是否有死锁产生,无法自动解除的死锁会将线程永远阻塞。
2、如果没有死锁,再使用案例1的打印线程栈的方法检测线程正在执行哪个方法,一般这些大量出现的方法就是慢方法。
判断方法耗时
使用OpenJDK中的jmh基准测试框架对某些特定的方法比如加密算法进行基准测试,jmh可以完全模拟运行环境中的Java虚拟机参数,同时支持预热能通过JIT执行优化后的代码获得更为准确的数据。注意添加黑洞消费,否则会被优化。
官网地址:https://github.com/openjdk/jmhc
Date 和 LocalDateTime