# GC

# 概述

# 算法

  • 标记-清除(Mark-Sweep)
    • 位置不连续,容易产生碎片
    • 标记和清除需要两遍扫描,都比较耗时
  • 复制(Copying)
    • 没有碎片产生
    • 50%的空间浪费
  • 标记-整理(Mark-Compact)
    • 没有碎片产生
    • 扫描和移动指针,效率低

# 指标

吞吐量:指系统应用程序花费的时间和系统运行总时长的比值。GC的吞吐量一般不低于95%。

GC的吞吐量 = 1- GC耗时/系统总运行时间

卡顿时间: 卡顿时间是垃圾收集器在工作的时候,应用程序暂停的时间。

垃圾回收频率: 垃圾回收频率时间和卡顿时间是互相影响的。

  • 如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。
  • 如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。

Minor GC

单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。如果堆内存中存活时间比较长的对象多,增加年轻代的空间,单次Minor GC的时间反而会增加,如果是堆内存中短期对象多,那么扩容后,单词Minor GC的时间不会明显的增加,还降低了Minor GC频率。

Full GC

Full GC的触发通常是因为堆内存空间不足或者老年代对象太多造成的

  • 1.减少创建大对象
  • 2.增大对内存空间
  • 3.合适的GC回收器:

# JVM GC

java中的GCroot:

  • Java虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中JNI(既一般说的Native方法)引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量的引用对象。
  • 类加载器
  • 线程 Thread 类
-XX:+PrintGC  #输出GC日志
-XX:+PrintGCDetails #输出GC的详细日志
-XX:+PrintGCTimeStamps #输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps #输出GC的时间戳(以日期的形式,如 2020-12-08T23:59:59.234+0800)
-XX:+PrintHeapAtGC #在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log #日志文件的输出路径

# YGC触发条件

  1. eden区快满: 一个是为对象分配内存不够,一个是为 TLAB 分配内存不够。
  2. 部分收集器进行FGC前

# 进入老年代的条件

  1. 年龄超过参数
  2. 对象大小超过参数
  3. 空间分配担保
  4. 动态年龄判定规则
-XX:PretenureSizeThreshold
 
-XX:+MaxTenuringThreshold # 动态年龄会调整该值

-XX:TargetSurvivorRatio # 动态年龄比例阈值,默认50%

# 触发FGC的条件

主要: 晋升对象大于老年代的剩余空间

  1. 老年代的内存使用率大于阈值
  2. 空间分配担保规则
  3. 元空间内存不足
  4. System.gc()调用,jmap -dump等命令

在YGC之前,会先检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。如果小于,说明YGC是不安全的,则会查看参数 HandlePromotionFailure 是否被设置成了允许担保失败,如果不允许则直接触发Full GC;如果允许,那么会进一步检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果小于也会触发 Full GC。

# GC影响按严重程度从高到底

  1. FGC过于频繁:FGC通常是比较慢的,几百毫秒到几秒,正常情况FGC每隔几个小时甚至几天才执行一次,对系统的影响还能接受。但是,一旦出现FGC频繁(比如几十分钟就会执行一次),这种肯定是存在问题的,它会导致工作线程频繁被停止,让系统看起来一直有卡顿现象,也会使得程序的整体性能变差。

  2. YGC耗时过长:YGC的总耗时在几十或者上百毫秒是比较正常的,虽然会引起系统卡顿几毫秒或者几十毫秒,这种情况几乎对用户无感知,对程序的影响可以忽略不计。但是如果YGC耗时达到了1秒甚至几秒(都快赶上FGC的耗时了),那卡顿时间就会增大,加上YGC本身比较频繁,就会导致比较多的服务超时问题。

  3. FGC耗时过长:FGC耗时增加,卡顿时间也会随之增加,尤其对于高并发服务,可能导致FGC期间比较多的超时问题,可用性降低,这种也需要关注。

  4. YGC过于频繁:即使YGC不会引起服务超时,但是YGC过于频繁也会降低服务的整体性能,对于高并发服务也是需要关注的。

