本文在第一段先简单讲解调优的缘由和过程,具体涉及到的知识点,在后面段中具体介绍。

1. 调优过程

1.1. 问题定位

有一天突然收到监控告警,大批量产线服务实例在自动重启。于是赶紧上平台下载dump日志,以及检查其他监控事件,最终定位到问题:

那几分钟内,涌入几十万用户登录平台操作,导致内存吃紧,几乎每个实例都触发了几次 Full GC。而由于集中性的 Full GC,STW 时间过长,服务测活接口长期调不通,k8s判定服务故障,就重启pod。

问题定位了,除了优化代码,减少无效内存大量占用以外,还可以调优一下Jvm参数了。

1.2. gc 问题定位

既然是 gc 出的问题,那就通过 jstat -gcutil pid 时长间隔 命令,实时看一下gc的过程状态。

通过一段时间观察,发现每次 young gc 后,survivor 区域中占用比例很高(近百分之百),甚至某些次 old 区域中有略微增长。这说明一个问题:

young gc 后存活的对象太多,survivor区存放不下,溢出的对象就直接进入了老年代。这就加快了老年代内存的占用速度,提前需要 full gc。

gc 的问题也定位到了,接下来分几个步骤优化

1.3. gc 优化

分了几个维度:

  • 最直观表现是 survivor 区不足,因此可以加大一下年轻代中 survivor 比例,即减少-XX:SurvivorRatio(eden区和单个survivor区的比例,默认值:8)的值。
  • 稍微加大一下年轻代的占比,即减少-XX:NewRatio(老年代和年轻代的比例,默认值:2)的值。
  • 最根本的还是内存不足,所以如果可以,加大xms/xmx
  • 调优垃圾收集器,减少 full gc 中 stw 的时间,避免测活接口长时间停机。因为之前是jdk 8默认的(Parallel Scavenge + Parallel Old),换成了更适合高并发的(ParNew + CMS)。

2. jvm命令及参数

2.1. jstat -gcutil

注意:出于保密考虑,实际的数据不能在文中展示,下列展示的是与上下文无关的服务数据,是比较正常的 gc 过程数据:

[root@xxxapi data]# jstat -gcutil 1 1000
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
 50.07   0.00  53.80  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  57.11  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  62.04  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  64.26  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  66.61  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  69.18  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  71.68  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  74.75  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  77.20  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  80.12  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  83.09  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  87.77  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  89.82  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  92.29  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  94.57  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
 50.07   0.00  99.00  13.25  89.91  86.07   1082   21.299     5    2.055   23.354
  0.00  74.61   3.38  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61   7.88  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  11.46  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  14.93  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  18.23  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  21.29  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  25.22  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  28.74  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  31.64  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  36.85  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  39.30  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  44.76  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  48.55  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  51.25  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  54.17  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  58.48  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  61.99  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  64.52  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  67.25  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  70.92  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  74.60  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  78.43  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  82.41  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  86.26  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  90.79  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  93.74  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  95.90  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
  0.00  74.61  99.28  13.25  89.91  86.07   1083   21.325     5    2.055   23.380
 51.43   0.00   2.69  13.29  89.91  86.07   1084   21.345     5    2.055   23.400
 51.43   0.00   5.53  13.29  89.91  86.07   1084   21.345     5    2.055   23.400
 51.43   0.00   8.21  13.29  89.91  86.07   1084   21.345     5    2.055   23.400
  • S0:幸存1区当前使用比例
  • S1:幸存2区当前使用比例
  • E:伊甸园区使用比例
  • O:老年代使用比例
  • M:元数据区使用比例
  • CCS:压缩使用比例
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

2.2. 对象进入老年代途径

1. 对象年龄达到阈值后进入老年代

默认情况下,对象在新生代经历了15次GC后,便会达到进入老年代的条件,将对象转移进入老年代。当然,年龄的阈值可以通过JVM参数进行设置:

-XX:MaxTenuringThreshold=10
2. 大对象直接进入老年代

通过以下JVM参数进行设置:(注意此参数仅适用于Serial和ParNew两款新生代收集器。)

