跳至主要內容

GC

mozzie大约 9 分钟JavaJava

GC

垃圾判别算法

判断一个对象是否是一个垃圾的算法

  1. 引用计数算法:对于一个对象A,只要任何一个对象引用了A,那么的A的引用计数器则+1,当引用失效时,引用计数器-1。如果A的引用计数器=0了,则表示对象A不可能再被使用,可进行回收。

    优点:实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟性

    缺点:

    • 需要单独的字段存储计数器,增加了内存开销
    • 每次赋值都需要更新计数器,需要加减法操作,增加了时间开销
    • 无法处理循环引用,致命缺陷,Java的垃圾回收器没有使用这个算法
  2. 可达性分析算法(GC Root 根搜索算法):以根对象集合为起始点,按照从上至下的方法搜索被根对象集合所连接的目标对象是否可达。使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接,搜索所走过的路径称为引用链。如果目标对象没有任何引用链相连,则是不可达标记为垃圾对象。只有能够被根集合直接或者间接连接的对象才是存活对象。

    优点:实现简单,执行高效,有效的解决循环引用的问题,防止内存泄露。

    GC Roots:

    • 虚拟机栈中引用的对象:各个线程被调用的方法中使用到的参数、局部变量等。
    • 本地方法栈内JNI (通常说的本地方法)引用的对象
    • 类静态属性引用的对象:比如: Java类的引用类型静态变量
    • 方法区中常量引用的对象:字符串常量池(String Table)里的引用
    • 所有被同步锁synchronized持有的对象
    • Java虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的异常对象( 如: NullPointerException、OutOfMemoryError),系统加载类
    • 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
    • 除了这些固定的GCRoots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)

可达性分析算法必须在一个能保证一致性的快照中进行,因为你的一个对象当前可能是可达的,但是下一秒可能不可达,所以判断必须是某一个快照时刻去判断,现在的虚拟机常用的是可达性分析算法。

垃圾清除算法

当一个对象被判别为一个垃圾对象,如何清除的算法

  1. 标记-清除算法(Mark-Sweep)

    • 过程:首先标记所有需要回收的对象,然后统一回收
    • 优点:实现简单
    • 缺点:效率低,会产生大量内存碎片
  2. 标记-整理算法(Mark-Compact)

    • 过程:标记后将存活对象向一端移动,然后清理边界以外的内存
    • 优点:不会产生内存碎片
    • 缺点:移动对象需要更新引用,效率较低
  3. 复制算法(Copying)

    • 过程:将内存分为两块,只使用一块,当这块用完时,将存活对象复制到另一块
    • 优点:高效,不会产生内存碎片
    • 缺点:可用内存减少为原来的一半
  4. 分代收集算法(Generational Collection)

    • 过程:根据对象存活周期,将内存划分为不同的区域,根据各个区域特点使用不同算法
    • 优点:结合各种算法优点,提高回收效率
    • 缺点:实现复杂

垃圾回收器

1. Serial/Serial Old 回收器

  • 执行过程:
    • 新生代回收时,Eden区满触发Minor GC
    • 单线程执行,完全暂停应用线程(STW)
    • 老年代回收时,单线程执行Full GC
  • 优缺点:
    • 优点: 简单高效,单线程无线程切换开销
    • 缺点: 停顿时间长,不适合多核处理器

2. Parallel/Parallel Old 回收器

  • 执行过程:
    • 新生代回收时,多线程并行执行垃圾回收
    • 应用线程仍然完全暂停(STW)
    • 注重吞吐量,可通过参数控制最大停顿时间和吞吐量
  • 优缺点:
    • 优点: 充分利用多核CPU,提高吞吐量
    • 缺点: 仍有较长停顿时间,不适合需要低延迟的应用

配置参数

-XX:MaxGCpauseMillis,设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒

-XX:GCTimeRation,垃圾收集时间占总时间的比例( = 1 / (N + 1)),用于衡量的大小,取值范围(0,100),默认99,垃圾回收时间不超过1%

-XX:+UseAdaptiveSizePolicy,设置Parallel Scavenge收集器具有自适应调节策略

3. CMS 回收器

  • 执行过程:
    1. 初始标记(STW): 仅标记GC Roots能直接关联的对象
    2. 并发标记: 与用户线程并发执行,进行GC Roots追踪
    3. 重新标记(STW): 修正并发标记期间用户线程导致的变动
    4. 并发清除: 与用户线程并发执行,清除垃圾对象
  • 优缺点:
    • 优点: 并发收集,低停顿
    • 缺点:
      • CPU资源敏感
      • 无法处理浮动垃圾
      • 会产生内存碎片

