更多相关内容可查看
JVM什么时候需要调优–面试的时候,所以本篇会针对面试的角度去描述JVM调优
JVM什么时候需要调优
- 性能下降
- 响应时间变慢:应用程序的响应时间超过了可接受的阈值。
- 处理速度变慢:任务处理的速度显著下降,特别是在高并发应用中。
- 垃圾回收问题
- 频繁的 Full GC:如果 Full GC 的频率变得很高,可能会影响应用程序的性能。可以通过 JVM 的监控工具(如 Java VisualVM、JConsole)观察到这些情况。
- 长时间停顿:垃圾回收引起的停顿时间过长,导致用户体验不佳。
- 内存使用情况
- 内存泄漏:应用程序的内存使用量不断增加,但没有相应的释放,可能导致
OutOfMemoryError
。 - 碎片化:老年代的内存碎片化,可能导致有效内存无法被使用。
- CPU 使用率高
- 如果应用程序在运行过程中 CPU 使用率持续居高不下,尤其是在进行垃圾回收时,会需要调优。
- 线程问题
- 线程争用:在多线程环境中,如果线程之间竞争过于激烈,导致性能瓶颈,通常需要调优线程的管理和调度。
- 死锁:线程间的死锁情况可能导致程序无法继续执行,亦需进行调优。
- 应用程序扩展
- 增加用户负载:当预计应用的用户负载增加时,可能需要重新评估并调优 JVM 设置,以适应变化。
- 上线新功能或模块:添加新功能后,可能需要监控性能并进行相应的调优。
- 性能测试
- 基准测试:在应用程序部署到生产环境前,通过负载测试发现性能瓶颈,通常会促使调优。
- 运行环境变化
- 硬件升级:如果服务器硬件有所升级,可能需要重新评估和调优 JMV 参数以充分利用新硬件的性能。
本篇会以垃圾回收的角度去聊一个JVM调优的案例
调优思路(垃圾回收器方向)
直接升级垃圾回收器:比如将CMS切换到G1或者ZGC,但是你需要了解这三个垃圾回收器的原理,面试官会问你为什么切,切换能解决什么问题,有什么好处等等,可查看这篇一篇聊透内存泄露,栈内存溢出,JVM中的垃圾回收器超详细
调优思路(逐步排查)
分析问题、定位问题、解决问题,三个角度去描述,描述你具体怎么做的
分析问题
分析问题就必须知道一些具体的参数值是否在正常范围内,及一些分析问题的手段或者工具、比如:
CPU指标:
- 查看占用CPU最多的进程
- 查看占用CPU最多的线程
- 查看线程堆栈快照信息
- 分析代码执行热点
- 查看哪个代码占用CPU执行时间最长
- 查看每个方法占用CPU时间比例
常见命令:
// 显示系统各个进程的资源使用情况
top
// 查看某个进程中的线程占用情况
top -Hp pid
// 查看当前 Java 进程的线程堆栈信息
jstack pid
常见的工具:JProfiler、JVM Profiler、Arthas等。
JVM 内存指标:
- 查看当前 JVM 堆内存参数配置是否合理
- 查看堆中对象的统计信息
- 查看堆存储快照,分析内存的占用情况
- 查看堆各区域的内存增长是否正常
- 查看是哪个区域导致的GC
- 查看GC后能否正常回收到内存
常见的命令:
// 查看当前的 JVM 参数配置
ps -ef | grep java
// 查看 Java 进程的配置信息,包括系统属性和JVM命令行标志
jinfo pid
// 输出 Java 进程当前的 gc 情况
jstat -gc pid
// 输出 Java 堆详细信息
jmap -heap pid
// 显示堆中对象的统计信息
jmap -histo:live pid
// 生成 Java 堆存储快照dump文件
jmap -F -dump:format=b,file=dumpFile.phrof pid
常见的工具:Eclipse MAT、JConsole等。
JVM GC指标
- 查看每分钟GC时间是否正常
- 查看每分钟YGC次数是否正常
- 查看FGC次数是否正常
- 查看单次FGC时间是否正常
- 查看单次GC各阶段详细耗时,找到耗时严重的阶段
- 查看对象的动态晋升年龄是否正常
GC日志常用 JVM 参数
// 打印GC的详细信息
-XX:+PrintGCDetails
// 打印GC的时间戳
-XX:+PrintGCDateStamps
// 在GC前后打印堆信息
-XX:+PrintHeapAtGC
// 打印Survivor区中各个年龄段的对象的分布信息
-XX:+PrintTenuringDistribution
// JVM启动时输出所有参数值,方便查看参数是否被覆盖
-XX:+PrintFlagsFinal
// 打印GC时应用程序的停止时间
-XX:+PrintGCApplicationStoppedTime
// 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
-XX:+PrintReferenceGC
正常参数范围:
- jvm.gc.time:每分钟的GC耗时在1s以内,500ms以内尤佳
- jvm.gc.meantime:每次YGC耗时在100ms以内,50ms以内尤佳
- jvm.fullgc.count:FGC最多几小时1次,1天不到1次尤佳
- jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳
知道了以上内容才能去评判是否出现了问题,以下案例参考网络
环境:ParNew + CMS + JDK8
问题:服务频繁出现FGC
所以对这个问题分析,我们就要看gc的日志进行分析
定位问题
查看gc日志:
java -Xms512m -Xmx1024m -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -jar YourApp.jar
cat gc.log
根据日志可以分析问题:
Metaspace used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
参数含义:
- used :已使用的空间大小
- capacity:当前已经分配且未释放的空间容量大小
- committed:当前已经分配的空间大小
- reserved:预留的空间大小
结论:used 和 capacity 两者之差较大,说明此时存在内存碎片化的情况
继续分析为什么会出现内存碎片化的情况
生成dump 堆存储文件:
jmap -F -dump:format=b,file=dumpFile.phrof pid
然后把dump文件扔到Eclipse MAT中分析
Eclipse MAT使用方法:
分析如图(知道大概长什么样子,图示来源网络):
定位大对象:点击直方图图标(Histogram),对象会按内存大小排序,查看内存占用最大的对象
这个对象被谁引用:点击支配树(dominator tree),看大对象被哪个线程调用。这里可以看到是被主线程调用
定位具体代码:点击概述图标(thread_overview),看线程的方法调用链和堆栈信息,查看大对象所属类和第几行,定位到具体代码,解决问题。
分析结果:通过 dump 堆存储文件发现存在大量 DelegatingClassLoader
通过进一步分析,发现是由于反射导致创建大量 DelegatingClassLoader。其核心原理如下:
在 JVM 上,最初是通过 JNI
调用来实现方法的反射调用,当 JVM 注意到通过反射经常访问某个方法时,它将生成字节码来执行相同的操作,称为膨胀(inflation)机制
。如果使用字节码的方式,则会为该方法生成一个DelegatingClassLoader,如果存在大量方法经常反射调用,则会导致创建大量 DelegatingClassLoader。
反射调用频次达到多少才会从 JNI 转字节码?
默认是15次,可通过参数 -Dsun.reflect.inflationThreshold
进行控制,在小于该次数时会使用 JNI 的方式对方法进行调用,如果调用次数超过该次数就会使用字节码的方式生成方法调用。
分析结论:反射调用导致创建大量 DelegatingClassLoader,占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发 FGC。
解决问题
1)适当调大 metaspace 的空间大小。
java -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=1g -jar your-application.jar
2)优化不合理的反射调用。例如最常见的属性拷贝工具类 BeanUtils.copyProperties 可以使用 mapstruct 替换。