1垃圾收集器和内存分配策略

垃圾收集器与内存分配策略

对象存活的条件

  • 对象的存活条件取决于其是否被可达性分析(Reachability Analysis)判定为“可达”
  • JVM通过GC Roots作为起点,遍历对象引用链。若某个对象能被GC Roots直接或间接引用,则视为存活;否则,将被标记为可回收。

引用的概念

  • 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。引用不等同于对象本身,根据虚拟机种类的不同,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置。

引用计数算法

  • 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时刻计数器为0的对象就是不可能在被使用的。
  • 对象之间相互循环引用的问题无法解决

引用的分类

  • 1.2后对引用的概念进行了扩充

强引用(Strong Reference)

  • 类似(Object object = new Object()),只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

软引用(Soft Reference)

  • 用来描述还有用但并非必需对象,在系统见要发生内存溢出之前,将会把这些对象列进回收范围之中进行二次回收。进行二次回收之后还没有足够的内存,才会抛出内存溢出异常。
  • Srping配置文件,只有在启动时加载,所以使用软引用

弱引用(Weak Reference)

  • 用来描述非必需对象的,被弱引用关联的对象只能存活到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否够用,都会回收掉只被弱引用关联的对象。

虚引用(Phanton Reference)

  • 幽灵应用或幻影引用,一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一作用就是能在这个对象被收集器回收时收到一个系统通知。
  • 虚引用通常用来跟踪对象被垃圾回收的活动,虚引用的存在意义就是监控对象是否存活。

可达性分析算法

  • 通过一系列的称为”GC Roots”的对象作为起始点。从这些结点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则此对象为不可用
  • GC Roots举例:
    • 线程栈帧中的局部变量(Java方法栈、Native方法栈中的对象引用)。
    • 类的静态变量(如public static Object obj;)。
    • JNI(Java Native Interface)引用(本地代码持有的对象)。
    • 已启动且未终止的线程对象Thread实例)。
    • 系统类加载器加载的类(如java.lang.Class对象)。
    • 被同步锁(Monitor)持有的对象(如synchronized锁对象)。

可作为GC Roots对象的条件

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

宣告对象死亡的两次标记

  • 两次标记和一次筛选

两次标志和一次筛选

finalize()方法

  • 被放在F-Queue队列的对象的finalize(),虚拟机只会触发这个方法,但并不承诺会等待它运行结束。
  • 任何一个对象的finalize方法只会被系统自动调用一次。
  • 该方法不建议使用,不确定性太大。

永生代的无用的类的回收

  • 回收条件
    • 该类所有的实例已经被回收,Java堆中不存在该类的任何实例
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
  • 虚拟机可以对满足上述三个条件的类进行回收,但不是必然的。

JVM内存分代模型

  • 内存分代模型是基于对象生命周期差异设计的一种内存管理策略,旨在优化垃圾回收(GC)效率
  • JVM堆内存主要划分为年轻代(Young Generation)老年代(Old Generation),以及元空间(Metaspace,方法区的实现)

年轻代

  • 新创建的对象都会被分配到Eden区(如果该对象占用内存非常大,则直接分配到老年代区), 当Eden区内存不够的时候就会触发MinorGC(Survivor满不会引发MinorGC,而是将对象移动到老年代中)
  • 新生代:1/3的堆内存空间
    • eden区: 8/10 的新生代空间
    • survivor0:1/10的新生代空间
    • servivor1:1/10的新生代空间
  • 线程本地分配TLAB
    • 占用eden,默认1%
    • 多线程的时候不用竞争eden就可以申请空间,提高效率
    • 小对象
    • 无需调整
  • 一般使用复制算法

老年代

  • 一般使用标记清除(回收)算法或者标记压缩算法
  • 对象在Survivor区中每熬过一次Minor GC,年龄就会+1岁,当年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置,默认是15)的对象会被移动到年老代中
  • 虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
  • 如果该对象占用内存非常大,则直接分配到老年代区