4. G1 回收器

  • 特点

    • 应用于新生代和老年代,在JDK9之后默认使用G1
    • 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
    • 采用复制算法
    • 响应时间与吞吐量兼顾
    • 分成三个阶段:新生代回收(stw)、并发标记(重新标记stw)、混合收集
    • 如果并发失败(即回收速度赶不上创建新对象速度),会触发 FullGC
  • 执行过程:

    1. 初始标记(STW): 标记GC Roots直接关联对象
    2. 并发标记: 与用户线程并发执行
    3. 最终标记(STW): 处理并发阶段遗留的标记
    4. 筛选回收(STW): 对各个Region的回收价值排序,选择回收收益最大的Region
  • 优缺点:

    • 优点:
      • 可预测的停顿时间模型
      • 区域化分配与回收
      • 空间整合,不会产生大量碎片
    • 缺点:
      • 内存占用和额外执行负载比CMS高

总结

  1. 分代假设驱动堆划分

传统垃圾回收器(如Serial、Parallel Scavenge、CMS)基于分代假设(Generational Hypothesis)设计:

  • 年轻代:对象生命周期短(朝生夕死),适合复制算法(如Eden + Survivor区)。

  • 老年代:对象存活时间长,适合标记-清除标记-整理算法。

堆的分代划分(Eden/Survivor/老年代)直接服务于分代回收策略,不同代区使用不同的回收算法。

回收器算法堆结构分代模型并发/并行特点
Serial- 年轻代:标记-复制
- 老年代:标记-整理
固定分代:
年轻代(Eden + 2 Survivor) + 老年代
物理分代单线程(STW)简单高效,适用于客户端或小内存场景,单CPU环境下的Client模式
Parallel Scavenge- 年轻代:并行标记-复制
- 老年代:并行标记-整理
固定分代:
年轻代(Eden + 2 Survivor) + 老年代
物理分代多线程并行(STW)吞吐量优先,适合后台计算密集型而不需要太多交互的任务
CMS- 年轻代:并行标记-复制(ParNew)
- 老年代:并发标记-清除
固定分代:
年轻代(Eden + 2 Survivor) + 老年代
物理分代并发标记(部分阶段并发)低停顿老年代回收,但存在内存碎片和并发模式失败风险,集中在互联网站或B/S系统服务端上的Java应用
  1. 非分代堆结构

现代垃圾回收器(如G1、ZGC、Shenandoah)采用区域化堆设计,打破了传统分代模型的物理界限:

  • G1(Garbage-First):将堆划分为多个等大小Region(通常2MB~32MB),逻辑上仍分年轻代(Eden/Survivor Region)和老年代(Old Region),但物理上不固定。
  • ZGC/Shenandoah:彻底抛弃分代概念,将堆视为连续的内存块,通过着色指针或读屏障实现并发标记-整理。
回收器算法堆结构分代模型并发/并行特点
G1 (Garbage-First)- 分Region标记-整理
- 并发标记 + 增量回收
动态分区: 堆划分为多个等大小Region(2MB~32MB),逻辑分代(Eden/Survivor/Old)逻辑分代并发标记 + 并行回收平衡吞吐与延迟,可预测停顿时间,适合大堆内存,面向服务端应用,将来替换CMS
ZGC- 并发标记-整理
- 基于染色指针(Colored Pointers)和读屏障
连续堆内存,无物理分代无分代全阶段并发亚毫秒级停顿,适合超大堆(TB级),但需更高内存开销,适合大内存低延迟应用
Shenandoah- 并发标记-整理
- 基于转发指针(Brooks Pointer)和读屏障
连续堆内存,无物理分代无分代全阶段并发类似ZGC,但通过转发指针减少内存占用,适合中等规模堆。

项目中如何选择垃圾回收器:

  1. 根据机器情况判断,如果是单核机器,或者内存较小的机器,则选择Serial GC。
  2. 根据业务类型判断,看你的应用更在意的是吞吐量还是 STW 的时长。比如批处理任务的应用,更在意的就是吞吐量,而实时交易系统,更在意的就是 STW 的时长。
  3. 根据机器分配的堆内存大小进行判断,一把来说,我们认为至少达到4G 以上才可以用 G1、ZGC 等,通常要比如超过8G、16G 这样效果才更好。
  4. 根据 JDK 版本进行判断,不同的版本支持的垃圾收集器不一样。

GC评估指标:

  1. 吞吐量:程序的运行时间(程序的运行时间十内存回收的时间)
  2. 暂停时间(响应时间):执行垃圾收集时,程序的工作线程被暂停的时间
  3. 垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例。
  4. 收集频率:相对于应用程序的执行,收集操作发生的频率。
  5. 内存占用:Java堆区所占的内存大小。
  6. 快速:一个对象从诞生到被回收所经历的时间。

可以参考以下的选择方式(但是,并不绝对,尤其是 ZGC 和Shenandoah GC 的选择,其实还是要慎重,毕竟他们的稳定性各方面还有待验证):

一次完整的GC流程大致如下,基于JDK1.8

贡献者: mozzie