当前位置 : 主页 > 编程语言 > java >

JVM问题分析调优经验

来源:互联网 收集:自由互联 发布时间:2023-02-04
一、前言 ​​JVM​​性能优化步骤: 预估系统参数 压测后,调整​​JVM​​参数 线上系统监控和优化 统一的​​JVM​​参数模板 线上频繁​​Full GC​​的表现: 机器CPU负载过高 频

一、前言

​​JVM​​ 性能优化步骤:

  • 预估系统参数
  • 压测后,调整 ​​JVM​​ 参数
  • 线上系统监控和优化
  • 统一的 ​​JVM​​ 参数模板
  • 线上频繁 ​​Full GC​​ 的表现:

    • 机器 CPU 负载过高
    • 频繁 Full GC 报警
    • 系统无法处理请求或者处理过慢

    频繁 ​​​Full GC​​ 常见原因:

  • 对象频繁进入老年代,频繁触发 Full GC
  • 系统承载高并发请求,或处理数据量过大,导致 ​​Young GC​​ 频繁,每次 ​​Young GC​​ 过后存活对象太多,内存分配不合理,​​Survivor​​ 区域过小。

  • 系统一次性加载过多数据进入内存,大对象直接入老年代,频繁触发 Full GC
  • 内存泄漏,对象无法回收,一直占用在老年代里,频繁触发 Full GC
  • ​​MetaSpace​​ (永久代)加载类过多,触发 Full GC
  • 代码中使用 System.gc(),触发 Full GC
  • 针对以上 ​​​Full GC​​ 常见的原因,对应的优化方式:

  • ​​jstat​​ 分析,合理分配内存,调大 Survivor 区域
  • ​​dump​​ 出内存快照,用 MAT 工具进行分析,代码上排查
  • ​​dump​​ 出内存快照,用 MAT 工具进行分析,代码上排查
  • 若内存使用不多,还频繁触发 Full GC,那么优化加载的类
  • 若内存使用不多,还频繁触发 Full GC,代码上排查,删除 System.gc()
  • 一、案例一:高分配速率(​​High Allocation Rate​​)

    分配速率(​​Allocation rate​​)表示单位时间内分配的内存量。

    通常使用 ​​MB/sec​​ 作为单位。上一次垃圾收集之后, 与下一次 ​​GC​​ 开始之前的年轻代使用量, 两者的差值除以时间, 就是分配速率。分配速率过高就会严重影响程序的性能, 在 ​​JVM​​ 中可能会导致巨大的 ​​GC​​ 开销。

    • 正常系统: 分配速率较低 ~ 回收速率 -> 健康
    • 内存泄漏: 分配速率 持续大于 回收速率 -> ​​OOM​​
    • 性能劣化: 分配速率较高 ~ 回收速率 -> 亚健康
  • ​​JVM​​ 启动之后 291 ms, 共创建了 33,280 KB 的对象。第一次 Minor GC(小型GC) 完成后, 年轻代中还有 5,088 KB 的对象存活。
  • 在启动之后 446 ms, 年轻代的使用量增加到 38,368 KB , 触发第二次 GC, 完成后年轻代的使用量减少到 5,120 KB。
  • 在启动之后 829 ms, 年轻代的使用量为 71,680 KB, GC 后变为 5,120 KB。
  • 思考一个问题, 分配速率, 到底影响什么?

    想一想, ​​new​​ 出来的对象, 在什么地方。

    答案就是, ​​Eden​​。

    假如我们增加 ​​Eden​​, 会怎么样。 考虑蓄水池效应。 最终的效果是, 影响 ​​Minor GC​​ 的次数和时间, 进而影响吞吐量。

    在某些情况下, 只要增加年轻代的大小, 即可降低分配速率过高所造成的影响。

    增加年轻代空间并不会降低分配速率, 但是会减少 ​​GC​​ 的频率。如果每次 ​​GC​​ 后只有少量对象存活, ​​minor GC​​ 的暂停时间就不会明显增加。

    二、案例二:过早提升(​​Premature Promotion​​)

    提升速率(​​promotion rate​​)用于衡量单位时间内从年轻代提升到老年代的数据量。

    一般使用 ​​MB/sec​​ 作为单位, 和分配速率类似。

    ​​JVM​​ 会将长时间存活的对象从年轻代提升到老年代。根据分代假设, 可能存在一种情况, 老年代中不仅有存活时间长的对象, 也可能有存活时间短的对象。

    这就是过早提升: 对象存活时间还不够长的时候就被提升到了老年代。

    ​​major GC​​ 不是为频繁回收而设计的, 但 ​​major GC​​ 现在也要清理这些生命短暂的对象, 就会导致 ​​GC​​ 暂停时间过长。这会严重影响系统的吞吐量。

    JVM问题分析调优经验_加载

    ​​GC​​ 之前和之后的年轻代使用量以及堆内存使用量。

    这样就可以通过差值算出老年代的使用量。

    和分配速率一样, 提升速率也会影响 ​​GC​​ 暂停的频率。但分配速率主要影响 ​​minor GC​​, 而提升速率则影响 ​​major GC​​ 的频率。

    有大量的对象提升, 自然很快将老年代填满。老年代填充的越快, 则 ​​major GC​​ 事件的频率就会越高。

    JVM问题分析调优经验_加载_02

    一般来说过早提升的症状表现为以下形式:

  • 短时间内频繁地执行 ​​full GC​​
  • 每次 ​​full GC​​ 后老年代的使用率都很低, 在 10-20% 或以下
  • 提升速率接近于分配速率
  • 要演示这种情况稍微有点麻烦, 所以我们使用特殊手段, 让对象提升到老年代的年龄比默认情况小很多。 指定 ​​GC​​ 参数 ​​-Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1​​, 运行程序之后, 可以看到下面的 ​​GC​​ 日志:

    JVM问题分析调优经验_JVM_03

    解决这类问题, 需要让年轻代存放得下暂存的数据, 有两种简单的方法:

  • 增加年轻代的大小, 设置 JVM 启动参数, 类似这样: -Xmx64m -XX:NewSize=32m, 程序在执行时, Full GC 的次数自然会减少很多, 只会对 minor GC 的持续时间产生影响。
  • 减少每次批处理的数量, 也能得到类似的结果。至于选用哪个方案, 要根据业务需求决定。在某些情况下, 业务逻辑不允许减少批处理的数量, 那就只能增加堆内存, 或者重新指定年轻代的大小。 如果都不可行, 就只能优化数据结构, 减少内存消耗。
  • 但总体目标依然是一致的: 让临时数据能够在年轻代存放得下。

    上一篇:Spring 集成对 Web 服务的支持
    下一篇:没有了
    网友评论