1、JVM为什么需要调优?
如图所示:
问题1:如果eden区比较小,会导致什么问题?
- YoungGC会变多,频繁YGC让内存有空间,会导致stop to world
问题2:如果eden区比较大,会导致什么问题?
- 11
问题3:survivor区变小的影响点?
- survivor区变小会导致大对象,根据动态年龄设置,会直接晋升到老年代
问题4:survivor区变大的影响点?
- 会浪费一些内存
问题4:的影响点?
面试时如何回答
- 首先表态如果使用合理的JVM参数设置,在大多数情况下应该是不需要调优的;
- JVM 参数的默认(推荐)值都是经过 JVM 团队的反复测试和前人的充分验证得出的比较合理的值,因此通常来说是比较靠谱和通用的,一般不会出大问题;
- 大部分情况下:是自己代码的bug导致了OOM、CPU load高、GC频繁等,这些场景基本修复bug即可,不需要动JVM;
- 其次说明可能还是存在少量场景需要调优,我们可以对一些JVM核心指标(GC次数,内存容量)配置监控告警,当出现波动时,人为介入分析评估;
- 针对商品中心自身的业务场景,对JVm参数进行优化调整,使其更适合我们的业务;
- 使用更好性能的垃圾收集器
- 在JDK8中使用了G1,存在的问题?
- 使用ZGC,公开数据是最大暂停时间不超过10ms,甚至是1ms
- ZGC存在的问题:
- 1、吞吐量较于 G1 会有所下降,官方称最大不超过 15%
- 2、ZGC如果遇到非常高的对象分配速率,会跟不上,目前唯一有效的“调优”方式是增大整个GC堆的大小
- JVM 有哪些核心指标?合理范围应该是多少?
- 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以内尤佳
- 通常来说,只要这几个指标正常,其他的一般不会有问题
- 最后举一个实际的调优例子来加以说明。见第三章
2、JVM 优化步骤?
2.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
以上就是我们定位系统瓶颈的常用手段,大部分问题通过以上方式都能定位出问题原因,然后结合代码去找到问题根源。
2.2、确定优化目标
定位出系统瓶颈后,在优化前先制定好优化的目标是什么,例如:
- 将FGC次数从每小时1次,降低到1天1次
- 将每分钟的GC耗时从3s降低到500ms
- 将每次FGC耗时从5s降低到1s以内
2.3、制订优化方案
针对定位出的系统瓶颈制定相应的优化方案,常见的有:
- 代码bug:升级修复bug。
- 典型的有:死循环、使用无界队列。
- 不合理的JVM参数配置:优化 JVM 参数配置。
- 典型的有:年轻代内存配置过小、堆内存配置过小、元空间配置过小。
2.4、对比优化前后的指标,统计优化效果
2.5、持续观察和跟踪优化效果
2.6、如果还需要的话,重复以上步骤
3、调优案例1:metaspace导致频繁FGC问题
服务环境:
- ParNew(年轻代) + CMS(老年代) + JDK8
问题现象:
- 服务频繁出现FGC
原因分析:
- 1、首先查看GC日志,发现出现FGC的原因是metaspacd空间不够
- 对应GC日志:
Full GC (Metadata GC Threshold)
- 进一步查看日志发现元空间存在内存碎片化现象
- 对应GC日志:
Metaspace used 35337K, capacity 56242K, committed 56320K, reserved 1099776K
- 这边简单解释下这几个参数的意义
- used :已使用的空间大小
- capacity:当前已经分配且未释放的空间容量大小
- committed:当前已经分配的空间大小
- reserved:预留的空间大小
- 元空间的分配以 chunk 为单位,当一个 ClassLoader 被垃圾回收时,所有属于它的空间(chunk)被释放,此时该 chunk 称为 Free Chunk,而 committed chunk 就是 capacity chunk 和 free 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(自定义加载器),占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发FGC。
- 其核心原理如下:
- 在 JVM 上,最初是通过 JNI 调用来实现方法的反射调用—》当 JVM 注意到通过反射经常访问某个方法时,它将生成字节码来执行相同的操作,称为膨胀(inflation)机制。如果使用字节码的方式,则会为该方法生成一个 DelegatingClassLoader,如果存在大量方法经常反射调用,则会导致创建大量 DelegatingClassLoader
- 反射调用频次达到多少才会从 JNI 转字节码?
- 默认是15次,可通过参数 -Dsun.reflect.inflationThreshold 进行控制,在小于该次数时会使用 JNI 的方式对方法进行调用,如果调用次数超过该次数就会使用字节码的方式生成方法调用。
- 分析结论:
- 反射调用导致创建大量 DelegatingClassLoader,占用了较大的元空间内存,同时存在内存碎片化现象,导致元空间利用率不高,从而较快达到阈值,触发 FGC。
优化策略
- 1、适当调大 metaspace 的空间大小
- 2、优化不合理的反射调用。例如最常用的属性拷贝工具类 BeanUtils.copyProperties 可以使用 mapStruct替换
优化效果:
- 频繁FGC问题得到解决
4、调优案例2:CMS内存碎片化导致频繁FGC问题
问题现象:
- C端核心业务在高峰期服务器发生FGC,导致部分请求超时报错,影响用户体验
原因分析
- CMS使用标记清除算法,不再进行任何压缩和整理的工作,意味着老年代随着应用的运行会变得碎片化;碎片过多会影响大对象的分配,虽然老年代还有很大的剩余空间,但是没有连续的空间来分配大对象。长期如此:最终可能会导致FGC发生
优化策略
- 业务低峰期显示触发FGC,优化内存碎片并压缩堆,降低在业务高峰期发生FGC的概率
- System.gc(),没有开启 -XX:+DisableExplicitGC
- 每天凌晨3、4点左右,美团在这么干
- jmpa -histo:live pid
优化效果:
- 业务高峰期基本没有出现FGC
5、调优案例3:YGC和OLD GC频繁
问题现象:
- 服务器YGC和FGC频繁导致 TP999耗时较高,YGC每分钟50次,每次25毫秒,FGC几分钟一次,每次200毫秒
原因分析:
- 该服务要求低延迟,为了YGC较快完成,年轻代设置较小,但是由于年轻代较小,反而导致YGC次数较多,查看GC日志发现,由于动态年龄的因素,大量对象较早晋升到老年代,从而导致FGC频繁
优化策略
- 扩大新生代内存为原来的3倍
优化效果:
- 单词YGC耗时增加了5%,频率降低了60%,服务TP99降低了 10ms+,FGC频率降低为几小时一次
6、JVM 调优的参数可以在哪儿设置参数值
可以在IDEA,Eclipse,工具里设置
如果上线了是WAR包的话可以在Tomcat设置
如果是Jar包直接 :java -jar 直接插入JVM命令就好了
- 补充图片
7、说一下 JVM 调优的工具?
JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具
- jconsole:用于对 JVM 中的内存、线程和类等进行监控
- jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等
Grafane:
- todo 补充数据
8、常用的 JVM 调优的参数都有哪些?
#常用的设置
年轻代
- -Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
- -Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。
- -Xmn:设置堆中年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。
- -XX:NewSize=n 设置年轻代初始化大小大小
- -XX:MaxNewSize=n 设置年轻代最大值
- -XX:NewRatio=n 设置年轻代和年老代的比值。如: -XX:NewRatio=3,表示年轻代与年老代比值为 1:3,年轻代占整个年轻代+年老代和的 1/4
- -XX:SurvivorRatio=n 年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。8表示两个Survivor :eden=2:8 ,即一个Survivor占年轻代的1/10,默认就为8
- -Xss:设置每个线程的堆栈大小。JDK5后每个线程 Java 栈大小为 1M,以前每个线程堆栈大小为 256K。
- -XX:ThreadStackSize=n 线程堆栈大小
- -XX:PermSize=n 设置持久代初始值
- -XX:MaxPermSize=n 设置持久代大小
- -XX:MaxTenuringThreshold=n 设置年轻带垃圾对象最大年龄。如果设置为 0 的话,则年轻代对象不经过 Survivor 区,直接进入年老代。
#下面是一些不常用的
- -XX:LargePageSizeInBytes=n 设置堆内存的内存页大小
- -XX:+UseFastAccessorMethods 优化原始类型的getter方法性能
- -XX:+DisableExplicitGC 禁止在运行期显式地调用System.gc(),默认启用
- -XX:+AggressiveOpts 是否启用JVM开发团队最新的调优成果。例如编译优化,偏向锁,并行年老代收集等,jdk6之后默认启动
- -XX:+UseBiasedLocking 是否启用偏向锁,JDK6默认启用
- -Xnoclassgc 是否禁用垃圾回收
- -XX:+UseThreadPriorities 使用本地线程的优先级,默认启用
9、JVM的GC收集器设置
-xx:+Use xxx GC
xxx 代表垃圾收集器名称
- -XX:+UseSerialGC:设置串行收集器,年轻代收集器
- -XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。
- -XX:+UseParallelGC:设置并行收集器,目标是目标是达到可控制的吞吐量;
- -XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。
- -XX:+UseConcMarkSweepGC:设置年老代并发收集器;
- -XX:+UseG1GC:设置 G1 收集器,JDK1.9默认垃圾收集器;
10、标准中心虚拟机参数
原来是这样的:-Xmx6g -Xms6g -Xmn3g -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+ExplicitGCInvokesConcurrent -XX:+CMSScavengeBeforeRemark -XX:+PrintCommandLineFlags -XX:ErrorFile=/opt/zcy/modules/item-standard-center/hs_err_%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/zcy/modules/item-standard-center/ -Xloggc:/opt/zcy/modules/item-standard-center/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCCause -XX:+PrintPromotionFailure
现在是这样的:-Xmx8g -Xms8g -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC -XX:G1NewSizePercent=45 -XX:MaxGCPauseMillis=200 -XX:G1MaxNewSizePercent=65 -XX:G1ReservePercent=5 -XX:InitiatingHeapOccupancyPercent=30 -XX:G1HeapRegionSize=16M -XX:+PrintAdaptiveSizePolicy -XX:+PrintCommandLineFlags -XX:ErrorFile=/opt/zcy/modules/log/hs_err_%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/zcy/modules/log/ -Xloggc:/opt/zcy/modules/log/gc.log -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCCause -XX:+PrintPromotionFailure -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC
11、什么是堆内存? 参数如何设置? 美团
堆内存是指由程序代码自由分配的内存,与栈内存作区分。
- 在 Java 中,堆内存主要用于分配对象的存储空间,只要拿到对象引用,所有线程都可以访问堆内存
- -Xmx, 指定最大堆内存。如 -Xmx4g. 这只是限制了 Heap 部分的最大值为 4g。这个内存不包括栈内存,也不包括堆外使用的内存。
- -Xms, 指定堆内存空间的初始大小。如 -Xms4g。而且指定的内存大小,并不是操作系统实际分配的初始值,而是 GC 先规划好,用到才分配。专用服务器上需要保持 – Xms和 – Xmx 一致,否则应用刚启动可能就有好几个 FullGC。当两者配置不一致时,堆内存扩容可能会导致性能抖动
- -Xmn, 设置堆中年轻代大小,等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该设置该选项,在其他的某些业务场景下可以设置。官方建议设置为 -Xmx 的 1/2 ~ 1/4
- -XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。 Java8 默认允许的 Meta 空间无限大, 此参数无效。
- -XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间, 一般不允许设置该选项。
- -XX:MaxDirectMemorySize=size, 系统可以使用的最大堆外内存, 这个参数跟 -Dsun.nio.MaxDirectMemorySize 效果相同。
- -Xss, 设置每个线程栈的字节数。 例如 -Xss1m 指定线程栈为 1MB, 与-XX:ThreadStackSize=1m 等价
12、调优案例
- OLD GC 耗时较长影响业务
- 原因:Remark阶段时间较长
- 优化:-XX:+CMSScavengeBeforeRemark
- YGC耗时增加
- 原因:jackson进行反序列化时将key进行String#intern(放在了常量池),导致扫描时,GCRoot变大
- 解决:禁用jackson的String#intern
- YGC次数增加
- 原因:-XX: MaxGCPauseMilllis 参数时间设置过小,导致JVM降低年轻代region
- 解决:1)调大 -XX:MaxGCPauseMillis值;2)将年轻代region大小设置为固定值。
仔细看GC日志,比对GC前和后,内存的变化
Action1:京东618,JVM方法区调优经历
问题现象:
- 618备战期间,观察JVM监控发现一个问题,
- 正常的曲线如下图所示(grafana)
- 我们有一个服务的曲线如下图所示
- 方法区(NonHeap)不断升高,导致频繁发生FGC,
- 思考:如果是你,你的思路是怎样的呢?
- 首先:判断是否方法区被填满了,其保存的是类信息、常量池、静态变量,类在不停的加载到方法区,导致了频繁发生FGC。然后我们需要dump方法区里面当前保存的是啥,得到相应的类信息。
- 是否是netty的堆外内存出现了溢出
原因分析
- 现在问题是:是哪段代码在生成类,生成的又是什么类?如果是堆空间,我们可以把堆空间dump下来,然后通过一些工具进行分析;那么方法区我们怎么dump呢?通过查询文档找到了JVM启动参数
verbose:class
,能够打印出所有的类加载信息。正常的类加载信息应该是这样的
- 现在的类加载信息如下:
- 会有很多的这个类,JVM_DefineClass,拿到这个类去idea里面查找代码,发现下面这行代码。使用的是表达式解析引擎 mvel,生成增强类加载到方法区,类似Spring中的spel,原来是每次请求我们都会编译生成类,从而导致方法区的不断膨胀,并没有做缓存
优化策略
- 接入缓存,经过缓存改造后,问题得到了解决
优化效果:
- 堆外问题其实很少遇到,如果你遇到了,可以使用
verbose:class
, 排查是不是由不断的类生成,导致了方法区膨胀,从而导致FGC。