面试官:如何进行 JVM 调优(附真实案例)
前言
面试官:在工作中做过 JVM 调优吗?讲讲做过哪些 JVM 调优?
我一个QPS不到10的项目,上次问我缓存穿透缓存雪崩,这次问我 JVM 调优,我是真滴难。
不过大家别慌,热心的我给大家找来了几个满分回答,大家选择合适的使用。
回答1:听好了,下面将是我第一次 JVM 调优。
回答2:我一般面试的时候才调优。
回答3:我一般直接加机器、加内存。
回答4:老子直接用的 ZGC,调个蛇皮。
正文
1、JVM 究竟需不需要调优?
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 源码的改动。
2、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)面试角度
如果你回答直接升级垃圾收集器,面试官可能也赞同,但是这个话题可能就这样结束了,面试官大概率没听到他想要的回答,你在这题的肯定拿不到加分,甚至可能会被扣分。
所以,在面试的时候,你可以回答升级垃圾收集器,但是你不能只回答升级垃圾收集器。
3、JVM 何时优化?
忌过早优化。《计算机程序设计艺术》的作者高德纳(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、JVM 优化步骤?
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后能否正常回收到内存
常见的命令:
// 查看当前的 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等。
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、如果还需要的话,重复以上步骤
5、调优案例:metaspace导致频繁FGC问题
以下案例来源于网络或本人真实经验,皆能自圆其说,理解掌握后同学们皆可拿来与面试官对线。
服务环境: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 1099776Kclass 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 1099776Kclass 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 1099776Kclass 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 1079296Kclass 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,你懂了吗?
面试官:如何进行 JVM 调优(附真实案例)相关推荐
- 面试又栽在JVM调优上了!
很多人都是抱着旁观者的心态看关于jvm的文章,给个镜子看看你们的嘴脸- 那我今天要来和你们过过招了,前几天我在知乎上看到一个叫做为什么要学习jvm的话题下回答虽然寥寥无几,却成了大型打脸现场... 想 ...
- 美团面试:熟悉哪些JVM调优参数,幸好我准备过!
关注公众号"Java后端技术全栈" 回复"000"获取程序员必备电子书 大家好,我是田维常,江湖人称老田.田哥.田神,今天来和大家分享JVM调优参数. 之前,我 ...
- java面试 系统调优_面试官:Java性能调优你会多少?一个问题就把我问的哑口无言,哭了!...
一.前言 什么是性能调优? 性能调优其实很好理解,就是优化硬件.操作系统.应用之间的一个充分的协作,最大化的发挥出硬件的极致性能,来应对高负载的业务需求. 为什么需要性能优化? 其实说到底就是两个原因 ...
- 后端技术:Spring Boot 项目优化和 JVM 调优,真实有效。
项目调优 作为一名工程师,项目调优这事,是必须得熟练掌握的事情. 在SpringBoot项目中,调优主要通过配置文件和配置JVM的参数的方式进行. 一.修改配置文件 关于修改配置文件applicati ...
- JVM调优之实战案例(六)(转载)
特此声明:文章为转载,原文链接:http://www.cnblogs.com/redcreen/archive/2011/05/05/2038331.html java application项目(非 ...
- JVM 调优实战--一个案例理解常用工具(命令)
测试代码 import java.math.BigDecimal; import java.util.ArrayList; import java.util.Date; import java.uti ...
- 如何应对面试官的JVM调优问题
为什么要进行JVM调优 我猜你肯定是为了面试,现在很多公司都会问这个,虽然你工作了N年JVM调优可能都不会接触到,但我觉得还是有考察的必要的.因为很多时候我们考察一个人不光要考察他的硬实力,还要看他有 ...
- java面试jvm调优的意义
压力测试的理解,xxx的性能10w/s,对你有意义么? 没有那家卖瓜的会说自己家的不甜,同样,没有哪个开源项目愿意告诉你在对它条件最苛刻的时候压力情况是多少,一般官网号称给你看的性能指标都是在最理想环 ...
- java 面试 jvm 调优的意义 _java 培训
1. 压力测试的理解,xxx 的性能 10w/s,对你有意义么? 没有那家卖瓜的会说自己家的不甜,同样,没有哪个开源项目愿意告诉你在对它条件最苛刻的时候压力情况是多少,一般官网号称给你看的性能指标都是 ...
- SpringBoot项目优化和Jvm调优(亲测,真实有效)
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:星朝 原文:cnblogs.com/jpfss/p/9753 ...
最新文章
- tensorflow GPU python cuda cudnn 匹配
- 串结构练习——字符串匹配 解题报告
- 通过几个问题深入分析Vue中的diff原理
- android应用 百度地图,Android学习之百度地图API的应用(一)
- 13张PPT带你了解主动式消息队列处理集群
- MATLAB库函数polly2trellis(卷积码生成多项式转网格图描述)的实现过程详解
- 守护线程 java 1615478655
- C语言中动态内存分配的本质是什么?
- Linux 内核版本命名
- 用Linux编写简单的atm取款机系统,详细解析C++编写的ATM自动取款机模拟程序
- linux下定时执行sh
- jQuery版本升级踩坑大全
- 婚礼邀请函微信小程序
- 人工神经网络优化算法,进化算法优化神经网络
- 基于asp.net725原创(古代)文学交流网站系统
- Fantastic-Matplotlib 第二回
- CIO40:家电巨头数字化运营历程(IT-DT-OT)
- (附源码)spring boot网上购物系统 毕业设计 311236
- 仿淘票票 —— 微信小程序
- VSCODE(三)用户界面
热门文章
- java getstring_Java String 类 | 菜鸟教程
- 史上最全Java基础视频教程
- JAVA 基础(0)教学视频的选择和笔记本的选择
- 解决Teamviewer屏保锁屏、黑屏无法进入问题
- GlobalMapper20使用控制点对地形数据(高程数据)进行高程纠正(高程拟合/纠偏/配准)
- sockaddr_in结构体、socklen_t类型、bzero函数简要记录
- NEFUCTF校赛-题解
- QQ在线客服代码 网页qq咨询html代码
- Windows 10 Enterprise 2015 LTSB 2019_免费下载:Intouch软件、Windows操作系统、SQL数据库,Office办公、VB6.0、C#、虚拟机、PLC...
- 2020版Java视频教程|java零基础到就业全套视频教程线上免费观看,java免费教程直接看