JVM第四讲:JVM性能调优

JVM第四讲:性能调优

本文是JVM第四讲:JVM性能调优

文章目录

  • JVM第四讲:性能调优
      • 1、JVM为什么需要调优?
      • 2、JVM 优化步骤?
        • 2.1、分析和定位当前系统的瓶颈
        • 2.2、确定优化目标
        • 2.3、制订优化方案
        • 2.4、对比优化前后的指标,统计优化效果
        • 2.5、持续观察和跟踪优化效果
        • 2.6、如果还需要的话,重复以上步骤
      • 3、调优案例1:metaspace导致频繁FGC问题
        • 服务环境:
        • 问题现象:
        • 原因分析:
        • **优化策略**
        • 优化效果:
      • 4、调优案例2:CMS内存碎片化导致频繁FGC问题
        • 问题现象:
        • 原因分析
        • 优化策略
        • 优化效果:
      • 5、调优案例3:YGC和OLD GC频繁
        • 问题现象:
        • 原因分析:
        • 优化策略
        • 优化效果:
      • 6、JVM 调优的参数可以在哪儿设置参数值
      • 7、说一下 JVM 调优的工具?
      • 8、常用的 JVM 调优的参数都有哪些?
      • 9、JVM的GC收集器设置
      • 10、标准中心虚拟机参数
      • 11、什么是堆内存? 参数如何设置? 美团
      • 12、调优案例

1、JVM为什么需要调优?

如图所示:
JVM第四讲:JVM性能调优_第1张图片
问题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 之和 JVM第四讲:JVM性能调优_第2张图片
        • 当出现 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第四讲:JVM性能调优_第3张图片

    JVM第四讲:JVM性能调优_第4张图片

    • 其核心原理如下:
      • 在 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和OLD GC频繁导致 TP999耗时较高,YGC每分钟50次,每次25毫秒,OLD GC几分钟一次,每次200毫秒

原因分析:

  • 该服务要求低延迟,为了YGC较快完成,年轻代设置较小,但是由于年轻代较小,反而导致YGC次数较多,查看GC日志发现,由于动态年龄的因素,大量对象较早晋升到老年代,从而导致OLD GC频繁

优化策略

  • 扩大新生代内存为原来的3倍

优化效果:

  • 单词YGC耗时增加了5%,频率降低了60%,服务TP99降低了 10ms+,OLD GC频率降低为几小时一次

6、JVM 调优的参数可以在哪儿设置参数值

可以在IDEA,Eclipse,工具里设置
JVM第四讲:JVM性能调优_第5张图片

如果上线了是WAR包的话可以在Tomcat设置

如果是Jar包直接 :java -jar 直接插入JVM命令就好了

  • 补充图片

7、说一下 JVM 调优的工具?

JDK 自带了很多监控工具,都位于 JDK 的 bin 目录下,其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具

  • jconsole:用于对 JVM 中的内存、线程和类等进行监控

  • JVM第四讲:JVM性能调优_第6张图片

  • jvisualvm:JDK 自带的全能分析工具,可以分析:内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等

  • JVM第四讲:JVM性能调优_第7张图片

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前和后,内存的变化

你可能感兴趣的:(深入理解java虚拟机,java,JVM,调优,实战)