【JVM】常见的 Java 垃圾回收算法以及常见的垃圾回收器介绍及选型

一、概述

  1. 垃圾:运行程序中没有任何指针指向的对象,如果一个对象不被任何其他对象引用,则该对象属于垃圾,可以被标记为已死亡的对象
  2. 吞吐量:程序运行时间占总时间的比例,即 (程序运行时间)/(程序运行时间+GC时间)(程序运行时间)/(程序运行时间 + GC时间)(程序运行时间)/(程序运行时间+GC时间)
  3. 响应速度:衡量垃圾收集器的暂停时间(Stop-The-World duration),较短的暂停时间意味着更好的用户体验
  4. 工作流程
    1. 垃圾标记阶段:判断对象是否存活,只有被标记为己经死亡的对象,GC才会在执行垃圾回收时释放掉其所占用的内存空间
    2. 垃圾清除阶段:回收被标记为已死亡的对象

引用(强软弱虚)

  1. 强引用 (Strong Reference)
    • 最普遍的引用类型,就是我们平常创建对象时默认的引用类型
    • 只要强引用存在,垃圾收集器永远不会回收被引用的对象
  2. 软引用 (Soft Reference)
    • 用于描述还有用但非必需的对象
    • 当内存不足时,系统会回收软引用对象
    • 常用于实现内存敏感的高速缓存
  3. 弱引用 (Weak Reference)
    • 比软引用更弱的引用关系,被弱引用关联的对象只能生存到下次垃圾收集之前
    • 无论内存是否充足,都会被回收
    • 常用于避免内存泄漏
  4. 虚引用 (Phantom Reference)
    • 最弱的引用关系,随时都可能被回收
    • 不能通过虚引用获取对象实例
    • 主要用于跟踪对象被垃圾回收的状态

finalization 机制

  1. 功能:允许开发人员提供对象被销毁之前的自定义处理逻辑
  2. 执行时间:当垃圾回收器发现没有引用指向一个对象(垃圾回收此对象之前),总会先调用这个对象的 finalize() 方法

二、垃圾标记阶段

可达性分析算法(Java)

  1. 定义

    1. 可达性分析算法:Tracing Garbage Collection,也叫追踪性垃圾收集算法,或根搜索算法
    2. 引用链:Reference Chain,使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径即为引用链
    3. 根对象集合
      1. 定义:GCRoots,是一组必须活跃的引用(指针),是一个本身并不存放在堆内存里面,但是保存了堆内存里面的对象的指针

      2. 常见的 GCRoots

        GCRoots类型 说明 示例
        虚拟机栈引用 调用方法使用到的参数、局部变量等 方法中创建的对象、传入的参数对象
        本地方法栈引用 本地方法引用的对象 JNI方法中引用的Java对象
        方法区静态属性引用 类的引用类型静态变量 static修饰的对象引用
        方法区常量引用 字符串常量池里的引用 "Hello"等字符串常量
        同步锁引用 被Synchronized持有的对象 synchronized(obj)中的obj对象
        虚拟机内部引用 基本数据类型对应的Class对象,常驻异常对象 Integer.class, NullPointerException实例
        内部情况引用 回调函数、本地代码缓存 Thread实例、NIO Buffer等
  2. 算法思想

    1. 可达性判断:以 GCRoots 为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达
    2. 判断死亡:如果目标对象没有任何引用链可以追溯到 GCRoots,则是不可达的、己经死亡的,可以标记为垃圾对象
  3. 注意:如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行,否则分析结果无法保证准确性

引用计数算法(Python)

  1. 定义:引用计数算法(Reference Counting),是统计一个对象被引用次数,从而判断对象是否死亡的算法
  2. 算法思想
    1. 对每个对象保存一个整型的引用计数器属性,通过这个属性记录对象被引用的情况
    2. 对于一个对象 A,只要有任何一个对象引用了 A 则 A 的引用计数器 +1、当引用失效时引用计数器 -1
    3. 对象 A 的引用计数器的值为 0,即表示对象A不可能再被使用,可进行回收
  3. 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
  4. 缺点
    1. 空间开销:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
    2. 时间开销:每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销
    3. 循环引用问题:引用计数器无法处理循环引用的情况,这是一条致命缺陷,导致在 Java 的垃圾回收器中没有使用这类算法
  5. Python 针对内存泄漏的解决方案:通过使用弱引用 weakref,弱引用必回收,从而在合适的时机手动解除引用关系

三、垃圾清除阶段

