GC调优总结

jvm虚拟机怎么调优?

调优套路总结、沉淀

1.内存分布

img

分代和分区

  • 新生代、老年代、方法区
  • 分模块(扩展)

jvm为什么选择分代?

三个假说(对象年龄差异)

  • 弱分代假说
    • 有的朝生夕死 接口结果
  • 强分代假说
    • 有的长期存活 spring相关,单例
  • 跨代引用假说
    • 相互引用

针对不同分代使用不同策略的收集算法

年轻代

特性

​ 对象存活率低

使用算法

​ 复制算法

具体内存分布

为什么使用三个区 eden\Survivor1\Survivor2? 而不是传统的from to的划分?

  • 首先是比例:8:1:1?

    普通复制算法 空间浪费50%,98% 会死亡,所以不需要太多空间

  • 9:1不就好了,那么为啥要两个呢

    • From to的思想
    • 碎片化问题
    • 根本原因why a separate Eden space

*Your question, “why are there two survivor spaces?” would actually better be phrased, “why a separate Eden space?”.

HotSpot has introduced the Eden space as an optimization which keeps the capacity of the new allocation region constant, betting on the outcome that a large proportion of the objects will immediately turn into garbage. You can look at Eden as a portion of memory shared between the two spaces—the portion that will probably get freed upon the next GC. This actually improves memory utilization.*

@see https://stackoverflow.com/questions/21476348/java-gc-why-two-survivor-spaces

@see https://stackoverflow.com/questions/10695298/java-gc-why-two-survivor-regions

流程

eden区满了 移动到s0 触发mirro gc

s0放不下怎么办?

​ 担保机制。

s0满了直接老年区?

​ 不 晋升机制

大对象直接存放(S0的一半)

老年代

特点:对象存活率高

使用算法:标记清除、标记整理

标记清除

​ cms

标记整理

g1

永久代

fullgc

Java7 之前字符串常量池被放到了 Perm 区,所有被 intern 的 String 都会被存在这里,由于 String.intern 是不受控的,所以 -XX:MaxPermSize 的值也不太好设置,经常会出现 java.lang.OutOfMemoryError: PermGen space 异常,所以在 Java7 之后常量池等字面量(Literal)、类静态变量(Class Static)、符号引用(Symbols Reference)等几项被移到 Heap 中。而 Java8 之后 PermGen 也被移除,取而代之的是 MetaSpace。

在上层,MetaSpace 主要由 Klass Metaspace 和 NoKlass Metaspace 两大部分组成。

  • Klass MetaSpace: 就是用来存 Klass 的,就是 Class 文件在 JVM 里的运行时数据结构,这部分默认放在 Compressed Class Pointer Space 中,是一块连续的内存区域,紧接着 Heap。Compressed Class Pointer Space 不是必须有的,如果设置了 -XX:-UseCompressedClassPointers,或者 -Xmx 设置大于 32 G,就不会有这块内存,这种情况下 Klass 都会存在 NoKlass Metaspace 里。
  • NoKlass MetaSpace: 专门来存 Klass 相关的其他的内容,比如 Method,ConstantPool 等,可以由多块不连续的内存组成。虽然叫做 NoKlass Metaspace,但是也其实可以存 Klass 的内容,上面已经提到了对应场景。

2.回收器

垃圾回收算法?

  • 复制
  • 标记清除
  • 标记整理

回收器

​ serial

​ parNew

​ parallel scavenge

​ cms

​ g1

she nan doah

zgc

3.日志&参数(活跃数据)

参考原则

​ 活跃对象是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小

空间 倍数
总大小 3-4 倍活跃数据的大小
新生代 1-1.5 活跃数据的大小
老年代 2-3 倍活跃数据的大小
永久代 1.2-1.5 倍Full GC后的永久代空间占用

确认目标

  • 吞吐优先(计算)
  • 延迟(rpc)
  • 内存占用(嵌入式应用)

优化

  • 通过GC信息(日志 最基本的-XX:+HeapDumpOnOutOfMemoryError)

    img

  • 选择GC回收器

  • 设置内存比例

  • 调整JVM参数

验证结果

4.举例子

案例一:Major GC和Minor GC频繁

确定目标

服务情况:服务情况:Minor GC每分钟100次 ,Major GC每4分钟一次,单次Minor GC耗时25ms,单次Major GC耗时200ms,接口响应时间50ms。

优化目标:降低TP99、TP90时间

优化

通常情况下,由于新生代空间较小,Eden区很快被填满,就会导致频繁Minor GC,因此可以通过增大新生代空间来降低Minor GC的频率。例如在相同的内存分配率的前提下,新生代中的Eden区增加一倍,Minor GC的次数就会减少一半。

==增加一倍?gc时间增加一倍?==

单次Minor GC时间由以下两部分组成:T1(扫描新生代)和 T2(复制存活对象到Survivor区)

扩大前: 存活对象多,需要复制

扩大后: 存活对象少,不需要复制,或复制少量

复制操作成本大于扫描成本,所以,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小

查看gc 日志,确认下服务中对象的生命周期分布情况

通过上图GC日志中两处红色框标记内容可知: 1. new threshold = 2(动态年龄判断,对象的晋升年龄阈值为2),对象仅经历2次Minor GC后就晋升到老年代,这样老年代会迅速被填满,直接导致了频繁的Major GC。 2. Major GC后老年代使用空间为300M+,意味着此时绝大多数(86% = 2G/2.3G)的对象已经不再存活,也就是说生命周期长的对象占比很小。

