JVM③垃圾回收


3、垃圾回收

3.1、四种引用

1、强引用

强引用(StrongReference),使用最普遍的引用。

  • 具有强引用的对象不会被垃圾回收;

  • 若内存不足,抛出 OutOfMemoryError 错误;

  • 当一个对象没有被任何 GC Root 强引用时,该对象才能被垃圾回收、

    1. 赋值 null;
    2. 超出生命周期。
    // 强引用
    String str = "hello";
    // 取消强引用
    str = null;
    

2、软引用

软引用(SoftReference),描述有用但非必需的对象(如缓存)。

  • GC 后仍内存不足时,会触发 Full GC(Full GC 会触发GC),回收软引用的对象;

  • 可以与引用队列关联使用,释放软引用自身

    1. 软引用的对象被回收后,软应用自身会进入关联的引用队列;
    2. 判断软引用是否入队,可释放软引用自身。
    // 软引用
    SoftReference softStr = new SoftReference<>("hello");
    
    // 关联引用队列
    ReferenceQueue queue = new ReferenceQueue<>();
    
    SoftReference softStr = new SoftReference<>("hello", queue);
    
    if(queue.poll != null){
    	softStr = null;
    }
    

3、弱引用

弱引用(WeakReference),描述非必需的对象,且生命周期更短。

  • 垃圾回收时,无论内存是否充足,弱引用的对象都会被 GC 回收;

  • 垃圾回收的线程优先级较低,未必能迅速发现并回收;

  • GC 只回收新生代的弱引用,Full GC 回收所有弱引用;

  • 可以与引用队列关联使用,释放弱引用自身。

    1. 弱引用的对象被回收后,弱应用自身会进入关联的引用队列;
    2. 判断弱引用是否入队,可释放弱引用自身。
    // 弱引用
    WeakReference weakStr = new WeakReference<>("hello");
    // 关联引用队列
    WeakReference queue = new WeakReference<>();
    
    WeakReference weakStr = new WeakReference<>("hello", queue);
    
    if(queue.poll != null){
    	weakStr = null;
    }
    

4、虚引用

虚引用(PhantomReference),“形同虚设”,相当于没有引用。

  • 任何时候都可能被垃圾回收;

  • 必须与引用队列关联使用,释放虚引用自身。

    • 虚引用的对象被回收时,虚引用自身会进入关联的引用队列;
    • 判断虚引用是否入队,可释放虚引用自身,或采取其它措施(如 ByteBuffer 释放直接内存)
    ReferenceQueue queue = new ReferenceQueue();
    PhantomReference pr = new PhantomReference(new String("hello"), queue);
    
  • 实例:ByteBuffer 被引用对象回收时,虚引用 Cleaner 入队,由 Reference Handler 线程调用虚引用相关方法(unsafe.freeMemory() 方法)释放直接内存。

* 终结器引用-FinalReference

  • 无需手动编码,但其内部关联引用队列;
  • 须与引用队列关联使用
    1. 在垃圾回收时,会将终结器引用入队;
    2. Finalizer 线程(优先级低)通过终结器引用找到被引用对象,调用它的 finalize() 方法;
    3. 下次垃圾回收时,会回收被引用对象。

3.2、对象”已死“判断算法

1、引用计数器

在对象中添加一个引用计数器。

  • 如果有地方引用该对象,引用计数加一;引用失效时则将引用计数减一。当引用计数为零时判断可回收。

  • 问题:对象循环引用。A 和 B 的引用计数器都不为零,但两个对象已经没有意义。导致内存泄漏

    image-20220222231116194

2、可达性分析