标记清除算法

  1. 定义:Mark-Sweep 算法

  2. 算法思想

    1. 第一步:当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world)
    2. 第二步:标记,Collector 从引用根节点开始遍历,标记所有被引用的对象(一般是在对象的 Header 中记录为可达对象)
    3. 第二步:清除,Collector 对堆内存进行线性的遍历,如果发现某个对象在其 Header 中没有标记为可达对象,则将其回收
  3. 优点:实现简单

  4. 缺点

    1. 标记清除算法的效率不算高
    2. 在进行 GC 的时候,需要停止整个应用程序,用户体验较差
    3. 这种方式清理出来的空闲内存是不连续的,产生内碎片,需要维护一个空闲列表

三色标记算法

  1. 定义:**Tri-Color Marking,**优化了标记阶段的一种标记清除算法,Go 语言采用的 GC 算法
  2. 目标:STW → 异步标记,使得标记阶段可以分多次进行,通过 "三色标记" + "混合写屏障" 避免 STW,并且避免并发问题
  3. 功能:确保并行和并发垃圾回收能够在不同的线程之间协调运行,并避免在垃圾回收过程中发生 "标记丢失" 问题
  4. 适用场景:常用于并行和并发垃圾回收算法中
  5. 算法思想
    1. 初始阶段:从 GC Roots 开始标记。GC Roots 中的对象会被标记为灰色
    2. 标记阶段:遍历灰色对象,逐一检查它们引用的对象,将这些对象标记为灰色,并将当前的灰色对象标记为黑色,这个过程通过遍历引用链,直到所有可达对象都被标记为黑色
    3. 结束阶段:所有可达对象都被标记为黑色,标记阶段完成,白色对象可以被回收
  6. 不同颜色的对象含义
    • 白色:表示对象未被标记,认为该对象是垃圾,可以被回收
    • 灰色:表示对象已经被标记,但它引用的对象尚未完全处理(即这些对象的子对象仍然是白色的)
    • 黑色:表示对象已经标记并且它引用的对象也已经被标记(即这些对象的子对象也是黑色的,所有可达对象已被标记)

标记丢失问题

  1. 定义:应用程序线程(Mutator)和垃圾回收器线程(Collector)同时运行时,可能导致某些本应被标记为"存活"的对象被错误地判定为"可回收",从而引发的内存泄漏或程序错误
  2. 产生条件(两个条件同时满足)
    1. 条件一:应用程序线程(Mutator)令 "黑色对象" 引用 "某个白色对象"
    2. 条件二:应用程序线程(Mutator)破坏 "所有灰色对象" 对这个白色对象的引用
  3. 解决方式
    1. 强三色不变式:黑色对象不允许引用白色对象(目标是破坏条件一)

    2. 弱三色不变式:黑色对象只可以引用被灰色对象 "引用" 或 "间接引用" 的白色对象(目标是破坏条件二)

混合写屏障