元空间(Metaspace,方法区)

  • 元空间(Metaspace) 是用于存储类元数据的内存区域,取代了早期的永久代(PermGen)
  • 存储内容
    • 类结构信息(如类名、方法、字段、访问修饰符)
    • 方法字节码
    • 常量池(Constant Pool)
    • 注解信息
    • 类加载器引用等
  • 使用本地内存(Native Memory)而非JVM堆内存,其大小仅受操作系统可用内存限制。
  • 特性 永久代(PermGen) 元空间(Metaspace)
    内存位置 JVM堆内存 本地内存(Native Memory)
    大小限制 固定(需手动设置-XX:MaxPermSize 默认无上限(可设-XX:MaxMetaspaceSize
    垃圾回收机制 Full GC时回收,效率低 独立GC周期,更高效
    内存碎片问题 容易产生碎片 分块(Chunk)管理,减少碎片
    类卸载 困难,易内存泄漏 支持高效卸载未使用的类
  • 元空间的应用场景
    • 动态类加载
      如OSGi框架、热部署(HotSwap)场景,需频繁加载/卸载类。
    • 反射与动态代理
      生成代理类(如Spring AOP、Java动态代理)时,元空间高效管理类元数据。
    • 大型应用
      微服务架构中多个应用共享JVM时,合理分配元空间防止冲突。

对象分配过程图

  • 对象分配过程详解

垃圾收集算法

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

  • 一般作用于老年代

  • 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

  • 不足

    • 效率问题
    • 空间问题:会产生大量不连续的空间碎片。分配较大对象时,可能无法找到连续的内存而不得不提前触发另一次垃圾收集

复制算法(Copying)

  • 一般作用于年轻代

  • 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面。然后再把已使用过的内存空间一次清理掉。

  • 将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和刚才使用过的Survivor还存活的对象一次性复制到另外一个Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。比例是Eden:Survivor0:Survivor = 8:1:1,但是无法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(老年代)进行分配担保(Handle Promotion)

  • 分配担保

    • 先检查老年代的可用空间是否能够兜底,有以下两种方案
      • 乐观策略:老年代的最大连续可用空间 > 新生代所有对象总大小(包括存活对象和未回收的垃圾)
        • 若成立:认为Minor GC是安全的,直接触发Minor GC。
        • 若不成立:进一步判断是否允许担保失败(需启用-XX:+HandlePromotionFailure参数,但该参数在JDK 6之后已逐渐废弃)。
          • 若允许,则继续检查老年代最大连续空间是否 > 历次晋升到老年代对象的平均大小
            • 是:尝试Minor GC(但存在风险,可能导致Full GC)。
            • 否:直接触发Full GC(回收整个堆)。
      • 保守策略:若未启用担保失败(或老年代空间不足),直接触发Full GC,确保老年代有足够空间接收晋升对象。

标记整理算法(Mark-Compact)

  • 一般作用于老年代

HotSpot

  • 最知名的JVM实现,核心设计思想:通过动态监控识别代码中的“热点”(频繁执行的代码段)并对其进行深度优化

枚举根节点

  • 枚举根节点(Enumeration of GC Roots)是垃圾回收(Garbage Collection, GC)过程中可达性分析(Reachability Analysis)的关键步骤,其目的是快速、准确地找出所有GC Roots对象。
  • 枚举根节点时,会发生Stop The Word
  • HotSpot通过OopMap的数据结构来确定哪些地方存放着对象引用,在类加载完成的时候,HotSpot就把对象内什么偏移量时什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置(安全点)记录下栈和寄存器中那些地方是引用。
  • OopMap是一种空间换时间的策略,当JVM到达安全点时,会更新自己的OopMap,记录哪些栈中代表着内存引用,枚举根节点时,从OopMap中拿取引用,这比查找内存先判断是否是一个引用要快很多
  • 枚举根节点是垃圾回收的基石,通过OopMap和安全点机制,实现了对GC Roots的高效定位,确保可达性分析的准确性与性能。这一过程的优化直接关系到GC的停顿时间,是JVM低延迟设计的关键环节。理解其原理有助于开发者在代码中避免创建不必要的根引用,从而提升垃圾回收效率。

安全点

  • 程序执行时并非在所有地方都能停顿进行GC,只有到达安全点才可以
  • 安全点的选定标准
    • 是否可以让程序长时间执行的特征。
    • 长时间执行的特征:方法调用、循环跳转、异常跳转。
  • 所有线程都跑到安全点的方案
    • 抢先式中断
      • 在GC发生时,先把所有线程中断。当线程中断的地方不在安全点上时,恢复线程,让它跑到安全点上
    • 主动式中断
      • GC需要中断线程时,不直接对线程进行操作,而是设置一个标志。各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,再加上创建对象需要分配内存的地方。

安全区域

  • 安全区域指在一段代码片段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的。
  • safePoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的SafePoint,但是程序不执行的情况没有考虑(程序不执行就是没有分配CPU时间,典型例子就是线程处于Sleep状态或者Blocked状态)
  • 当线程执行到Safe Region中的代码时,首先标识自己已经进入到了sqfe Region,当在这段时间里JVM要发起GC时,就可以忽略SafePoint的标识状态。当线程要离开SafeRegion时,要检查系统是否已经完成了根节点枚举(或者整个GC过程),直到收到可以安全离开的信号才可以离开。

垃圾收集器

  • 可以搭配使用

    • Serial/Serial Old
    • Serial/CMS
    • ParNew/Serial Old
    • ParNew/CMS
    • Parallel Scavenge/Serial Old
    • Parallel Scavenge/Parallel Old
  • 其中Serial Old作为CMS出现”Concurrent Mode Failure”失败的后备预案

Serial收集器

  • 特征

    • 单线程收集器,进行垃圾收集时,必须暂停所有用户线程。
    • 在Client模式下的默认新生代收集器
  • 优点

    • 简单高效(与其他收集器的单线程相比)

ParNew收集器(Parallel New)

  • 特征

    • Serial的多线程版本
    • 在Server模式下首选的新生代收集器
    • 除了Serial,只有它可以与CMS收集器配合
    • 单cpu下,不会比Serial有更好的效果,由于线程切换的开销

Parallel Scavenge收集器

  • 特征
    • 新生代收集器,与ParNew很相似
    • 主要关注点在达到一个可控制的吞吐量,吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量优先收集器
    • 可以通过参数来使虚拟机自动控制堆中新生代和老年代的比例及大小(自适应策略)。

Serial Old收集器

  • 特征

    • Serial收集器的老年代版本
    • 给Client模式下的虚拟机使用
  • 两大用途

    • 在JDK1.5以前与Parallel Scavenge搭配使用
    • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用

Parallel Old收集器

  • 特征

    • Parallel Scavenge收集器的老年代版本
    • Parallel Scavenge配合,适用于注重吞吐量及CPU资源敏感的场合

CMS收集器(Concurrent Mark Sweep)

  • 并发低停顿收集器,以实现最短 STW 时间为目标的收集器

  • 收集步骤

    • 初始标记(CMS inital mark)————-stop the word
      • 标记GC Roots能直接关联到的对象,速度很快

      • 并发标记(CMS concurrent mark)——-与用户线程并发执行

        • 进行GC Roots Tracing过程
      • 重新标记(CMS mark)——————-stop the word

        • 修正并发标记期间因用户程序继续运行而导致标记变动的那一部分对象的标记记录
    • 并发清除(CMS concurrent sweep)—–与用户线程并发执行
      • 并发清除死亡的对象
  • 缺点

    • CPU资源敏感
      • 默认启动的回收线程数是 (CPU数量+3)/4

        • 隐含:当处理器核心不足4个时,CMS对用户程序的印象可能会很大
      • 无法处理浮动垃圾,可能出现Concurrent Mode Failure

        • 浮动垃圾:并发清理过程中产生的垃圾。
        • 基于以上原因,无法等到老年代被填满在进行垃圾清理,需要预留空间给浮动垃圾。1.6后,是92%,预留8%的空间
        • 预留内存无法满足需要会启用Serial Old来重新进行老年代的垃圾收集
    • CMS 采用的是标记清除法,上文我们已经提到这种方法会产生大量的内存碎片,这样会给大内存分配带来很大的麻烦,如果无法找到足够大的连续空间来分配对象,将会触发 Full GC,这会影响应用的性能。当然我们可以开启 -XX:+UseCMSCompactAtFullCollection(默认是开启的),用于在 CMS 收集器顶不住要进行 FullGC 时开启内存碎片的合并整理过程,内存整理会导致 STW,停顿时间会变长,还可以用另一个参数 -XX:CMSFullGCsBeforeCompation 用来设置执行多少次不压缩的 Full GC 后跟着带来一次带压缩的。
  • ParNew + CMS如何让系统基本不产生FGC?

    • 加大JVM内存
    • 加大Young的比例
    • 提高Y-O的年龄
    • 提高S区比例
    • 避免代码内存泄漏
  • CMS用到的的垃圾回收算法

    • 三色标记 + Incremental Update
  • CMS日志

    • 执行命令:java -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC com.mashibing.jvm.gc.T15_FullGC_Problem01
    • [GC (Allocation Failure) [ParNew: 6144K->640K(6144K), 0.0265885 secs] 6585K->2770K(19840K), 0.0268035 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]

      ParNew:年轻代收集器
      6144->640:收集前后的对比

    (6144):整个年轻代容量

6585 -> 2770:整个堆的情况
(19840):整个堆大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[GC (CMS Initial Mark) [1 CMS-initial-mark: 8511K(13696K)] 9866K(19840K), 0.0040321 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
//8511 (13696) : 老年代使用(最大)
//9866 (19840) : 整个堆使用(最大)
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.018/0.018 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
//这里的时间意义不大,因为是并发执行*
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//标记Card为Dirty,也称为Card Marking*
[GC (CMS Final Remark) [YG occupancy: 1597 K (6144 K)][Rescan (parallel) , 0.0008396 secs][weak refs processing, 0.0000138 secs][class unloading, 0.0005404 secs][scrub symbol table, 0.0006169 secs][scrub string table, 0.0004903 secs][1 CMS-remark: 8511K(13696K)] 10108K(19840K), 0.0039567 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
//STW阶段,YG occupancy:年轻代占用及容量
//[Rescan (parallel):STW下的存活对象标记
//weak refs processing: 弱引用处理
//class unloading: 卸载用不到的class
//scrub symbol(string) table:
//cleaning up symbol and string tables which hold class-level metadata and
//internalized string respectively
//CMS-remark: 8511K(13696K): 阶段过后的老年代占用及容量
//10108K(19840K): 阶段过后的堆占用及容量
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
*//标记已经完成,进行并发清理
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
*//重置内部结构,为下次GC做准备

三色标记算法

  • 三色标记算法(Tri-color Marking) 是一种用于垃圾回收(GC)过程中追踪对象存活状态的核心算法,尤其在并发标记阶段(Concurrent Marking)中发挥关键作用。它的核心目标是通过颜色标记对象的状态,高效且安全地识别存活对象,同时支持垃圾回收器与应用程序线程的并发执行,减少停顿时间(STW)。
  • 颜色分类
    • 白色:表示对象没有被垃圾收集器访问过,在可达性分析的开始阶段,所有的对象都是白色的,若在分析结束阶段仍是白色的,则说明不可达
    • 灰色:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用没有被扫描过
    • 黑色:表示对象已经被垃圾收集旗访问过,且这个对象的所有引用都已经扫描过,它是安全存活的,如果有其它对象引用指向了黑色对象,无须重新扫描一遍,黑色对象不可能直接指向某个白色对象
  • 漏标的两种情况
    1. 在remark过程中,黑色指向了白色,如果不对黑色对象进行重新扫描,则会漏标会把白色D对象当做没有新引用指向从而回收掉。
    2. 并发标记过程中,删除了所有从灰色到白色的引用,会引起漏标,此时白色对象应该被回收
    • 漏标解决方案
      • Incremental Update 增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性
      • STAB snapshot at begining(原始快照):关注引用的删除,当灰色到白色的引用消失时,要把引用推到GC的堆栈,保证还能被GC扫描到。

G1 收集器(Garbage-First)

  • 关键概念

    • region:
      • region是把连续的Java堆划分为多大大小相等的独立区域
      • 每一个region都可以根据需要,扮演新生代的Edoen空间、Survivor空间或者老年代空间。收集器对扮演不同角色的Region采用不同的策略去处理
      • 大小通过参数可以设定,范围是1MB到32MB,且应为2的N次幂
      • 内存回收的最小单元,优先回收垃圾最多的Region(“Garbage-First”名称的由来)
      • 有两个TAMS指针,用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须在这两个指针位置上
      • 每个Region上都维护有一份卡表,导致内存占用比其他收集器较高,6到8GB以上在考虑使用G1
    • humongous区域
      • 专门用来存储大对象,只要大小超过了一个Region容量一半的对象即判定为大对象
  • 详细步骤

    • 初始标记(Initial Marking)——————-stop the word
      • 标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start:下一个开始处的端点,为了让下一阶段的并发执行时,能正确的在可用的region中分配新对象)的值,
    • 并发标记(Concurrent Marking)————-与用户线程并发执行
      • 是从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行
    • 最终标记(Final Marking)——————stop the word
      • 是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,(处理少量的STAB记录)
    • 筛选回收(Live Data Counting and Evacuation)—-STW
      • 首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划
  • 特征

    • 并发与并行
    • 分代收集
    • 空间整合
      • 整体是标记-清理算法,局部(两个Region之间)基于“复制”算法实现的
    • 可预测、可控的停顿。用户可以指定期望停顿时间,G1 会将停顿时间控制在用户设定的停顿时间以内。
    • 将java堆分为多个大小相等的Region
      • Regin之间的对象引用以及其他收集器中的新生代与老年代的对象引用,虚拟机都是使用Remembered Set来避免全推扫描的。
CSet和RSet
  • Collection Set
    • 一组可被回收的分区的集合
    • 在CSet中存活的数据会在GC过程中被转移到另一个可用分区,CSet中的分区可以来自Eden空间、survivor空间或者老年代。
    • CSet会占用不到整个堆空间的1%大小
  • Remember Set
    • 记录了其他region中的对象到本Region的引用
    • RSet的价值在于使得垃圾收集器不需要扫描整个堆来确认谁引用了当前分区的对象,只需要扫描Rest即可。
G1中的MixedGC
  • 相当于CMS
  • XX:InitiatingHeapOccupacyPercent
    • 默认值45%, 对象占用堆的百分比
    • 当O超出这个值时,启动MixedGC
G1使用的算法
  • 三色标记 + SATB
G1日志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[GC pause (G1 Evacuation Pause) (young) (initial-mark), 0.0015790 secs]
//young -> 年轻代 Evacuation-> 复制存活对象
//initial-mark mixdGC阶段,混合回收的阶段,这里是YGC混合老年代回收
[Parallel Time: 1.5 ms, GC Workers: 1] //一个GC线程
[GC Worker Start (ms): 92635.7]
[Ext Root Scanning (ms): 1.1]
[Update RS (ms): 0.0]
//更新了多少remember Set
[Processed Buffers: 1]
[Scan RS (ms): 0.0]
[Code Root Scanning (ms): 0.0]
[Object Copy (ms): 0.1]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 1.2]
[GC Worker End (ms): 92636.9]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
//清理CardTable
[Clear CT: 0.0 ms]
[Other: 0.1 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.0 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)]
[Times: user=0.00 sys=0.00, real=0.00 secs]
//以下是混合回收其他阶段
[GC concurrent-root-region-scan-start]
[GC concurrent-root-region-scan-end, 0.0000078 secs]
[GC concurrent-mark-start]
//无法evacuation,进行FGC
[Full GC (Allocation Failure) 18M->18M(20M), 0.0719656 secs]
[Eden: 0.0B(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->0.0B Heap: 18.8M(20.0M)->18.8M(20.0M)], [Metaspace: 38
76K->3876K(1056768K)] [Times: user=0.07 sys=0.00, real=0.07 secs]
G1产生FGC的解决方案
  • G1的FGC在Java 10 以前是串行的,10以后是并行的
  1. 扩内存
  2. 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
  3. 降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)

Shenandoah

  • 非官方的HotSpot垃圾收集器

  • G1的升级版本,也是基于Region布局

  • 与G1的不同之处

    • 支持并发的整理算法(多线程并行但不能与用户线程并行
    • 默认不适用分代收集
    • 去除了收集集,改用名为 连接矩阵(Connection Matrix)来记录region的引用关系
  • 过程

    • Shenandoah收集器的链接矩阵示意图

    • 初始标记(Initial Marking) : 与G1一样, 首先标记与GC Roots直接关联的对象, 这个阶段仍是“Stop The World”的, 但停顿时间与堆大小无关, 只与GC Roots的数量相关。

    • 并发标记(Concurrent Marking) : 与G1一样, 遍历对象图, 标记出全部可达的对象, 这个阶段是与用户线程一起并发的, 时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。

    • 最终标记(Final Marking) : 与G1一样, 处理剩余的SATB扫描, 并在这个阶段统计出回收价值最高的Region, 将这些Region构成一组回收集(Collection Set) 。 最终标记阶段也会有一小段短暂的停顿。

    • 并发清理(Concurrent Cleanup) : 这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region) 。

    • 并发回收(Concurrent Evacuation) : 并发回收阶段是Shenandoah与之前HotSpot中其他收集器的核心差异。 在这个阶段, Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。 复制对象这件事情如果将用户线程冻结起来再做那是相当简单的, 但如果两者必须要同时并发进行的话, 就变得复杂起来了。 其困难点是在移动对象的同时, 用户线程仍然可能不停对被移动的对象进行读写访问, 移动对象是一次性的行为, 但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址, 这是很难一瞬间全部改变过来的。 对于并发回收阶段遇到的这些困难, Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决 。 并发回收阶段运行的时间长短取决于回收集的大小。

    • 初始引用更新(Initial Update Reference) : 并发回收阶段复制对象结束后, 还需要把堆中所有指向旧对象的引用修正到复制后的新地址, 这个操作称为引用更新。 引用更新的初始化阶段实际上并未做什么具体的处理, 设立这个阶段只是为了建立一个线程集合点, 确保所有并发回收阶段中进行的收集器线程都已完成分配给它们的对象移动任务而已。 初始引用更新时间很短, 会产生一个非常短暂的停顿。

    • 并发引用更新(Concurrent Update Reference) : 真正开始进行引用更新操作, 这个阶段是与用户线程一起并发的, 时间长短取决于内存中涉及的引用数量的多少。 并发引用更新与并发标记不同, 它不再需要沿着对象图来搜索, 只需要按照内存物理地址的顺序, 线性地搜索出引用类型, 把旧值改为新值即可。

    • 最终引用更新(Final Update Reference) : 解决了堆中的引用更新后, 还要修正存在于GC Roots中的引用。 这个阶段是Shenandoah的最后一次停顿, 停顿时间只与GC Roots的数量相关。

    • 并发清理(Concurrent Cleanup) : 经过并发回收和引用更新之后, 整个回收集中所有的Region已再无存活对象, 这些Region都变成Immediate Garbage Regions了, 最后再调用一次并发清理过程来回收这些Region的内存空间, 供以后新对象分配使用

  • Brooks Pointers

    • 在原有对象布局结构的最前面统一增加一个新的引用字段, 在正常不处于并发移动的情况下, 该引用指向对象自己

ZGC

  • 目标
    • 尽可能对吞吐量影响不太大的前提下, 实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟
  • 使用算法:colored pointer
  • GC记录在指针上,不是记录在头部
  • 42位指针 寻址空间4T
  • 使用Region(又叫Page或者Zpage),与G1不同的是具有动态性-动态创建和销毁以及动态的区域容量大小
    • 以下实在x64硬件平台
    • 小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
    • 中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
    • 大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。 每个大型Region中只会存放一个大对象, 这也预示着虽然名字叫作“大型Region”, 但它的实际容量完全有可能小于中型Region, 最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作, 用于复制对象的收集器阶段, 稍后会介绍到)的, 因为复制一个大对象的代价非常高昂

