本文转载于:面试官:如何进行 JVM 调优(附真实案例
面试官:在工作中做过 JVM 调优吗?讲讲做过哪些 JVM 调优?
我一个QPS不到10的项目,上次问我缓存穿透缓存雪崩,这次问我 JVM 调优,我是真滴难。
不过大家别慌,热心的我给大家找来了几个满分回答,大家选择合适的使用。
回答1:听好了,下面将是我第一次 JVM 调优。
回答2:我一般面试的时候才调优。
回答3:我一般直接加机器、加内存。
回答4:老子直接用的 ZGC,调个蛇皮。
JVM 经过这么多年的发展和验证,整体是非常健壮的。个人认为99%的情况下,基本用不到 JVM 调优。
通常来说,我们的 JVM 参数配置大多还是会遵循 JVM 官方的建议,例如:
-XX:NewRatio=2,年轻代:老年代=1:2
-XX:SurvivorRatio=8,eden:survivor=8:1
堆内存设置为物理内存的3/4左右
等等
JVM 参数的默认(推荐)值都是经过 JVM 团队的反复测试和前人的充分验证得出的比较合理的值,因此通常来说是比较靠谱和通用的,一般不会出大问题。
当然,更重要的是,大部分的应用 QPS 都不到10,数据量不到几万,这种低压环境下,想让 JVM 出问题,说实话也挺难的。
大部分同学更常遇到的应该是自己的代码 bug 导致 OOM、CPU load高、GC频繁啥的,这些场景也基本都是代码修复即可,通常不需要动 JVM。
当然,俗话说得好,凡事无绝对,还是有一小部分场景,是可能需要用到 JVM 调优的。具体哪些场景,我们在下面介绍。
值得一提的是,我们这边所说的 JVM 调优更多的是针对自己的业务场景对 JVM 参数进行优化调整,使其更适合我们的业务,而不是指对 JVM 源码的改动。
这是我在网上看到的一个说法,因为赞同的人比较多,我估计有不少同学也会有这个想法,因此在这边谈下自己的看法。
1)实战角度
不考虑应付面试的因素,升级垃圾回收器确实会是最有效的方式之一,例如:CMS 升级到 G1,甚至 ZGC。
这个很容易理解,更高版本的垃圾回收器相当于是 JVM 开发人员对 JVM 做的优化,人家毕竟是专门做这个的,所以通常来说升级高版本的性能会有不少的提升。
G1 目前已经有开始在逐渐应用开来,周围有不少团队在 JDK8 中使用了 G1,就我了解到的,还是存在不少问题的,不少同学在不断进行参数的调整,而在 JDK11 中能优化成啥样还有待验证。
ZGC 目前应用的还比较少,仅从对外公布的数据来看很好看,最大暂停时间不超过10ms,甚至是1ms,大家都抱有很高的期望。但是从目前我收集到的一些资料来看,ZGC 也并不是银弹,已知的明显问题有:
吞吐量相较于 G1 会有所下降,官方称最大不超过15%
ZGC如果遇到非常高的对象分配速率(allocation rate)的话会跟不上,目前唯一有效的“调优”方式就是增大整个GC堆的大小来让ZGC有更大的喘息空间——R大与ZGC领队沟通后的原话
而且,随着后续 ZGC 应用开来,后续一定会不断出现更多问题的。
整体而言,个人觉得 JVM 调优在某些场景下还是有必要的,毕竟有句话叫:没有最好的,只有最合适的。
2)面试角度
如果你回答直接升级垃圾收集器,面试官可能也赞同,但是这个话题可能就这样结束了,面试官大概率没听到他想要的回答,你在这题的肯定拿不到加分,甚至可能会被扣分。
所以,在面试的时候,你可以回答升级垃圾收集器,但是你不能只回答升级垃圾收集器。
忌过早优化。《计算机程序设计艺术》的作者高德纳(Donald Ervin Knuth)曾说过一句经典的话:
The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.
真正的问题是,程序猿在错误的地方和错误的时间花了太多的时间担心效率问题;过早的优化是编程中所有(或者至少是大部分)罪恶的根源。
忌过早并不是说就完全不管,比较正确的做法应该是给核心服务的一些重要 JVM 指标配上监控告警,当指标出现波动或者异常时,能及时介入排查。
面试官:JVM 有哪些核心指标?合理范围应该是多少?
这个问题没有统一的答案,因为每个服务对AVG/TP999/TP9999等性能指标的要求是不同的,因此合理的范围也不同。
为了防止面试官追问,对于普通的 Java 后端应用来说,我这边给出一份相对合理的范围值。以下指标都是对于单台服务器来说:
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以内尤佳
通常来说,只要这几个指标正常,其他的一般不会有问题,如果其他地方出了问题,一般都会影响到这几个指标。
4.1、分析和定位当前系统的瓶颈
对于JVM的核心指标,我们的关注点和常用工具如下:
1)CPU指标
查看占用CPU最多的进程
查看占用CPU最多的线程
查看线程堆栈快照信息
分析代码执行热点
查看哪个代码占用CPU执行时间最长
查看每个方法占用CPU时间比例
常见的命令:
-
/
/ 显示系统各个进程的资源使用情况
-
top
-
/
/ 查看某个进程中的线程占用情况
-
top -Hp pid
-
/
/ 查看当前 Java 进程的线程堆栈信息
-
jstack pid
常见的工具:JProfiler、JVM Profiler、Arthas等。
2)JVM 内存指标
查看当前 JVM 堆内存参数配置是否合理
查看堆中对象的统计信息
查看堆存储快照,分析内存的占用情况
查看堆各区域的内存增长是否正常
查看是哪个区域导致的GC
查看GC后能否正常回收到内存
常见的命令:
// 显示系统各个进程的资源使用情况
top
// 查看某个进程中的线程占用情况
top -Hp pid
// 查看当前 Java 进程的线程堆栈信息
jstack pid
常见的工具:Eclipse MAT、JConsole等。
3)JVM GC指标
查看每分钟GC时间是否正常
查看每分钟YGC次数是否正常
查看FGC次数是否正常
查看单次FGC时间是否正常
查看单次GC各阶段详细耗时,找到耗时严重的阶段
查看对象的动态晋升年龄是否正常
JVM 的 GC指标一般是从 GC 日志里面查看,默认的 GC 日志可能比较少,我们可以添加以下参数,来丰富我们的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
以上就是我们定位系统瓶颈的常用手段,大部分问题通过以上方式都能定位出问题原因,然后结合代码去找到问题根源。
4.2、确定优化目标
定位出系统瓶颈后,在优化前先制定好优化的目标是什么,例如:
将FGC次数从每小时1次,降低到1天1次
将每分钟的GC耗时从3s降低到500ms
将每次FGC耗时从5s降低到1s以内
...
4.3、制订优化方案
针对定位出的系统瓶颈制定相应的优化方案,常见的有:
代码bug:升级修复bug。典型的有:死循环、使用无界队列。
不合理的JVM参数配置:优化 JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小。
4.4、对比优化前后的指标,统计优化效果
4.5、持续观察和跟踪优化效果
4.6、如果还需要的话,重复以上步骤
以下案例来源于网络或本人真实经验,皆能自圆其说,理解掌握后同学们皆可拿来与面试官对线。
服务环境:ParNew + CMS + JDK8
问题现象:服务频繁出现FGC
原因分析:
1)首先查看GC日志,发现出现FGC的原因是metaspace空间不够
对应GC日志:
Full GC (Metadata GC Threshold)
2)进一步查看日志发现元空间存在内存碎片化现象
对应GC日志:
Metaspace used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
这边简单解释下这几个参数的意义
used :已使用的空间大小
capacity:当前已经分配且未释放的空间容量大小
committed:当前已经分配的空间大小
reserved:预留的空间大小
这边 used 比较容易理解,reserved 在本例不重要可以先忽略,主要是 capacity 和 committed 这2个容易搞混。
结合下图来看更容易理解,元空间的分配以 chunk 为单位,当一个 ClassLoader 被垃圾回收时,所有属于它的空间(chunk)被释放,此时该 chunk 称为 Free Chunk,而 committed chunk 就是 capacity chunk 和 free chunk 之和。
之所以说内存存在碎片化现象就是根据 used 和 capacity 的数据得来的,上面说了元空间的分配以 chunk 为单位,即使一个 ClassLoader 只加载1个类,也会独占整个 chunk,所以当出现 used 和 capacity 两者之差较大的时候,说明此时存在内存碎片化的情况。
GC日志demo如下:
-
{Heap before GC invocations=
0 (full
0):
-
par
new generation total
314560K, used
141123K [
0x00000000c0000000,
0x00000000d5550000,
0x00000000d5550000)
-
eden space
279616K,
50% used [
0x00000000c0000000,
0x00000000c89d0d00,
0x00000000d1110000)
-
from space
34944K,
0% used [
0x00000000d1110000,
0x00000000d1110000,
0x00000000d3330000)
-
to space
34944K,
0% used [
0x00000000d3330000,
0x00000000d3330000,
0x00000000d5550000)
-
concurrent mark-sweep generation total
699072K, used
0K [
0x00000000d5550000,
0x0000000100000000,
0x0000000100000000)
-
Metaspace used
35337K, capacity
56242K, committed
56320K, reserved
1099776K
-
class space used
4734K, capacity
8172K, committed
8172K, reserved
1048576K
-
1.448: [Full
GC (Metadata GC Threshold)
1.448: [CMS:
0K->
10221K(
699072K),
0.0487207 secs]
141123K->
10221K(
1013632K), [Metaspace:
35337K->
35337K(
1099776K)],
0.0488547 secs] [Times: user=
0.09 sys=
0.00, real=
0.05 secs]
-
Heap after GC invocations=
1 (full
1):
-
par
new generation total
314560K, used
0K [
0x00000000c0000000,
0x00000000d5550000,
0x00000000d5550000)
-
eden space
279616K,
0% used [
0x00000000c0000000,
0x00000000c0000000,
0x00000000d1110000)
-
from space
34944K,
0% used [
0x00000000d1110000,
0x00000000d1110000,
0x00000000d3330000)
-
to space
34944K,
0% used [
0x00000000d3330000,
0x00000000d3330000,
0x00000000d5550000)
-
concurrent mark-sweep generation total
699072K, used
10221K [
0x00000000d5550000,
0x0000000100000000,
0x0000000100000000)
-
Metaspace used
35337K, capacity
56242K, committed
56320K, reserved
1099776K
-
class space used
4734K, capacity
8172K, committed
8172K, reserved
1048576K
-
}
-
{Heap before GC invocations=
1 (full
1):
-
par
new generation total
314560K, used
0K [
0x00000000c0000000,
0x00000000d5550000,
0x00000000d5550000)
-
eden space
279616K,
0% used [
0x00000000c0000000,
0x00000000c0000000,
0x00000000d1110000)
-
from space
34944K,
0% used [
0x00000000d1110000,
0x00000000d1110000,
0x00000000d3330000)
-
to space
34944K,
0% used [
0x00000000d3330000,
0x00000000d3330000,
0x00000000d5550000)
-
concurrent mark-sweep generation total
699072K, used
10221K [
0x00000000d5550000,
0x0000000100000000,
0x0000000100000000)
-
Metaspace used
35337K, capacity
56242K, committed
56320K, reserved
1099776K
-
class space used
4734K, capacity
8172K, committed
8172K, reserved
1048576K
-
1.497: [Full
GC (Last ditch collection)
1.497: [CMS:
10221K->
3565K(
699072K),
0.0139783 secs]
10221K->
3565K(
1013632K), [Metaspace:
35337K->
35337K(
1099776K)],
0.0193983 secs] [Times: user=
0.03 sys=
0.00, real=
0.02 secs]
-
Heap after GC invocations=
2 (full
2):
-
par
new generation total
314560K, used
0K [
0x00000000c0000000,
0x00000000d5550000,
0x00000000d5550000)
-
eden space
279616K,
0% used [
0x00000000c0000000,
0x00000000c0000000,
0x00000000d1110000)
-
from space
34944K,
0% used [
0x00000000d1110000,
0x00000000d1110000,
0x00000000d3330000)
-
to space
34944K,
0% used [
0x00000000d3330000,
0x00000000d3330000,
0x00000000d5550000)
-
concurrent mark-sweep generation total
699072K, used
3565K [
0x00000000d5550000,
0x0000000100000000,
0x0000000100000000)
-
Metaspace used
17065K, capacity
22618K, committed
35840K, reserved
1079296K
-
class space used
1624K, capacity
2552K, committed
8172K, reserved
1048576K
-
}
元空间主要适用于存放类的相关信息,而存在内存碎片化说明很可能创建了较多的类加载器,同时使用率较低。
因此,当元空间出现内存碎片化时,我们会着重关注是不是创建了大量的类加载器。
3)通过 dump 堆存储文件发现存在大量 DelegatingClassLoader
通过进一步分析,发现是由于反射导致创建大量 DelegatingClassLoader。其核心原理如下:
在 JVM 上,最初是通过 JNI 调用来实现方法的反射调用,当 JVM 注意到通过反射经常访问某个方法时,它将生成字节码来执行相同的操作,称为膨胀(inflation)机制。如果使用字节码的方式,则会为该方法生成一个 DelegatingClassLoader,如果存在大量方法经常反射调用,则会导致创建大量 DelegatingClassLoader。
反射调用频次达到多少才会从 JNI 转字节码?
默认是15次,可通过参数 -Dsun.reflect.inflationThreshold 进行控制,在小于该次数时会使用 JNI 的方式对方法进行调用,如果调用次数超过该次数就会使用字节码的方式生成方法调用。
分析结论:反射调用导致创建大量 DelegatingClassLoader,占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发 FGC。
优化策略:
1)适当调大 metaspace 的空间大小。
2)优化不合理的反射调用。例如最常见的属性拷贝工具类 BeanUtils.copyProperties 可以使用 mapstruct 替换。
当被面试官问到 JVM 调优时,完全可以按照本文的脉络回答:
首先表态如果使用合理的 JVM 参数配置,在大多数情况应该是不需要调优的——对应本文第1题
其次说明可能还是存在少量场景需要调优,我们可以对一些 JVM 核心指标配置监控告警,当出现波动时人为介入分析评估——对应本文第3题
最后举一个实际的调优例子来加以说明——对应本文第5题
如果面试官反问怎么分析排查的,则可以使用本文第4题的常用命令和工具来与之对线。
这一套流程下来,我相信大部分面试官都会对你印象不错。
我是囧辉,一个坚持分享原创技术干货的程序员,如果觉得本文对你有帮助,记得点赞关注,我们下期再见。
Java 基础高频面试题(2021年最新版)
Java 集合框架高频面试题(2021年最新版)
面试必问的 Spring,你懂了吗?
面试必问的 MySQL,你懂了吗?