| ⭐ 参考视频:【[Golang中GC回收机制三色标记与混合写屏障](https://www.bilibili.com/video/BV1wz4y1y7Kd?p=7\&vd_source=acb335dc7d2cad073b4d68c9815bc2d)】

  1. 定义:Mixed Write Barrier,一种用于 G1 垃圾回收器、针对特定类型的写屏障,用来捕获对象引用更新的机制
  2. 功能:帮助跟踪在垃圾回收过程中修改了对象的引用,确保不同代之间的引用在垃圾回收时不会被错误地丢弃或忽略,并且尽可能的减少 STW 的时间
  3. 注意:混合写屏障不能完全避免 STW,但是只会对栈空间进行短暂的 STW,并且由于栈中数据量比堆中少很多,所以这种优化是有效且值得的
  4. 插入屏障
    1. 定义:对象被引用时触发的机制
    2. 算法思想:A 对象引用 B 对象的时候,B 对象必须被标记为灰色
    3. 缺点:只会对堆空间的对象执行插入屏障,对于栈空间还是需要 STW(为了保证执行速度)
  5. 删除屏障
    1. 定义:在删除对象时触发的机制
    2. 算法思想:A 对象取消对 B 对象的引用的时候,B 对象必须被标记为灰色
    3. 缺点:回收精度低,一个对象即使没有被引用了,也可以再多活一轮,在下一轮 GC 中才会被删除
  6. 混合写屏障
    • 算法思想
      • GC 开始时,将栈上的对象全部扫描并标记为黑色
      • GC 期间,任何在栈上创建的新对象均标记为黑色
      • 被删除和被添加的对象都标记为灰色

复制算法

  1. 定义:Copying 算法

  2. 算法思想

    1. 第一步:分区,将活着的内存空间分为两块,每次只使用其中一块
    2. 第二步:复制,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象
    3. 第三步:角色交换,交换两个内存块的角色,等待下一次垃圾清除
  3. 优点

    1. 没有标记和清除过程,实现简单,运行高效
    2. 复制过去以后保证空间的连续性,不会出现"碎片"问题
  4. 缺点

    1. 需要两倍的内存空间
    2. 复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,内存占用和时间开销都不小
    3. 如果系统中的垃圾对象很少,复制算法需要复制的存活对象数量就会很大,效率很低(因此更适用于新生代)
  5. 应用场景:存活对象少、垃圾对象多的场景,比如朝生夕死的伊甸园区和 S0 区 S1 区

  6. 图示

标记-压缩算法

  1. 定义:Mark-Compact 算法,或 Mark-Sweep-Compact 算法
  2. 算法思想
    1. 第一步:从根节点开始标记所有被引用对象
    2. 第二步:将所有的存活对象压缩到内存的一端,按顺序排放
    3. 第三步:清理边界外所有的空间
  3. 图示

分代收集算法

  1. 算法思想:不同生命周期的对象采取不同的收集方式,以便提高回收效率
  2. 实现方式:把 Java 堆分为新生代和老年代,根据各个年代的特点使用不同的回收算法
    1. 年轻代:采用复制算法,内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解
    2. 老年代:采用 Mark-Sweep 或者是 Mark-Sweep 与 Mark-Compact 的混合实现

增量收集算法

  1. 定义:Incremental Collecting 算法,基于传统的 Mark-Sweep 和 Copying 算法
  2. 目标:减少 Stop the World 的状态,避免严重影响用户体验或者系统的稳定性
  3. 算法思想
    1. 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行
    2. 每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复直到完成垃圾收集
  4. 缺点:线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降

分区算法

  1. 目标:控制 GC 产生的停顿时间
  2. 算法思想:将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿

总结

Mark-Sweep Mark-Compact Copying
速率 中等 最慢 最快
空间开销 少(但会堆积碎片) 少(不堆积碎片) 通常需要活对象的2倍空间(不堆积碎片)
移动对象
  1. 特点
    1. Mark阶段的开销与存活对象的数量成正比
    2. Sweep阶段的开销与所管理区域的大小成正相关
    3. Compact阶段的开销与存活对象的数据成正比

四、GC 发生时间

  1. 安全点(Safe Point)
    1. 定义:垃圾收集器可以安全地进行垃圾回收操作的特定位置
    2. 中断方式:设置一个中断标志,各个线程运行到 Safe Point 的时候主动轮询这个标志,如果中断标志为真则进行中断挂起。(有轮询的机制)
  2. 安全区域(Safe Region)

五、经典垃圾收集器

  1. 年轻代收集器

    1. Serial GC
    2. ParNew GC
    3. Parallel Scavenge GC
  2. 老年代收集器

    1. CMS GC
    2. Serial Old GC
    3. Parallel Old GC
  3. 组合关系

  4. 选择标准

    1. 想要最小化地使用内存和并行开销,请选 Serial GC
    2. 想要最大化应用程序的吞吐量,请选 Parallel GC
    3. 想要最小化 GC 的中断或停顿时间,请选 CMS GC
    4. 内存小选择 CMS,内存大选择 G1,平衡点在 6-8G 之间

G1 垃圾收集器

  1. 目标:在延迟可控的情况下获得尽可能高的吞吐量

  2. 相关概念

    1. Region:最小回收单位,即分块后的分代内存区
    2. RSet:Remembered Set,每个 Region 都拥有自己的 RSet,用于记录其他 Region 对当前 Region 中的对象的引用,垃圾回收时作为 GCRoots
    3. 回收集:Collection Set,存放需要被回收的内存分段的集合
  3. 回收过程

    1. 年轻代GC(Young GC)

      1. 第一阶段:标记 "GC Roots 的直接可达对象" 和 "RSet 的跨代引用对象" 为存活对象
      2. 第二阶段:标记存活对象,通过可达性分析和已标记的存活对象,进一步标记 Eden 和 Survivor 中所有存活对象
      3. 第三阶段:复制对象,将存活的对象复制到 Survivor 区中空的内存分段
        • Survivor 区内存段中存活的对象年龄未达阈值,则年龄 +1
        • Survivor 区内存段中存活的对象年龄达到阈值,则复制到 Old 区中空的内存分段
        • 如果 Survivor 空间不够,则 Eden 空间的部分数据会直接晋升到老年代空间
      4. 第四阶段:最终 Eden 空间的数据为空,本次 Young GC 完成
    2. 老年代并发标记过程(Concurrent Marking)

      1. 初始标记阶段:STW 阶段,标记从根节点直接可达的对象(会触发一次 Young GC)
      2. 根区域扫描阶段:G1 GC 扫描 Survivor 区直接可达的老年代区域对象,并标记被引用的对象(在 YoungGC 之前完成)
      3. 并发标记阶段:在整个堆中进行并发标记(和应用程序并发执行),此过程可能被 YoungGC 中断
        • 如果区域对象中的所有对象都是垃圾,那这个区域会被立即回收
        • 计算每个区域的对象活性(区域中存活对象的比例),用于排序回收的优先级
      4. 再次标记阶段:STW 阶段,由于应用程序持续进行,需要修正上一次的标记结果
      5. 独占清理阶段:STW 阶段,计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域,为下阶段做铺垫
      6. 并发清理阶段:识别并清理完全空闲的区域
    3. 混合回收(Mixed GC)

      1. 老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收,G1 通过活性排序,优先回收垃圾多的内存分段
      2. 回收前先对比阈值来决定是否回收内存分段(通过 -XX:G1MixedGCLiveThresholdPercent 设定,默认为65%)
      3. 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段
      4. 混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程
      5. 混合回收并不一定要进行8次,G1 允许整个堆内存中有一定的空间被浪费(一个阈值-XX:G1HeapWastePercent,默认值为10%),意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收
  4. 优点

    1. 并行性:G1 在回收期间,可以有多个 GC 线程同时工作,有效利用多核计算能力(用户线程会 STW)
    2. 并发性:G1 拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般不会在整个回收阶段完全阻塞应用程序
    3. 分代收集:堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代
    4. 灵活内存分配:不要求整个 Eden 区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
    5. 避免碎片化:Region 之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片
  5. 适用场景

    1. 面向服务端应用,针对具有大内存、多处理器的机器(在普通大小的堆里表现并不惊喜)
    2. 需要低 GC 延迟,并具有大堆的应用程序(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会过长)
    3. 用来替换掉 JDK1.5 中的 CMS 收集器;在下面的情况时,使用 G1 可能比 CMS 好:
      • 超过 50% 的 Java 堆被活动数据占用
      • 对象分配频率或年代提升频率变化很大
      • GC停顿时间过长(长于0.5至1秒)
  6. 混合写屏障

    1. 定义:Mixed Write Barrier,一种用于 G1 垃圾回收器、针对特定类型的写屏障,用来捕获对象引用更新的机制
    2. 功能:帮助跟踪在垃圾回收过程中修改了对象的引用,确保不同代之间的引用在垃圾回收时不会被错误地丢弃或忽略
      • 跨代引用处理:如果一个对象的引用被修改,且引用指向了其他代的对象(比如,引用从 Young 区指向了 Old 区),写屏障会将这个引用记录下来,防止后续的垃圾回收阶段丢失或误处理这些引用
      • 跟踪对象的变化:当在堆中修改对象引用时,写屏障会实时地记录这些修改,确保垃圾回收在处理存活对象时,能够根据实际的引用情况准确地进行标记和回收

ZGC 垃圾收集器

  1. 目标:在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停颇时间限制在十毫秒以内的低延迟
  2. 定义:一款基于 Region 内存布局的、不设分代的(暂时)、使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-压缩算法的、以低延迟为首要目标的一款垃圾收集器
  3. 工作过程:并发标记 → 并发预备重分配 → 并发重分配 → 并发重映射
  4. 优点:不论是平均停顿、95%停顿、99%停顿、99.9%停顿,还是最大停顿时间,ZGC都能毫不费劲控制在10毫秒以内

总结

  1. 截止JDK1.8,一共有7款不同的垃圾收集器。每一款的垃圾收集器都有不同的特点,需要在具体场景根据具体的情况选用不同的垃圾收集器

    垃圾收集器 分类 作用位置 使用算法 特点 适用场景
    Serial 串行运行 作用于新生代 复制算法 响应速度优先 适用于单CPU环境下的client模式
    ParNew 并行运行 作用于新生代 复制算法 响应速度优先 多CPU环境Server模式下与CMS配合使用
    Parallel 并行运行 作用于新生代 复制算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
    Serial Old 串行运行 作用于老年代 标记-压缩算法 响应速度优先 适用于单CPU环境下的Client模式
    Parallel Old 并行运行 作用于老年代 标记-压缩算法 吞吐量优先 适用于后台运算而不需要太多交互的场景
    CMS 并发运行 作用于老年代 标记-清除算法 响应速度优先 适用于互联网或B/S业务
    G1 并发、并行运行 作用于新生代、老年代 标记-压缩算法、复制算法 响应速度优先 面向服务端应用
  2. GC发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC

  3. 怎么选择垃圾收集器?

    1. 优先调整堆的大小让JVM自适应完成
    2. 如果内存小于100M,使用串行收集器
    3. 如果是单核、单机程序,并且没有停顿时间的要求,串行收集器
    4. 如果是多CPU、需要高吞吐量、允许停顿时间超过1秒,选择并行或者JVM自己选择
    5. 如果是多CPU、追求低停顿时间,需快速响应(比如延迟不能超过1秒,如互联网应用),使用并发收集器官方推荐G1,性能高,现在互联网的项目,基本都是使用G1
  4. 注意

    1. 没有最好的收集器,更没有万能的收集
    2. 调优永远是针对特定场景、特定需求,不存在一劳永逸的收集器

GC 日志

  1. ParallelGC

  2. FullGC


参数配置

  1. 基本配置

    参数 说明 默认值 注意事项
    XX:+UseG1GC 指定使用G1垃圾收集器 - 推荐使用的垃圾收集器
    XX:MaxGCPauseMillis 期望达到的最大GC停顿时间 200ms JVM会尽力实现,但不保证达到
    XX:G1HeapRegionSize 设置每个Region的大小 堆内存的1/2000 值是2的幂,范围1MB到32MB
    XX:+ParallelGCThread STW工作线程数 - 最多设置为8
    XX:ConcGCThreads 并发标记线程数 - 建议设为ParallelGCThreads的1/4
    XX:InitiatingHeapOccupancyPercent 触发并发GC周期的堆占用率阈值 45% 超过此值触发GC
  2. GC 日志配置

    参数 说明 默认值 注意事项
    XX:+PrintGC 输出GC日志 - 类似verbose:gc
    XX:+PrintGCDetails 输出GC详细日志 - 提供更详细的GC信息
    XX:+PrintGCTimestamps 输出GC时间戳 - 基准时间形式
    XX:+PrintGCDatestamps 输出GC日期时间戳 - 日期形式显示
    XX:+PrintHeapAtGC 打印GC前后堆信息 - 用于分析堆使用情况
    Xloggc GC日志输出路径 - 需指定完整路径
  3. 选择规则

    • 优先让JVM自适应,调整堆的大小
    • 串行收集器:内存小于100M;单核、单机程序,并且没有停顿时间的要求
    • 并行收集器:多CPU、高吞吐量、允许停顿时间超过1秒
    • 并发收集器:多CPU、追求低停顿时间、快速响应(比如延迟不能超过1秒,如互联网应用)
    • 官方推荐G1,性能高。现在互联网的项目,基本都是使用G1
相关推荐
虾饺爱下棋11 分钟前
FCN语义分割算法原理与实战
人工智能·python·神经网络·算法
fouryears_234171 小时前
适配器模式——以springboot为例
java·spring boot·适配器模式
Y第五个季节1 小时前
JVM-GC 相关知识
jvm
汽车功能安全啊2 小时前
利用对称算法及非对称算法实现安全启动
java·开发语言·安全
paopaokaka_luck3 小时前
基于Spring Boot+Vue的吉他社团系统设计和实现(协同过滤算法)
java·vue.js·spring boot·后端·spring
Eloudy4 小时前
简明量子态密度矩阵理论知识点总结
算法·量子力学
点云SLAM4 小时前
Eigen 中矩阵的拼接(Concatenation)与 分块(Block Access)操作使用详解和示例演示
人工智能·线性代数·算法·矩阵·eigen数学工具库·矩阵分块操作·矩阵拼接操作
Warren984 小时前
Java Stream流的使用
java·开发语言·windows·spring boot·后端·python·硬件工程
算法_小学生5 小时前
支持向量机(SVM)完整解析:原理 + 推导 + 核方法 + 实战
算法·机器学习·支持向量机
架构师沉默5 小时前
Java优雅使用Spring Boot+MQTT推送与订阅
java·开发语言·spring boot