堆内存布局

  • ZGC堆内存布局

染色指针

  • Colored Pointer (Tag Pointer \ Version Pointer)

  • 直接把标记信息记在引用对象的指针上

  • 染色指针将64的指针中,除去不能够寻址的18位,将其高4位提取出来存储四个标志信息,通过这些标志信息可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过Finalize方法才能被访问到

    • image-20240719004010174
  • 因为压缩了地址空间,所以ZGC能够管理的内存不可以超过4TB(2的42次幂),也不支持压缩指针

ZGC运行过程

  • ZGC运行过程
  • 并发标记(Concurrent Mark) : 与G1、 Shenandoah一样, 并发标记是遍历对象图做可达性分析的阶段, 前后也要经过类似于G1、 Shenandoah的初始标记、 最终标记(尽管ZGC中的名字不叫这些) 的短暂停顿, 而且这些停顿阶段所做的事情在目标上也是相类似的。 与G1、 Shenandoah不同的是, ZGC的标记是在指针上而不是在对象上进行的, 标记阶段会更新染色指针中的Marked 0、 Marked 1标志位。

  • 并发预备重分配(Concurrent Prepare for Relocate) : 这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region, 将这些Region组成重分配集(Relocation Set) 。 重分配集与G1收集器的回收集(Collection Set) 还是有区别的, ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收。 相反, ZGC每次回收都会扫描所有的Region, 用范围更大的扫描成本换取省去G1中记忆集的维护成本。 因此, ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中, 里面的Region会被释放, 而并不能说回收行为就只是针对这个集合里面的Region进行, 因为标记过程是针对
    全堆的。 此外, 在JDK 12的ZGC中开始支持的类卸载以及弱引用的处理, 也是在这个阶段中完成的。

  • 并发重分配(Concurrent Relocate) : 重分配是ZGC执行过程中的核心阶段, 这个过程要把重分配集中的存活对象复制到新的Region上, 并为重分配集中的每个Region维护一个转发表(ForwardTable) , 记录从旧对象到新对象的转向关系。 得益于染色指针的支持, ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中, 如果用户线程此时并发访问了位于重分配集中的对象, 这次访问将会被预置的内存屏障所截获, 然后立即根据Region上的转发表记录将访问转发到新复制的对象
    上, 并同时修正更新该引用的值, 使其直接指向新对象, ZGC将这种行为称为指针的“自愈”(SelfHealing) 能力。 这样做的好处是只有第一次访问旧对象会陷入转发, 也就是只慢一次, 对比Shenandoah的Brooks转发指针, 那是每次对象访问都必须付出的固定开销, 简单地说就是每次都慢,因此ZGC对用户程序的运行时负载要比Shenandoah来得更低一些。 还有另外一个直接的好处是由于染色指针的存在, 一旦重分配集中某个Region的存活对象都复制完毕后, 这个Region就可以立即释放用于
    新对象的分配(但是转发表还得留着不能释放掉) , 哪怕堆中还有很多指向这个对象的未更新指针也没有关系, 这些旧指针一旦被使用, 它们都是可以自愈的。

  • 并发重映射(Concurrent Remap) : 重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用, 这一点从目标角度看是与Shenandoah并发引用更新阶段一样的, 但是ZGC的并发重映射并不是一个必须要“迫切”去完成的任务, 因为前面说过, 即使是旧引用, 它也是可以自愈的, 最多只是第一次使用时多一次转发和修正操作。 重映射清理这些旧引用的主要目的是为了不变慢(还有清理结束后可以释放转发表这样的附带收益) , 所以说这并不是很“迫切”。 因此, ZGC很巧妙地把并发重映射阶段要做的工作, 合并到了下一次垃圾收集循环中的并发标记阶段里去完成, 反正它们都是要遍历所有对象的, 这样合并就节省了一次遍历对象图的开销。 一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

ZGC的算法

  • Colored Pointer + 读屏障(LoadBarrier)

1垃圾收集器和内存分配策略
https://x-leonidas.github.io/2022/02/01/04Java/JVM/1垃圾收集器和内存分配策略/
作者
听风
发布于
2022年2月1日
更新于
2025年4月3日
许可协议