启动的线程数由-XX:ParallelGCThread=N参数控制。这个参数值会影响到下面这些操作:

  • 使用-XX:+UseParallelGC收集新生代空间
  • 使用-XX:+UseParallelOldGC收集老年代空间
  • 使用-XX:+UseParNewGC收集新生代空间
  • CMS 收集器的 “STW” 阶段
  • G1收集器的 “STW” 阶段

# 收集器

Serial/Serial Old

  • 单线程
  • Serial:Copying
  • Serial Old :Mark-Compact
  • STW

-XX:+UseSerialGC -XX:+UseSerialOldGC

ParNew

  • Serial的多线程版本
  • Copying

-XX:+UseParNewGC

-XX:ParallelGCThreads 限制线程数量,默认开启和CPU数据相同的线程数。

Parallel Scavenge/Parallel Old

  • 吞吐量优先 : 达到一个可控制的吞吐量
  • 自适应调节策略

-XX:+UseParallelGC -XX:+UseParallelOldGC

-XX:ParallelGcrhreads: 年轻代并行收集器的线程数  [8 或者  3+(5*CPU Count)/8]
-XX:MaxGCPauseMillis: 最大停顿时间(毫秒)
-XX:GCTimeRatio: 垃圾收集时间占总时间的比例(取值范围(0,100)。默认值99)
-XX:+UseAdaptivesizepplicy: 自适应调节策略

# CMS

尽可能缩短垃圾收集时用户线程的停顿时间。

  • 标记-清除
# 阶段
  1. 初始标记(CMS initial mark)

标记直接可达对象

  1. 并发标记(CMS concurrent mark)

并发标记全部可达对象

  1. 重新标记(CMS remark)

修正变动导致的对象

  1. 并发清除(CMS concurrent sweep)
# 缺点:
  • 处理器敏感:并发标记、并发清理
  • 浮动垃圾
  • 并发失败
  • 内存碎片
# 参数
-XX:+UseConcMarkSweepGC # 开启CMS
-XX:+CMSParallelRemarkEnabled
-XX:MaxGCPauseMillis=50 # 设置GC暂停等待时间,单位为毫秒,不要设置过低。 
-XX:ParallelcMSThreads  #设置 CMS 的线程数量。(默认(Parallel GCThreads+3)/4)
-XX:+UseCompressedOops  # 对类对象数据进行压缩处理,提高内存利用率

-XX:CMSInitiatingOccupancyFraction #达到堆内存使用率的阈值后开始进行回收。

-XX:+UseCMSCompactAtFullCollection  #开启Full GC 时的压缩功能,减少内存碎片。
# 问题

concurrent mode failure

  • 原因1:CMS触发太晚
  • 方案:将-XX:CMSInitiatingOccupancyFraction=N调小;

由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用 Serial old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

  • 原因2:空间碎片太多
  • 方案:开启空间碎片整理,并将空间碎片整理周期设置在合理范围;

-XX:+UseCMSCompactAtFullCollection (空间碎片整理)

-XX:CMSFullGCsBeforeCompaction=n,执行多少次Full GC之后再执行一次内存碎片整理工作,默认是0,意思就是每次Full GC之后都会进行一次内存整理。

CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

  • 原因3:垃圾产生速度超过清理速度
  • 晋升阈值过小; Survivor空间过小,导致溢出; Eden区过小,导致晋升速率提高; 存在大对象;

ParNew+CMS 参数

