面试官:如何进行 JVM 调优(附真实案例)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 当被面试官问到JVM 调优时,完全可以按照本文的脉络回答

前言


面试官:在工作中做过JVM调优吗?讲讲做过哪些JVM 调优?

我一个QPS不到10的项目,上次问我缓存穿透缓存雪崩,这次问我 JVM 调优,我是真滴难。

image.png

不过大家别慌,热心的我给大家找来了几个满分回答,大家选择合适的使用。

回答1:听好了,下面将是我第一次 JVM 调优。

回答2:我一般面试的时候才调优。

回答3:我一般直接加机器、加内存。

回答4:老子直接用的ZGC,调个蛇皮。


正文


1JV


M究竟需不需要调优?


JVM 经过这么多年的发展和验证,整体是非常健壮的。个人认为99%的情况下,基本用不到 JVM 调优。

通常来说,我们的 JVM 参数配置大多还是会遵循 JVM 官方的建议,例如:

·       -XX:NewRatio=2,年轻代:老年代=1:2

·       -XX:SurvivorRatio=8eden:survivor=8:1

·       堆内存设置为物理内存的3/4左右

·       等等

JVM 参数的默认(推荐)值都是经过 JVM 团队的反复测试和前人的充分验证得出的比较合理的值,因此通常来说是比较靠谱和通用的,一般不会出大问题。

当然,更重要的是,大部分的应用 QPS 都不到10,数据量不到几万,这种低压环境下,想让 JVM 出问题,说实话也挺难的。

image.png

大部分同学更常遇到的应该是自己的代码 bug 导致 OOMCPU load高、GC频繁啥的,这些场景也基本都是代码修复即可,通常不需要动 JVM

当然,俗话说得好,凡事无绝对,还是有一小部分场景,是可能需要用到 JVM 调优的。具体哪些场景,我们在下面介绍。

值得一提的是,我们这边所说的 JVM 调优更多的是针对自己的业务场景对 JVM 参数进行优化调整,使其更适合我们的业务,而不是指对 JVM 源码的改动。


2JVM调优没有什么必要,使用性能更好的垃圾回收器就能解决问题了?


这是我在网上看到的一个说法,因为赞同的人比较多,我估计有不少同学也会有这个想法,因此在这边谈下自己的看法。


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)面试角度

如果你回答直接升级垃圾收集器,面试官可能也赞同,但是这个话题可能就这样结束了,面试官大概率没听到他想要的回答,你在这题的肯定拿不到加分,甚至可能会被扣分。

所以,在面试的时候,你可以回答升级垃圾收集器,但是你不能只回答升级垃圾收集器。


3JVM何时优化?


忌过早优化。《计算机程序设计艺术》的作者高德纳(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.countFGC最多几小时1次,1天不到1次尤佳

·       jvm.fullgc.time:每次FGC耗时在1s以内,500ms以内尤佳

通常来说,只要这几个指标正常,其他的一般不会有问题,如果其他地方出了问题,一般都会影响到这几个指标。


4JVM优化步骤?


4.1、分析和定位当前系统的瓶颈

对于JVM的核心指标,我们的关注点和常用工具如下:


1CPU指标

·       查看占用CPU最多的进程

·       查看占用CPU最多的线程

·       查看线程堆栈快照信息

·       分析代码执行热点

·       查看哪个代码占用CPU执行时间最长

·       查看每个方法占用CPU时间比例

常见的命令:

// 显示系统各个进程的资源使用情况
top
// 查看某个进程中的线程占用情况
top -Hp pid
// 查看当前 Java 进程的线程堆栈信息
jstack pid

常见的工具:JProfilerJVM ProfilerArthas等。


2JVM 内存指标

·       查看当前 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 MATJConsole等。


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次,降低到11

·       将每分钟的GC耗时从3s降低到500ms

·       将每次FGC耗时从5s降低到1s以内

·       ...

4.3、制订优化方案

针对定位出的系统瓶颈制定相应的优化方案,常见的有:

·       代码bug:升级修复bug。典型的有:死循环、使用无界队列。

·       不合理的JVM参数配置:优化JVM 参数配置。典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小。

4.4、对比优化前后的指标,统计优化效果

4.5、持续观察和跟踪优化效果

4.6、如果还需要的话,重复以上步骤


5、调优案例:metaspace导致频繁FGC问题


以下案例来源于网络或本人真实经验,皆能自圆其说,理解掌握后同学们皆可拿来与面试官对线。

服务环境:ParNew + CMS + JDK8

问题现象:服务频繁出现FGC

原因分析

1)首先查看GC日志,发现出现FGC的原因是metaspace空间不够