-XX:PretenureSizeThreshold=5242880

原因:

  • 大对象需要连续的内存空间,而新生代为了安放大对象可能需要多次进行GC,增加开销;
  • 新生代种伊甸园区和幸存者区常采用复制算法,需要经常复制对象到不同的区域,而大对象在复制时开销较大。
3. 动态选择进入老年代

HotSpot虚拟机并不一定会严格按照设置的年龄阈值,满足以下条件也能直接进入老年代:Survivor 区中,年龄从 1 到 n 的对象大小之和超过 Survivor 区的 50% 时,新生代中年龄大于等于 n 的对象将进入老年代。

注意一个误区:这个对象大小总和是按年龄从小到大累加的,并不是同龄对象。

4. young gc 后溢出的进入老年代

在 young gc后,正常存活对象放入 survivor区,但如果放不下,存活对象溢出的部分,就会被放入老年代。

3. 垃圾收集器

3.1. 年轻代

3.1.1. Serial

Serial是一类用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停。

  • 优势:简单高效,由于采用的是单线程的方法,因此与其他类型的收集器相比,对单个cpu来说没有了上下文之间的的切换,效率比较高。
  • 缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。
  • 适用场景:Client 模式(桌面应用);单核服务器。
参数配置
  • -XX:+UserSerialGC: 选择Serial作为新生代垃圾收集器

3.1.2. ParNew

ParNew收集器其实就是Serial的一个多线程版本,其在单核cpu上的表现并不会比Serail收集器更好,在多核机器上,其默认开启的收集线程数与cpu数量相等。

当用户线程都执行到安全点时,所有线程暂停执行,采用复制算法进行垃圾收集工作,完成之后,用户线程继续开始执行。

  • 优点:随着cpu的有效利用,对于GC时系统资源的有效利用有好处。
  • 缺点:和Serial是一样的。
  • 适用场景:ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器。因为CMS收集器只能与serial或者parNew联合使用,在当下多核系统环境下,首选的是ParNew与CMS配合。ParNew收集器也是使用CMS收集器后默认的新生代收集器。
参数配置
  • -XX:UseParNewGC: 新生代采用ParNew收集器
  • -XX:ParallelGCThreads: 设置JVM垃圾收集的线程数

3.1.3. Parallel Scavenge

Parallel Scavenge 也是一款用于新生代的多线程收集器,也是采用复制算法。与ParNew的不同之处在于:

Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而 ParNew 收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。例如虚拟机一共运行了 100 分钟,其中垃圾收集花费了 1 分钟,那吞吐量就是 99% 。比如下面两个场景:

  1. 垃圾收集器每 100 秒收集一次,每次停顿 10 秒;
  2. 垃圾收集器每 50 秒收集一次,每次停顿时间 7 秒。

虽然后者每次停顿时间变短了,但是总体吞吐量变低了,CPU 总体利用率变低了

  • 优点:追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。
  • 适用场景:注重吞吐量高效利用CPU,需要高效运算,且不需要太多交互。
参数配置
  • -XX:+UseParallelOldGC: 默认使用 ParallelOldGC 时候默认新生代使用的是 ParallelScavenge 收集器
  • -XX:MaxGCPauseMilis: 控制最大垃圾收集停顿时间,参数值是一个大于0的毫秒数,收集器尽可能保证回收花费时间不超过设定值。但将这个值调小,并不一定会使系统垃圾回收速度更快,GC停顿时间是以牺牲吞吐量和新生代空间换来的。
  • -XX:GCTimeRadio: 设置吞吐量大小,参数值是一个(0,100)两侧均为开区间的整数。也是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。若把参数设置为19,则允许的最大GC时间就占总时间的5%(1/(1+19))。默认值是99,即允许最大1%的垃圾收集时间。
  • -XX:+UserAdaptiveSizePolicy: 这是一个开关函数,当打开这个函数,就不需要手动指定新生代的大小,Eden与Survivor区的比例(-XX:SurvivorRatio,默认是8:1:1),晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等参数。JVM会动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略.

3.2. 老年代

