跳转到内容
wechat

10. 调优

1. 事前使用合理的手段,生产时基本上是不需要调优的

1.1. 合理的架构设计

  • 更好的针对性能问题和结构问题进行聚焦和优化

1.2. 代码Review

  • 禁止查询全表数据给客户端,会导致创建大量对象
  • 文件、链接等资源使用完毕之后需要关闭
  • 禁止使用System.gc()方法
    • 避免fullgc
  • 合适的设计模式
    • 有利于提高代码复用,减少方法区大小

1.3. 代码中完备的日志记录

1.4. 项目启动时合理的配置参数

1.5. 服务器上完善的监控系统

  • 链路追踪
  • 服务器资源监控

2. 两类问题

3. 大对象导致OOM

3.1. 获取堆栈信息

3.2. 使用jvisualvm找到大对象的堆栈信息

3.3. 在堆栈信息上找到对应的代码

4. 资源未关闭

4.1. 思路差不多同上


5. 综述

6. JVM调优和生产问题的方法论

事实上,JVM相关的问题并不可怕,重要的是我们需要建立关于JVM使用层面的完整的方法论。

  1. 问题总是在发展中逐渐被发现,换言之,我们不可能预想到所有的问题,所以先干起来再说,也就是不管三七二十一,先投产再说,遇到问题了再解决。
  2. JVM层面的调优和生产环境问题定位可以看做是两部分内容。

6.1. 判断JVM运行异常的方法论

  1. 要想知道JVM运行是否正常,首先要知道JVM正常运行时的状态是怎样的,也就是说要知道JVM正常运行时的性能指标是怎样的,我们要确立一个评价JVM正常运行与JVM异常运行的衡量标准。其次,我们还需要建立一个长效的监控机制,监控JVM长时间运行的状态,才能够捕捉异常的状态。最后,才是我们针对异常的运行状态进行调优的过程。
  2. 评价JVM运行是否正常的衡量标准,不是『放之四海而皆准』的标准,而是『家家有本经』的标准。所以,这个标准与我们的生产实践有关。常见的实践过程是通过试运营期来摸索。在试运营期,我们可以不断的调整JVM各项配置参数,并监控记录在此期间的各项资源的使用情况,以此来获取最优的配置参数。还有一种实践方式是直接压测方式,就是直接使用各种压测工具,对生产环境进行压力测试,测试出系统资源占用最大情况下的业务指标和JVM指标,并以资源占用最大情况下的JVM各种指标作为JVM的基准指标。