-XX:ParallelGCThreads=2  Young GC工作时的并行线程数
-XX:ParallelCMSThreads=3 CMS GC 工作时的并行线程数
-XX:+CMSParallelRemarkEnabled 并行运行最终标记阶段,加快最终标记的速度
-XX:+CMSParallelInitialMarkEnabled 初始阶段开启多线程并发执行,减少STW时间
-XX:+CMSScavengeBeforeRemark 在CMS重新标记阶段之前,执行一次Young GC,因为重新标记是整堆标记的,执行一次Young GC,回收调年轻代里没人引用的对象,减少扫描对象。
-XX:MaxTenuringThreshold=15 对象从新生代晋升到老年代的年龄阈值(每次 Young GC 留下来的对象年龄加一),默认值15
-XX:+UseCMSCompactAtFullCollection 开启碎片整理
-XX:CMSFullGCsBeforeCompaction=2 与-XX:+UseCMSCompactAtFullCollection配合使用,表示进行2次Full GC后进行整理
-XX:+UseCMSInitiatingOccupancyOnly 只根据老年代使用比例来决定是否进行CMS
-XX:CMSInitiatingOccupancyFraction=80 设置触发CMS老年代回收的内存使用率占比,达到80%时触发old gc
-XX:+CMSClassUnloadingEnabled 默认开启,表示开启 CMS 对元空间的垃圾回收,避免由于元空间耗尽带来 Full GC


# G1

在延迟可控的情况下获得尽可能高的吞吐量,

在 G1 中,「不存在单独回收老年代的行为,而是当要发生老年代的回收时,同时也会对新生代以及大对象进行回收,因此这个阶段称之为混合回收(Mixed GC)」

# 阶段
  1. 初始标记(Initial Marking):标记一下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程
  2. 并发标记(Concurrent Marking):从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
  3. 最终标记(Final Marking):修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程
  4. 筛选回收(Live Data Counting and Evacuation):对各个 Region 的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划
# 参数

开启G1后,需要设置最大内存和最大停顿时间

-XX:UseG1GC  #开启G1
-XX:MaxGCPauseMillis=200  #设置最大的GC停顿时间目标


-XX:G1NewSizePercent=5 #新生代占整个堆内存的初始比例,默认5
-XX:G1MaxNewSizePercent=60 #新生代占整个堆的最大比例,默认60


# 开始混合回收阶段
-XX:InitiatingHeapOccupancyPercent=45  #老年代占堆内存45%时,会触发混合回收阶段。

-XX:G1MixedGCCountTarget # 混合回收执行次数,默认8次

-XX:ConcGCThreads=n   # 并发GC使用的线程数
-XX:ParallelGCThreads=n # GC工作的线程数量


# 结束混合回收阶段
-XX:G1HeapWastePercent=5 #  默认值是5%,空闲Region数量达到了堆内存的5%,停止混合回收

-XX:G1ReservePercent=n     # 设置作为空闲时间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%

-XX:G1MixedGCLiveThresholdPercent #默认值85%,存活对象比例大于85%,不回收该region

G1收集器根据整个堆的占用情况而不是某个分代来触发一次并发回收循环。如果这个值指定为0,则意味着会进行恒定的GC循环。默认值为45(占用45%)

  1. humongous regions太多,可通过-XX:G1HeapRegionSize调整region的大小
  2. Concurrent Marking太慢,可通过-XX:ConcGCThreads调整线程数

当mixed gc回收速度赶不上内存分配的速度,G1会使用单线程(SerialOld)进行Full GC

优化原则:

  1. 尽量避免 Full GC 的触发。
  2. 减少 Mixed GC 发生的次数
# 对比

垃圾收集器的选择主要看两个关键指标,停顿时间和吞吐量。

  1. 并发收集器[停顿时间优先]:CMS、G1 ---->适用于相对时间有要求的场景,比如Web
  2. 并行收集器[吞吐量优先]:Parallel Scanvent 和 Parallel Old ----> 适用于科学计算、后台处理等若交互场景
  3. 串行收集器:Serial 和 Serial Old ---->适合内存比较小,嵌入式的设备

优点: 开发便利,防内存泄露

# ZGC

基于 reigon,没分新生代和老年代,几乎所有阶段都是并发的,整堆扫描,部分收集

https://juejin.cn/post/6915208286610391054

# 参考资料

  1. cms优化 (opens new window)
  2. oracle建议-G1 (opens new window)