3.2.1. Serial Old

Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法

  • 适用场景:Client模式;单核服务器;与Parallel Scavenge收集器搭配;作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure时使用

3.2.2. Parallel Old

Parallel Old是 Parallel Scavenge 收集器的老年代版本,使用 多线程和标记-整理算法,可以充分利用多核CPU的计算能力。

  • 适用场景:注重吞吐量与CPU资源敏感的场合,与Parallel Scavenge 收集器搭配使用,jdk7 和 jdk8 默认使用该收集器作为老年代收集器
参数配置
  • -XX:+UserParallelOldGC: 开启 ParallelScavenge + ParallelOld

3.2.3. CMS

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除”,运作过程分为四个步骤

  • 初始标记,标记GC Roots 能够直接关联到达对象
  • 并发标记,进行GC Roots Tracing 的过程
  • 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
  • 并发清除,用标记清除算法清除对象。

**其中“初始标记”和“重新标记”这两个步骤仍然需要STW(stop the world)。耗时最长的“并发标记”与“并发清除”过程收集器线程都可以与用户线程一起工作。总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。**

  • 优点:并发收集,低停顿
  • 缺点:
  • CMS收集器对CPU资源非常敏感,CMS默认启动对回收线程数(CPU数量+3)/4,当CPU数量在4个以上时,并发回收时垃圾收集线程不少于25%,并随着CPU数量的增加而下降,但当CPU数量不足4个时,对用户影响较大。
  • CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致一次FullGC的产生。这时会地洞后备预案,临时用SerialOld来重新进行老年代的垃圾收集。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然还会有新的垃圾产生,这部分垃圾出现在标记过程之后,CMS无法在当次处理掉,只能等到下一次GC,这部分垃圾就是浮动垃圾。同时也由于在垃圾收集阶段用户线程还需要运行,那也就需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他老年代几乎完全填满再进行收集。可以通过参数-XX:CMSInitiatingOccupancyFraction修改CMS触发的百分比。
  • 因为CMS采用的是标记清除算法,因此垃圾回收后会产生空间碎片。通过参数可以进行优化。
参数配置
  • -XX:+UseConcMarkSweepGC: 启用cms
  • -XX:ConcGCThreads: 并发的GC线程数
  • -XX:+UseCMSCompactAtFullCollection: FullGC之后做压缩整理(减少碎片)
  • -XX:CMSFullGCsBeforeCompaction: 多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
  • -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
  • -XX:+UseCMSInitiatingOccupancyOnly: 只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
  • -XX:+CMSScavengeBeforeRemark: 在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段
  • -XX:+CMSParallellnitialMarkEnabled: 表示在初始标记的时候多线程执行,缩短STW
  • -XX:+CMSParallelRemarkEnabled: 在重新标记的时候多线程执行,缩短STW;

3.4. 总结

可以搭配使用的垃圾收集器包括:

  • Serial + Serial old
  • Serial + CMS
  • ParNew + Serial old
  • ParNew + CMS
  • Parallel Scavenge + Serial 0ld
  • Parallel Scavenge + Parallel 0ld

目前常用的搭配如下:

  • Parrallel Scavenge + Parrallel OldJDK 8 默认收集器搭配。吞吐量优先,后台任务型服务适合;
  • ParNew + CMS:经典的低停顿搜集器,绝大多数商用、延时敏感的服务在使用;
  • G1JDK 9 默认收集器,堆内存比较大(6G-8G以上)的时候表现出比较高吞吐量和短暂的停顿时间;
作者:|KerryWu|,原文链接: https://segmentfault.com/a/1190000043591041

文章推荐

Java 网络编程 - RMI 框架

Kafka的系统架构和API开发

C# 面向对象

java垃圾回收机制(面试)

线上诊断神器-arthas基本应用

ros-python学习样例笔记

内存管理:判断对象是否存活

使用Docker部署Consul集群并由Ocelot调用

ModStart: 宝塔配置 MySQL 队列调度

Vue—关于插件(源码级别的插件分析+实践)

聊聊C#中的Mixin

详解TCP四次挥手(断开TCP连接过程)