6.2. GC配置参数及正常指标的获取过程

  1. 试运行期:
    1. 运维团队会划拨一些与投产环境资源配置一样的服务器资源,然后根据开发团队提供的一些JVM各项配置参数部署上我们的应用程序,并不断观察在试运行期间的各项指标:
      1. 服务器各项资源使用情况(包括CPU负载、内存占用情况、线程忙闲程度、网络开销、磁盘IO开销等)
      2. JVMGC时的各项指标(包括GC频率、GC耗时、STW时间等)
    2. 然后在试运行期间,除了公测用户使用系统外,测试团队也会进行各项测试(如压测),运维团队会根据观察到的服务器各项指标,动态调整试运行期间的JVM各项配置参数,以期获取在服务器各项资源使用率最高的情况下的JVM的配置参数以及GC的各项指标
    3. 之后运维团队会上一步获取到的JVM配置参数设置投产环境,并确定好正式投产环境具体的JVM指标(这也是后续优化的目标);
      1. 一些常见的JVM配置参数
        1. -Xmx 等价于 -XX:MaxHeapSize
        2. -Xms 等价于 -XX:InitialHeapSize
        3. -Xmn
        4. -XX:PermSize 等价于 -XX:MaxDirectMemorySize
        5. -XX:SurvivorRatio
        6. -XX:+PrintGCDetails
        7. -XX:+PrintGCDateStamps
        8. -XX:+PrintGCTimeStamps
        9. -XX:HeapDumpOnOutOfMemoryError
        10. -XX:HeapDumPath
        11. -XX:MaxTenuringThreshold
        12. -XX:+PrintGCApplicationStoppedTime
      2. 一些常见的JVM指标
        1. FullGC频率为每天一次;
        2. OldGC频率为每天一次;
        3. YoungGC频率为3-6s一次;
        4. 每次YoungGC耗时不得超过500ms;
        5. 每次FullGC耗时不得超过700ms;
        6. 每次OldGC耗时不超过700ms;
        7. 相邻两次YoungGC后的堆空间容量增长不超过10%『这说明YoungGC回收比较彻底,基本没有对象到老年代中,说明回收效果很好,这个指标只针对于设置了可动态增长的堆空间大小』;
        8. OldGC后老年代容量增长比例不超过10%;
  2. 正常运行期,即功能迭代期
    1. 之后便是功能迭代,在不断的功能迭代过程中,运维团队也会不断的观察服务器各项指标,在功能迭代过程中,发现某项指标发生异常(与前面确定好的优化目标对比),此时就需要进行JVM调优了;
    2. 假如试运行期我们设置的YoungGC频率为3-6s一次,FullGC频率为一天一次,那么在后续的生产过程中,如果GC的频率远远高于这个值,比如1s十次,那就说明那么就需要进行调优了;如果GC频率远远低于这个值,比如一分钟一次,那就说明JVM相当空闲,基本处于无事可做的状态,那么就需要缩小占用空间了,就需要调低Xmn;
      1. YoungGC频率过高,则可以检查:
        1. 新生代所占空间大小,可以适当调大Xmn,使得新生代在整个堆空间中占比25%-40%;
        2. SurvivorRatio的比例,为8比较合适,这是经过JVM团队验证过的;
        3. 每次YoungGC后,幸存区的大小,如果幸存区容量过大,则表示不合理;
        4. 服务器SWAP、IO等情况,SWAP发生时,会拉长GC耗时,需要进行优化;
      2. FullGC频率过高,则可以检查:
        1. 新生代与老年代的比例,推荐是4:6
        2. 每次FullGC后,老年代的增长情况,如果增长比例很高,说明可能存在大对象,需要排查大对象;
        3. 业务代码中是否存在一些System.gc();如果没有配置-XX:+DisableExplicitGC,即没有屏蔽System.gc()触发FullGC,那么可以通过排查GC日志中有System字样判断是否System.gc()触发(日志样本:558082.666: [Full GC (System) [PSYoungGen: 368K->0K(42112K)];
        4. 如果使用了CMS收集器,可能还需要观察内存碎片问题;
        5. -XX:MetaspaceSize的值是首次FullGC的触发条件。当首次FullGC后,释放的空间不足,那么在不超过-XX:MaxMetaspaceSize的前提下,jvm会适当提高实际的元空间大小;如果释放的空间过多,则会适当降低元空间的大小。
  3. 但是实际运行过程中,我们处理更多的应该是OOM的情况。也就是说调优的阶段会在试运营期就完成,但是在实际生产运营期间,我们遇到的问题多是由我们的代码写的太菜而导致的。可以这样说:假如我们能够严格的控制软件工程的每一个阶段的质量,我们甚至连OOM可能都会很少遇到。

总结:

  1. 预估字节码的占用空间决定初始值;
  2. 小范围内测获得第一首性能指标
  3. 压测获得极限性能指标;
  4. 监控获得异常指标;
  5. 发展中发现问题并解决问题,即不过早优化;
  6. 具体业务有具体的指标;

6.3. 代码审核的一些要求

  1. 禁止进行全表查询;
  2. 禁止使用select *操作,用到那个字段就展示哪个字段等;
  3. 使用文件、网络等做IO操作时,需要关闭资源;
  4. 使用线程池而不是使用简单线程;
  5. List集合对象不宜过大,过大要考虑拆分;
  6. 禁止使用System.gc()方法

6.4. JVM的配置参数

6.5. GC日志分析

2023-04-04T12:08:11.670+0800: 104.882: [GC (Allocation Failure) [PSYoungGen: 262144K->43503K(305664K)] 282367K->138752K(1005056K), 0.0710736 secs] [Times: user=0.34 sys=0.06, real=0.07 secs] 
2023-04-04T12:06:27.802+0800: 1.014: [GC (Metadata GC Threshold) [PSYoungGen: 173046K->16268K(305664K)] 173046K->16292K(1005056K), 0.0088983 secs] [Times: user=0.03 sys=0.01, real=0.01 secs] 
2023-04-04T12:06:27.811+0800: 1.023: [Full GC (Metadata GC Threshold) [PSYoungGen: 16268K->0K(305664K)] [ParOldGen: 24K->15506K(699392K)] 16292K->15506K(1005056K), [Metaspace: 20436K->20434K(1067008K)], 0.0200635 secs] [Times: user=0.07 sys=0.00, real=0.02 secs] 
2023-04-04T12:08:23.399+0800: 116.611: [Full GC (Ergonomics) [PSYoungGen: 43515K->0K(160256K)] [ParOldGen: 555360K->584932K(699392K)] 598876K->584932K(859648K), [Metaspace: 40740K->40740K(1087488K)], 0.9348212 secs] [Times: user=6.37 sys=0.07, real=0.94 secs]
2023-04-04T12:08:11.670+0800: 104.882: [GC (Allocation Failure) [PSYoungGen: 262144K->43503K(305664K)] 282367K->138752K(1005056K), 0.0710736 secs] [Times: user=0.34 sys=0.06, real=0.07 secs] 

2023-04-04T12:08:11.670+0800: 表示发生GC的时间戳
104.882 : 表示从JVM启动,到打印这行日志,经过了104.882秒的时间;
GC (Allocation Failure) : 表示GC触发的类型是『Allocation Failure』,即分配失败;
[PSYoungGen: 262144K->43503K(305664K)] : 表示新生代使用的是ParraleScavenge回收器,262144K->43503K,表示从262144K回收到43503K,305664K表示新生代总容量;
282367K->138752K(1005056K) : 表示整个堆从282367K降到138752K,堆的总容量为1005056K;
0.0710736 secs : 表示GC的时间是0.0710736秒;
Times: user=0.34 sys=0.06, real=0.07 secs : 表示整个GC过程中,用户态占用时间、系统态占用时长和真实用时
2023-04-04T12:06:27.811+0800: 1.023: [Full GC (Metadata GC Threshold) [PSYoungGen: 16268K->0K(305664K)] [ParOldGen: 24K->15506K(699392K)] 16292K->15506K(1005056K), [Metaspace: 20436K->20434K(1067008K)], 0.0200635 secs] [Times: user=0.07 sys=0.00, real=0.02 secs] 

这是FullGC的过程日志。
新生代总大小为305664K,从16268K降到0K;
老年代总大小为699392K,GC前为24K,GC后为15506K,整个堆大小为1005056K,GC前为16292K,GC后为15506K;
元空间总大小为:1067008K,GC前为20436K,GC后为20434K;
Times: user=0.07 sys=0.00, real=0.02 secs : 表示整个GC过程中,用户态占用时间、系统态占用时长和真实用时
2023-04-04T12:09:49.638+0800: 202.850: [Full GC (Ergonomics) [PSYoungGen: 116735K->0K(160256K)] [ParOldGen: 699391K->14256K(699392K)] 816127K->14256K(859648K), [Metaspace: 40493K->40493K(1087488K)], 0.0174834 secs] [Times: user=0.07 sys=0.01, real=0.02 secs] 
Heap
 PSYoungGen      total 160256K, used 41013K [0x00000007aab00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 116736K, 35% used [0x00000007aab00000,0x00000007ad30d7f8,0x00000007b1d00000)
  from space 43520K, 0% used [0x00000007bd580000,0x00000007bd580000,0x00000007c0000000)
  to   space 116224K, 0% used [0x00000007b1d00000,0x00000007b1d00000,0x00000007b8e80000)
 ParOldGen       total 699392K, used 14256K [0x0000000780000000, 0x00000007aab00000, 0x00000007aab00000)
  object space 699392K, 2% used [0x0000000780000000,0x0000000780dec388,0x00000007aab00000)
 Metaspace       used 41838K, capacity 43950K, committed 44592K, reserved 1087488K
  class space    used 5400K, capacity 5783K, committed 5936K, reserved 1048576K




used :已使用的空间大小
capacity:当前已经分配且未释放的空间容量大小
committed:当前已经分配的空间大小
reserved:预留的空间大小

capacity + 已经被释放的空间容量 = committed

G1日志样本

image.png

6.6. JVM的监控指标

./ch10-promote/image/1680776690652.png

6.7. JVM的调优工具

  1. Linux服务器自带的一些命令,如Top、ps等;
  2. jdk自带的一些命令,如jinfo、jcmd、jstack、jheap、jstat、jmap、jps、jvisualvm等
  3. 第三方工具,如Arthas、MAT、Jprofiler、JConsole等;

6.8. JVM调优案例

试运营期时,JVM调优的一些案例

  1. 堆空间、新生代、永久代空间、直接内存过小;
  2. 大对象
  3. 数组越界
  4. 线程资源耗尽

正式投产期,JVM调优的案例

  1. YoungGC频率异常
  2. FullGC频率异常
  3. STW时间过长

6.9. 线上OOM定位案例

  1. 大对象
  2. IO资源未关闭

7. 参考

诊断gc是否正常线上JVM调优实践,FullGC40次/天到10天一次的优化面试官:如何进行 JVM 调优(附真实案例)


// 待整理

  1. 事前要有良好的软件控制过程良好的软件监控过程

    1. 投产之前的代码Review过程;
    2. 代码中完备的日志打印;
    3. 项目的链路监控
    4. 服务器资源监控工具,如普罗米修斯、
  2. 投产时要配置上相对应的JVM监控参数;

    1. dump文件开关、dump文件目录
    2. gc文件开关、gc文件目录
  3. OOM的定位

    1. 根据OOM类型分情况讨论
  4. Fullgc的调优

8. 定位思路

如果有监控,那么通过图形能比较直观、快速的了解gc情况; 如果没有监控,那么只能看gc日志或jstat来分析 这是基本技能 一定要熟练

  1. 观察年轻代 gc的情况,多久执行一次、每次gc后存活对象有多少 survivor区多大 存活对象比较多 超过survivor区大小或触发动态年龄判断 => 调整内存分配比例
  2. 观察老年代的内存情况 水位情况,多久执行一次、执行耗时多少、回收掉多少内存 如果在持续的上涨,而且full gc后回收效果不好,那么很有可能是内存溢出了 => dump 排查具体是什么玩意
  3. 如果年轻代和老年代的内存都比较低,而且频率低 那么又可能是元数据区加载太多东西了
  4. 其实如果是自己负责的系统,可能要看是不是发版改了什么配置、代码

make it come true