JVM2️⃣垃圾回收

1、四种引用

1.1、强引用

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

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

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

  • 当一个对象没有被任何 GC Root 强引用时,该对象才能被垃圾回收(显式赋值 null 或超出生命周期)。

    // 强引用
    String str = "hello";
    // 取消强引用
    str = null;
    

1.2、软引用

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

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

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

    1. 软引用的对象被回收后,软应用自身会进入关联的引用队列;

    2. 判断软引用是否入队,可释放软引用自身。

      // 软引用
      SoftReference<String> softStr = new SoftReference<>("hello");
      
      // 关联引用队列
      ReferenceQueue<String> queue = new ReferenceQueue<>();
      
      SoftReference<String> softStr = new SoftReference<>("hello", queue);
      
      if(queue.poll != null){
      	softStr = null;
      }
      

1.3、弱引用

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

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

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

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

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

    1. 弱引用的对象被回收后,弱应用自身会进入关联的引用队列;

    2. 判断弱引用是否入队,可释放弱引用自身。

      // 弱引用
      WeakReference<String> weakStr = new WeakReference<>("hello");
      // 关联引用队列
      WeakReference<String> queue = new WeakReference<>();
      
      WeakReference<String> weakStr = new WeakReference<>("hello", queue);
      
      if(queue.poll != null){
      	weakStr = null;
      }
      

1.4、虚引用

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

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

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

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

* 终结器引用-FinalReference

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

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

2.1、引用计数器

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

  • 若该对象在某处被引用则计数加 1,引用失效时则将引用计数减 1

  • 引用计数为零时判断可回收

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

    image-20220222231116194

2.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<Object> list = new ArrayList<>();
      	// 2
          list = null;
      }
      

3、GC 算法(❗)

3.1、标记清除

Mark Sweep

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

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

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

3.2、复制

Copy,新生代使用

将内存分为两块,每次只使用其中一块。

  1. 做法

    • 通过可达性分析算法,标记存活对象
    • 将存活对象复制到未使用的内存块中清除旧内存区域交换两个内存角色。
  2. 特点:没有内存碎片,但牺牲部分内存用于复制(双倍)。

    image-20220223203735990

3.3、标记整理(压缩)

Mark Compact,老年代使用

  1. 做法

    • 通过可达性分析算法,标记存活对象
    • 将存活对象的内存压缩到内存一端清除边界的内存
  2. 特点:没有内存碎片,避免在老年代使用复制算法的效率问题。

    image-20220312104813482

4、垃圾回收(❗)

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

4.1、分代垃圾回收

4.1.1、堆结构划分

思想:根据对象的生命周期,将内存划分为新生代和老年代。

  1. 新生代:大部分对象不会存活很久,适合复制算法。
    • Eden:新创建的对象(1)
    • Survivor:存活对象(2)
  2. 老年代:大部分对象会继续存活,适合标记整理算法。
  3. 方法区
    • 永久代:1.6 属于堆
    • 去永久代:1.7 部分移出
    • 元空间:1.8 本地内存

4.1.2、过程描述(❗)

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,哪怕老年代还有剩余一一部分内存。

4.1.3、Stop-The-World

STW(Stop the world):Java 全局暂停的现象。

  • 多数是由 GC 触发,此时本地方法可以执行但不能与 JVM 交互。
  • 少数是由 Dump 线程、堆 Dump、死锁检查触发。

STW 使用户线程阻塞,垃圾回收线程执行结束后,用户线程才恢复运行。

  • 目的
    • 避免无法清理干净
    • GC 工作需要在一个能确保一致性的快照中进行。
  • 危害:长时间服务停止,没有响应。

4.2、对象进入老年代(❗)

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

4.3、GC 触发条件

CMS 收集器 可以单独进行 Minor GC 和 Major GC。

其它垃圾收集器只会单独进行 Minor GC,在 Full GC 触发时进行 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 区内存不足,且老年代的可用内存小于该对象大小。

5、垃圾回收器(❗)

image-20220225201518184

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

5.1、新生代 GC

Serial、ParNew、Parallel Scavenge

5.1.1、Serial(串行)

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

  • 特点

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

    image-20220223235538652

5.1.2、ParNew(并行)

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

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

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

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

    image-20220224001359671

5.1.3、Parallel Scavenge(吞吐量优先)

Parallel Scavenge 与吞吐量相关,也称吞吐量优先收集器(JDK 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

5.2、老年代 GC

Serial Old、Parallel Old、CMS

5.2.1、Serial Old

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

应用场景

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

5.2.2、Parallel Old

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

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

5.2.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

5.3、整堆 GC

G1:Garbage First

JDK 7 可用,JDK 9 默认,取代 CMS

  • 做法:将整个堆划分为相等大小的独立区域(Region)。

  • 特殊区域:Humongous,巨型对象(一个对象占用的内存超过 Region 内存的一半)

    • 在以前的垃圾回收器中,巨型对象会被分配在老年代。如果该对象只是短期存在,会降低 GC 的效率。
    • Humongous 专门存放巨型对象,若一个 H 区装不下。会寻找连续的 H 区来存储。(甚至引发 Full GC 来产生连续的 H 区)
  • 特点

    • 并行与并发:充分利用多核 CPU,缩短 STW 时间。

    • 分代回收:独立管理整个堆,采用不同方式管理新生代和老年代。

    • 空间整合:不会产生内存碎片。

      image-20220225230609098

5.3.1、Young GC

Eden 区满时触发。

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

寻找 GC Roots

问题:如何找到所有的 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. 处理引用队列:软引用,弱引用,虚引用

5.3.2、Mix GC

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

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

全局并发标记

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

并发标记

三色标记算法

从 GC Roots 开始遍历并标记

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

步骤

  1. 初始:从 GC Roots 开始遍历,正在处理的对象为灰色,处理后为黑色。

    image-20220225234958546

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

    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. 可能存在游离的垃圾,将在下次被回收。
posted @ 2022-02-24 11:39  Jaywee  阅读(62)  评论(0编辑  收藏  举报

👇