优化结果

通过扩容新生代为为原来的三倍,单次Minor GC时间增加小于5ms,频率下降了60%,服务响应时间TP90,TP99都下降了10ms+,服务可用性得到提升

小结

如何选择各分区大小应该依赖应用程序中对象生命周期的分布情况:如果应用存在大量的短期对象,应该选择较大的年轻代;如果存在相对较多的持久对象,老年代应该适当增大。

案例二:Major GC时间过长

确定目标

major gc过长,查看gc日志发现remark阶段时间过长 1s多

优化

CMS的四个主要阶段

==一、Init-mark初始标记(STW)==

​ 标记GC ROOT能直接关联到的对象

img

  • ​ GC root(运行中的数据)

    • 虚拟机栈引用的变量

    • 静态属性引用的变量

    • 常量池子引用的变量

    • 本地虚拟机栈

    • 虚拟机内部类

    • 同步锁

    • 虚拟机内部的jmxbean jtibean

    • ==部分回收时候,其他区域的引用==

二、并发标记阶段

​ 三色标记法

==三、Remark重新标记(STW)==

​ 修正并发期间,增量更新 :黑色对象插入新的引用关系,重新扫描黑色对象(插入新引用变为灰色 cms)

如果仅扫描老年代中对象,即以老年代中对象为根,判断对象是否存在引用,上图中,对象A因为引用存在新生代中,它在Remark阶段就不会被修正标记为可达,GC时会被错误回收。 新生代对象持有老年代中对象的引用,这种情况称为“跨代引用”。因它的存在,Remark阶段必须扫描整个堆来判断对象是否存活,包括图中灰色的不可达对象。

img

四、并发清理,进行并发的垃圾清理

优化效果

经过增加CMSScavengeBeforeRemark参数,单次执行时间>200ms的GC停顿消失,从监控上观察,GCtime和业务波动保持一致,不再有明显的毛刺。

小结

通过案例分析了解到,由于跨代引用的存在,CMS在Remark阶段必须扫描整个堆,同时为了避免扫描时新生代有很多对象,增加了可中断的预清理阶段用来等待Minor GC的发生。只是该阶段有时间限制,如果超时等不到Minor GC,Remark时新生代仍然有很多对象,我们的调优策略是,通过参数强制Remark前进行一次Minor GC,从而降低Remark阶段的时间。

Minor GC时也必须扫描老年代。怎么避免全堆扫描的,老年代持有新生代对象引用的情况不足1%,引入记忆集,具体实现是卡表。

卡表的具体策略是将老年代的空间分成大小为512B的若干张卡(card)。卡表本身是单字节数组,数组中的每个元素对应着一张卡,当发生老年代引用新生代时,虚拟机将该卡对应的卡表元素设置为适当的值。如上图所示,卡表3被标记为脏(卡表还有另外的作用,标识并发标记阶段哪些块被修改过),之后Minor GC时通过扫描卡表就可以很快的识别哪些卡中存在老年代指向新生代的引用。这样虚拟机通过空间换时间的方式,避免了全堆扫描。

案例三 发生Stop-The-World的GC

并发的 CMS GC 算法,退化为 Foreground 单线程串行 GC 模式,STW 时间超长,有时会长达十几秒

目标:优化目标是降低单次STW回收停顿时间,提高可用性。

优化

  • Perm空间不足;
  • **晋升失败 **promotion failed(是指在进行 Young GC 时,Survivor 放不下,对象只能放入 Old,但此时 Old 也放不下)
    • 内存碎片,配置清理策略
  • 增量收集担保失败,统计得到的Young GC晋升到老年代的平均大小大于老年代的剩余空间;
    • 降低触发 CMS GC 的阈值
  • 并发模式失败concurrent mode failure(concurrent mode failure发生的原因一般是CMS正在进行,但是由于老年代空间不足,需要尽快回收老年代里面的不再被使用的对象,这时停止所有的线程,同时终止CMS,直接进行Serial Old GC);
    • CMSScavengeBeforeRemark ,防止晋升过多对象,浮动垃圾
  • 主动触发Full GC(执行jmap -histo:live [pid])来避免碎片问题。

原因:根据日志发现Full GC后,Perm区变大了,推断是由于永久代空间不足容量扩展导致的。

解决方法:

  • 通过把-XX:PermSize参数和-XX:MaxPermSize设置成一样,强制虚拟机在启动的时候就把永久代的容量固定下来,避免运行时自动扩容。
  • CMS默认情况下不会回收Perm区,通过参数CMSPermGenSweepingEnabled、CMSClassUnloadingEnabled ,可以让CMS在Perm区容量不足时对其回收。

效果

5.根因鱼骨图

img

资料:美团

https://tech.meituan.com/2020/11/12/java-9-cms-gc.html

https://tech.meituan.com/2017/12/29/jvm-optimize.html


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 951488791@qq.com

文章标题:GC调优总结

字数:2.7k

本文作者:zhengyumin

发布时间:2021-01-13, 21:36:01

最后更新:2021-01-26, 15:30:21

原始链接:http://zyumin.github.io/2021/01/13/GC%E8%B0%83%E4%BC%98%E6%80%BB%E7%BB%93/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。