JVM 采用可达性分析算法,判断对象是否可回收。

  • 以树的形式呈现对象引用,根节点是 GC Roots 对象。

  • 做法:从 GC Roots 开始向下搜索并作标记,遍历完树后,未被标记的对象则判断为可回收的对象。

    image-20220223001333997

  • 可以作为GC Roots的对象

    • System Class:系统核心类
    • Native Stack:本地栈
    • Thread:线程的活动栈帧中的对象
    • Busy Monitor:被加锁的对象
  • 实例

    • 状态1:list 是一个 GC Roots 对象;
    • 状态2:list 不再是 GC Roots 对象;
    public static void main(String[] args){
    	// 1
        List list = new ArrayList<>();
    	// 2
        list = null;
    }
    
    
    
    

    3.3、GC 算法

    1、标记清除

    Mark Sweep

    1. 做法:通过可达性分析算法,标记不可回收对象,回收未被标记的对象。

    2. 特点:速度快,但会造成内存碎片。

      • 标记完成后直接清除,回收速度快‘
      • 若有一个较大的对象实例需要在堆中分配内存空间。可能无法找到足够的连续内存,不得不再次触发 GC。

      image-20220223203635718

    2、复制

    Copy,新生代使用

    1. 做法:通过可达性分析算法,标记不可回收对象。将不可回收对象复制到 To Survival 内存区域,将旧内存区域全部释放。并交换 From Survival 和 To Survival。

    2. 特点:没有内存碎片,但牺牲部分内存用于复制。(双倍)

      image-20220223203735990

    3、标记整理(压缩)

    新生代中,大部分对象不会存活很久,使用复制算法较高效;

    老年代中,大部分对象会继续存活,使用复制算法效率降低。

    Mark Compact,老年代使用

    1. 做法:通过可达性分析算法,标记不可回收对象。将不可回收对象的内存压缩到内存一端(移动),清除边界的内存。

    2. 特点:没有内存碎片,避免复制算法的效率问题。

      image-20220223203515513

    3.4、垃圾回收

    • Partial GC
      • Minor GC:新生代
      • Major GC:(CMS)老年代
      • Mixed GC:(G1)整个新生代,部分老年代
    • Full GC:整堆(新生代、老年代)和方法区

    1、分代垃圾回收

    image-20220223212937795

    新创建的对象被分配在 Eden 区,初始时两块 survivor 区都为空。

    1. Eden 区满时,触发 Minor GC
      • 将 Eden 区存活对象复制到 From 区,且存活对象年龄 +1。回收不存活的对象(清空 Eden 区);
      • 下次触发 Minor GC 时,Eden 和 From 区的情况同上;
    2. 当触发 Minor GC 时,Eden 和 From 区都满的情况下
      • 将 Eden 和 From 区的存活对象复制到 To 区,且存活对象年龄 +1。回收不存活的对象(清空 Eden 和 From 区);
      • 此时 From 区为空,To 区存在年龄不同的对象(从 From 过来的,从 Eden 过来的),交换 From 和 To 区(让 To 区始终为空);
    3. 之后每次触发 Minor GC,重复步骤 2 和 3(交换 From 和 To)
    4. 当存活对象的年龄达到一个阈值(-XX: MaxTenuringThershold,默认 15),就会从新生代晋升到老年代(Promotion);
    5. 当触发 Minor GC 时,JVM 会检查晋升对象的大小
      • 若大于老年代的剩余空间,则触发 Full GC;
      • 否则,检查是否设置了允许担保失败+HandlePromotionFailure
        • 是则只进行 Minor GC,否则进行 Full GC;
        • 意味着设置了该参数,触发 Minor GC 时可能触发 Full GC,哪怕老年代还有剩余一一部分内存。

    :Minor GC 会触发 STW(Stop the world),使用户线程阻塞。垃圾回收线程执行结束后,用户线程才恢复运行。

    2、GC 触发条件

    :仅 Minor GC 会单独进行,Major GC 只会在 Full GC 触发时进行(CMS 收集器除外)。

    (CMS 收集器可以单独进行 Major GC)

    • Partial GC
      • Minor GC:新生代 Eden 满触发,采用复制算法;
      • Major GC:CMS 老年代内存不足时触发,采用标记整理算法。
    • Full GC:整堆(新生代、老年代)和方法区,触发时机如下
      1. 调用System.gc() 时,但不一定会执行;
      2. 老年代空间不足;
      3. 方法区空间不足;
      4. Minor GC 后,从新生代晋升的对象大小,大于老年代的可用内存;
      5. Minor GC 时 From 区满,且 To 区内存不足,且老年代的可用内存小于该对象大小。

    3、对象进入老年代

    1. 长期存活对象:存活对象的年龄达到一个阈值,就会从新生代晋升到老年代(参数-XX: MaxTenuringThershold,默认 15)。
    2. To 区内存不足:Minor GC 时 From 区满,且 Eden、From 区对象大小,大于 To 区可用内存。
    3. 动态年龄判定:在 From 区中,所有相同年龄对象占用的总内存超过 From 区的一半。则不小于该年龄的对象就会被移动到老年代,无需达到阈值(默认15)。
    4. 大对象:新创建的对象大于某个阈值,即使 Eden 区有足够内存,也会直接存入老年代。(参数PretenureSizeThreshold,默认 3M)。

    3.5、垃圾回收器

    image-20220225201518184

    • 垃圾回收器(连线表示可以搭配使用)
      • 新生代:Serial、ParNew、Parallel Scavenge
      • 老年代:Serial Old、Parallel Old、CMS
      • 整堆:G1
    • 相关概念
      • 吞吐量:CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值
        • 吞吐量 = 用户代码运行时间 / (用户代码运行时间 + 垃圾回收时间)
        • 例:JVM 共运行 100min,GC 消耗 1min,则吞吐量为 99%
      • 并行收集:多个垃圾回收线程同时工作,用户线程阻塞。
      • 并发收集:垃圾回收线程与用户线程同时工作。

    新生代 GC

    Serial、ParNew、Parallel Scavenge

    1、Serial

    Serial 是最基本的,发展最悠久的 GC。

    • 特点

      1. 单线程,复制算法
      2. 简单高效:由于没有线程交互的开销,专注于垃圾收集。
      3. STW:需要暂停所有工作线程,直到垃圾回收结束(Stop the world)
    • 应用场景:堆内存较小,Client 模式下的 JVM

      image-20220223235538652

    2、ParNew(并行)

    ParNew 可以看作 Serial 的多线程版本。

    • 特点:除了多线程外,其它特点与 Serial 一致

    • 应用场景:Server 模式下的 JVM 的首选新生代 GC。它是除了 Serial 外,唯一能与 CMS 搭配使用的。

    • -XX:ParallelGCThreads:并行垃圾回收线程数,默认为 CPU 核数

      image-20220224001359671

    3、Parallel Scavenge(吞吐量优先)

    Parallel Scavenge 与吞吐量相关,也称吞吐量优先收集器。

    JDK 1.8 默认

    • 特点:类似 ParNew,但增加 GC 自适应调节策略

    • 目标:高吞吐量(单位时间内,GC 总时间最短)

    • 应用场景:要求高吞吐量和 CPU 资源敏感。

    • -XX:ParallelGCThreads

    • -XX:+UseAdaptiveSizePolicy:GC 自适应调节策略(JVM 动态设置新生代内存比例,晋升阈值等)

    • 吞吐量控制

      • -XX:GCTimeRatio:GC 时间占比。默认99,对应 t = 1%【r = (1-t)/t】
      • -XX:MaxGCPauseMillis:最大 GC 暂停时间,默认 200ms

      image-20220224001359671

    老年代 GC

    Serial Old、Parallel Old、CMS

    1、Serial Old

    Serial Old Serial 的老年代版本,标记整理算法。

    应用场景

    • 堆内存较小,Client 模式下的 JVM
    • 也可以在 Server 模式下使用
      1. 在 JDK 1.5 之前,与 Parallel Scavenge 搭配使用;
      2. CMS 的后备方案,并发收集失败时使用(Concurrent Mode Failure)

    2、Parallel Old

    Parallel Scavenge 的老年代版本,标记整理算法。

    应用场景:要求高吞吐量和 CPU 资源敏感。

    3、CMS(并发,最快响应时间)

    CMS(Concurrent Mark Sweep),标记清除算法。

    • 特点:多线程并发收集,与用户线程并发运行

    • 应用场景:要求响应速度快,停顿时间短,用户体验高。(web 应用,B/S 服务)

    • 目标:最快响应时间(单次 GC 时间最短)

    • 过程

      1. 初始标记:标记 GC Roots 对象(STW)
      2. 并发标记:可达性分析算法,找出存活对象(与用户线程并发运行)
      3. 重新标记:修正部分变动的标记(STW)
        • 在并发标记阶段,垃圾回收线程与用户线程并发运行;
        • 可能导致部分对象的标记发生改变。
      4. 并发清除:回收标记对象(与用户线程并发运行)
    • 缺点

      1. CPU 资源敏感。
      2. 内存碎片:采用标记清除算法。
      3. 浮动垃圾:并发清除阶段,垃圾回收时其它用户线程可能产生新的垃圾。可能出现 Concurrent Model Failure 而再次触发 Full GC。

      image-20220224001439960

    整堆:G1

    Garbage First:JDK 1.7 可用,JDK 9 默认,取代 CMS

    • 将整个堆划分为相等大小的独立区域(Region)。
    • 特殊区域:Humongous,巨型对象。
      • 巨型对象:一个对象占用的内存超过 Region 内存的一半。
      • 在以前的垃圾回收器中,巨型对象会被分配在老年代。如果该对象只是短期存在,会降低 GC 的效率。
      • Humongous 专门存放巨型对象,若一个 H 区装不下。会寻找连续的 H 区来存储。(甚至引发 Full GC 来产生连续的 H 区)
    • 特点
      • 并行与并发:充分利用多核 CPU,缩短 STW 时间。
      • 分代回收:独立管理整个堆,采用不同方式管理新生代和老年代。
      • 空间整合:不会产生内存碎片。

    image-20220225230609098

    Young GC

    Eden 区满时触发。

    • 将 Eden 区的数据移动到 Survivor 区。若 Survivor 区内存不足,部分数据会直接晋升到老年代。
    • Survivor 区的数据移动到新的 Survivor 区,也有部分数据晋升到老年代。
    • 最终 Eden 区空,GC 完成,用户线程恢复运行。

    问题:如何找到所有的 GC Roots 对象?

    • 在堆中,任何 Region 的任意对象之间都可以发生引用关系。
    • 采用可达性分析算法时,需扫描整个堆,效率低。

    解决:Remembered Set 和 CardTable。

    • Remembered Set:HashTable,Key 是其它 Region 的其实地址,Value 是一个集合(元素是 CardTable 的 Index)
      • G1 中每个 Region 都有一个 Remembered Set(point-in)。
      • 当其它 Region 引用了当前 Region 的对象时,记录在 RSet 中。
    • CardTable:通常为字节数组
      • 逻辑上分为固定大小的连续区域(卡),默认都未被引用。
      • 脏卡:当一个地址空间被引用时,对应数组下标的卡值标记为 0
      • RSet 也记录该数组下标。
    • 具体
      1. 程序在对 Reference 类型进行写操作时,产生一个 Write Barrier 暂时中断写操作
      2. 检查 Reference 引用对象是否处于多个 Region 中(即跨代引用:老年代中引用了新生代中的对象)
      3. 如果是,标记脏卡,被引用 Region 的 Remembered Set。

    Young GC 过程

    1. 根扫描:扫描静态和本地对象
    2. 更新 RSet:处理 Dirty card 队列,更新 RSet
    3. 处理 RSet:检测从新生代指向老年代的对象
    4. 对象拷贝:将存活对象拷贝到 survivor/old 区域
    5. 处理引用队列:软引用,弱引用,虚引用

    Mix GC

    回收所有新生代,部分老年代。

    包括全局并发标记和拷贝存活对象两个阶段。

    全局并发标记(Glob Concurrent Marking)

    1. 初始标记:标记 GC Roots 对象(STW)
    2. 根区域扫描
    3. 并发标记:可达性分析算法,找出存活对象(与用户线程并发运行)
    4. 最终标记:修正部分变动的标记(STW)
    5. 并发清除:对各个 Region 的回收价值和成本及逆行排序,根据 MaxGCPauseMillis 来制定回收计划。(与用户线程并发运行

    并发标记:三色标记算法

    • 黑色:GC Roots 对象,或者已处理完的对象
    • 灰色:正在处理的对象
    • 白色:未处理的对象。扫描完所有对象后仍为白色说明是不可达对象。

    步骤

    1. 初始:GC Roots 为黑色,子对象为置为灰色

      image-20220225234827105

    2. 继续由灰色遍历,处理后置为黑色

      image-20220225234958546

    3. 扫描完所有对象后,可达对象都是黑色,不可达的对象为白色,需要被清理。

      image-20220225235041223

    存在问题:对象处理遗漏问题。并发清除阶段,垃圾回收线程与用户线程并发进行,可能引起对象的指针改变。

    1. 某一状态:A 已处理完,B 正在处理,C 是 B 的子对象且未处理。

      image-20220225235336856

    2. 在处理 C 前,用户线程修改了 C 对象的指针,将 C 与 已处理完的 A 引用

      image-20220225235440931

    3. 此时 GC 会认为 B 已处理结束,置为黑色。并且认为 A 也处理完成,结束对象扫描。

      image-20220225235538585

    4. 此时 C 是白色,被认为是垃圾而被清理。这显然是不合理的。

    解决:SATB(snapshot-at-the-beginning)

    1. 初始标记阶段,生成一个快照标记存活对象;
    2. 并发标记阶段,将所有被修改的对象入队;
    3. 可能存在游离的垃圾,将在下次被回收。
    JVM