对应GC日志:

Full GC (Metadata GC Threshold)

2)进一步查看日志发现元空间存在内存碎片化现象

对应GC日志:

Metaspace     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 之和。

image.png

之所以说内存存在碎片化现象就是根据 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,你懂了吗?

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
19天前
|
Arthas 监控 Java
(十一)JVM成神路之性能调优篇:GC调优、Arthas工具详解及各场景下线上最佳配置推荐
“在当前的互联网开发模式下,系统访问量日涨、并发暴增、线上瓶颈等各种性能问题纷涌而至,性能优化成为了现时代开发过程中炙手可热的名词,无论是在开发、面试过程中,性能优化都是一个常谈常新的话题”。
|
22天前
|
监控 Java 测试技术
JVM 性能调优 及 为什么要减少 Full GC
JVM 性能调优 及 为什么要减少 Full GC
47 4
|
3天前
|
缓存 算法 Java
这些年背过的面试题——JVM篇
本文是技术人面试系列JVM篇,面试中关于JVM都需要了解哪些基础?一文带你详细了解,欢迎收藏!
|
19天前
|
运维 Java Linux
(九)JVM成神路之性能调优、GC调试、各内存区、Linux参数大全及实用小技巧
本章节主要用于补齐之前GC篇章以及JVM运行时数据区的一些JVM参数,更多的作用也可以看作是JVM的参数列表大全。对于开发者而言,能够控制JVM的部分也就只有启动参数了,同时,对于JVM的性能调优而言,JVM的参数也是基础。
|
1月前
|
缓存 监控 Java
Java虚拟机(JVM)性能调优实战指南
在追求软件开发卓越的征途中,Java虚拟机(JVM)性能调优是一个不可或缺的环节。本文将通过具体的数据和案例,深入探讨JVM性能调优的理论基础与实践技巧,旨在为广大Java开发者提供一套系统化的性能优化方案。文章首先剖析了JVM内存管理机制的工作原理,然后通过对比分析不同垃圾收集器的适用场景及性能表现,为读者揭示了选择合适垃圾回收策略的数据支持。接下来,结合线程管理和JIT编译优化等高级话题,文章详细阐述了如何利用现代JVM提供的丰富工具进行问题诊断和性能监控。最后,通过实际案例分析,展示了性能调优过程中可能遇到的挑战及应对策略,确保读者能够将理论运用于实践,有效提升Java应用的性能。 【
124 10
|
1月前
|
监控 算法 Java
深入理解Java虚拟机:JVM调优的实用策略
在Java应用开发中,性能优化常常成为提升系统响应速度和处理能力的关键。本文将探讨Java虚拟机(JVM)调优的核心概念,包括垃圾回收、内存管理和编译器优化等方面,并提供一系列经过验证的调优技巧。通过这些实践指导,开发人员可以有效减少延迟,提高吞吐量,确保应用稳定运行。 【7月更文挑战第16天】
|
29天前
|
JSON Java BI
一次Java性能调优实践【代码+JVM 性能提升70%】
这是我第一次对系统进行调优,涉及代码和JVM层面的调优。如果你能看到最后的话,或许会对你日常的开发有帮助,可以避免像我一样,犯一些低级别的错误。本次调优的代码是埋点系统中的报表分析功能,小公司,开发结束后,没有Code Review环节,所以下面某些问题,也许在Code Review环节就可以避免。
115 0
一次Java性能调优实践【代码+JVM 性能提升70%】
|
1月前
|
存储 Java 程序员
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
Java面试题:方法区在JVM中存储什么内容?它与堆内存有何不同?
48 10
|
1月前
|
监控 算法 Java
深入探索Java虚拟机:性能监控与调优实践
在面对日益复杂的企业级应用时,Java虚拟机(JVM)的性能监控和调优显得尤为重要。本文将深入探讨JVM的内部机制,分析常见的性能瓶颈,并提供一系列针对性的调优策略。通过实际案例分析,我们将展示如何运用现代工具对JVM进行监控、诊断及优化,以提升Java应用的性能和稳定性。
|
1月前
|
存储 运维 Java
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
38 9