开发者

JVM GC垃圾回收算法使用及说明

开发者 https://www.devze.com 2025-10-15 10:26 出处:网络 作者: backRoads
目录垃圾回收算法(GC Algorithms)标记-清除(Mark-Sweep)复制算法(Copying)​​标记-整理(Mark-Compact)分代收集(Generational Collection)可达性算法(Reachability Analysis)可达性算法的基本流程​GC R
目录
  • 垃圾回收算法(GC Algorithms)
    • 标记-清除(Mark-Sweep)
    • 复制算法(Copying)
    • ​​标记-整理(Mark-Compact)
    • 分代收集(Generational Collection)
  • 可达性算法(Reachability Analysis)
    • 可达性算法的基本流程​
    • GC Roots 的具体类型​
      • 虚拟机栈中的引用​​
      • 本地方法栈中的引用​​
      • 方法区的静态变量和常量​​
      • 同步锁持有的对象​​
      • JVM 内部对象​​
      • 跨代引用记录对象​​
    • 引用类型对可达性的影响​
    • 三色标记(Tri-color Marking)
      • 定义
        • 标记流程(基本步骤)​
          • ​​初始阶段
          • 标记阶段(并发)
          • ​​最终阶段
        • 并发标记的两种问题​
          • 漏标(对象丢失)
          • 错标(浮动垃圾)
      • OopMap(Ordinary Object Pointer Map)
        • OopMap 的生成时机​
          • 方法编译时(JIT 阶段)​
          • 安全点(Safe Point)​
          • 特定指令插入​
        • OopMap 如何协助 JVM 获取 GC Roots​
          • 快速定位引用位置​
          • 结合安全点减少 STW 时间​
          • 与卡表(Card Table)协作​
        • OopMap 与 GC 流程的协作​
        • 总结

          垃圾回收算法(GC Algorithms)

          JVM 根据对象生命周期特性(分代假设)采用不同的回收算法,核心算法包括:

          标记-清除(Mark-Sweep)

          此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。

          • 流程​​:标记所有存活对象 → 清除未标记对象。
          • ​​优点​​:简单、无对象移动开销。
          • ​​缺点​​:内存碎片化、效率较低(需遍历两次堆)。

          JVM GC垃圾回收算法使用及说明

          复制算法(Copying)

          此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小, 同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。

          • ​流程​​:将存活对象复制到另一块内存区域 → 清空原区域。
          • ​​优点​​:无碎片、效率高(适用于存活率低的对象,如新生代)。
          • ​​缺点​​:内存利用率低(需预留一半空间)。

          JVM GC垃圾回收算法使用及说明

          ​​标记-整理(Mark-Compact)

          此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

          • 流程​​:标记存活对象 → 移动对象到内存一端 → 清理边界外内存。
          • ​​优点​​:无碎片、内存利用率高。
          • ​​缺点​​:对象移动开销大(适用于老年代)。

          JVM GC垃圾回收算法使用及说明

          分代收集(Generational Collection)

          • 核心思想​​:根据对象存活时间划分内存区域(新生代、老年代)。
          • ​​新生代​​:存活率低 → 使用复制算法(如 Serial、ParNew)。
          • 老年代​​:存活率高 → 使用标记-清除或标记-整理算法(如 cms、G1)。

          可达性算法(Reachability Analysis)

          可达性算法(​​Reachability Analysis​​)是 JVM 垃圾回收的核心机制,用于判断对象是否存活。

          其核心思想是:​​从一组根对象(GC Roots)出发,遍历所有引用链,未被引用的对象视为可回收垃圾​​。以下是其详细原理、流程和应用场景:

          JVM GC垃圾回收算法使用及说明

          可达性算法的基本流程​

          1.​​确定 GC Roots​​

          JVM 枚举所有根对象(如栈帧中的局部变量、静态变量等),作为遍历起点。

          2.遍历引用链​​

          从 GC Roots 出发,递归遍历所有直接或间接引用的对象,形成​​对象图(Object Graph)​​。

          3.​​标记存活对象​​

          所有被遍历到的对象标记为存活(如使用三色标记法中的黑色或灰色状态)。

          4.​​回收不可达对象​​

          未被遍历到的对象(白色对象)视为垃圾,由具体 GC 算法回收(如标记-清除、复制等)。

          GC Roots 的具体类型​

          以下对象被定义为 GC Roots,不会被回收:

          虚拟机栈中的引用​​

          当前执行方法的局部变量、方法参数(如 public void foo(Object param))。

          当前线程的调用栈中所有方法的局部变量。

          本地方法栈中的引用​​

          JNI(Java Native Interface)方法中的对象引用(如通过 JNIEnv 调用的对象)。

          方法区的静态变量和常量​​

          类的静态变量(static 修饰)。

          字符串常量池中的引用(如 String s = “abc”)。

          同步锁持有的对象​​

          通过 synchronized 关键字锁定的对象(如 synchronized(obj) 中的 obj)。

          JVM 内部对象​​

          系统类加载器(ClassLoader)加载的类。

          异常对象(如 OutOfMemoryError)、线程对象等。

          跨代引用记录对象​​

          卡表(Card Table)中记录的老年代对新生代的引用(需特殊处理)。

          引用类型对可达性的影响​

          引用类型强引用(Strong)软引用(Soft)弱引用(Weak)虚引用(Phantom)
          定义​默认引用(Object obj = new Object())内存不足时回收(SoftReference)下次 GC 必回收(WeakReference)仅用于跟踪对象回收(PhantomReference)
          ​​可达性影响​强可达软可达弱可达不可达
          ​​回收条件​不可达时回收内存不足时回收无论内存是否充足均回收对象回收后入队通知

          示例:

          Object strongRef = new Object();          // 强引用
          SoftReference<Object> softRef = new SoftReference<>(new Object());
          WeakReference<Object> weakRef = new WeakReference<>(new Object());
          PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), new ReferenceQueue<>());
          

          三色标记(Tri-color Marking)

          定义

          用三种颜色抽象对象的状态:

          ​​白色(White)​​

          • ​​初始状态​​:对象未被访问(未标记)。
          • ​​最终回收​​:标记完成后仍为白色的对象视为可回收垃圾。

          ​​灰色(Gray)​​

          • ​​中间状态​​:对象被标记为存活,但其子引用(成员变量、数组元素等)尚未被遍历。

          ​​黑色(Black)​​

          • ​​最终存活状态​​:对象被标记为存活,且所有子引用已被遍历完成。

          标记流程(基本步骤)​

          ​​初始阶段

          • 所有对象设为白色。
          • 将 GC Roots 直接引用的对象标记为灰色(放入灰色队列)。

          标记阶段(并发)

          从灰色队列中取出对象:

          • —遍历该对象的所有子引用,将子引用指向的白色对象标记为灰色。
          • —将当前对象标记为黑色。
          • 重复上述过程,直到灰色队列为空。

          ​​最终阶段

          所有存活对象应为黑色,白色对象视为垃圾。

          并发标记的两种问题​

          因用户线程与 GC 线程并发运行,对象引用可能发生变化,导致两种风险:

          漏标(对象丢失)

          场景​​:

          黑色对象(已标记完成)被用户线程写入了一个新的白色对象引用。

          用户线程删除了灰色对象到某个白色对象的引用。

          结果​​:

          白色对象未被标记为存活,导致被错误回收。

          例​​:

          初始:黑(A) → 灰(B) → 白(C) → 白(D)
          B 即将处理,但此时用户线程执行:
          1. A.field = D   // 黑对象引用了白对象
          2. B.field = null // 断开灰对象到C的引用
          最终php:C未标记(白色),D未标记(白色),会被误回收。
          

          解决

          1. 增量更新(Incremental Update)​​

          • ​​原理​​:记录新插入的引用,重新标记。
          • ​​实现​​:当用户线程将黑色对象插入对白色对象的引用时,通过​​写屏障(Write Barrier)​​ 将黑色对象重新标记为灰色。
          • ​​示例 GC​​:CMS(Concurrent Mark-Sweep)。

          ​​2. 原始快照(SATB, Snapshot At The Beginning)​​

          • ​​原理​​:基于标记开始时存在的对象引用关系快照(即假设这些对象是存活的)。
          • ​​实现​​:当用户线程修改引用关系(如删除一个引用),通过写屏障将旧引用的目标对象标记为灰色。
          • ​​示例 GC​​:G1、ZGC、Shenandoah。

          错标(浮动垃圾)

          场景​​:

          对象实际已死亡,但在标记阶段被标记为存活。

          解决:

          ​​可容忍​​:仅导致少量内存未及时释放,下次 GC 可清理。

          OopMap(Ordinary Object Pointer Map)

          OopMap 记录了栈上本地变量到堆上对象的引用关系。其作用是:垃圾收集时,收集线程会对栈上的内存进行扫描,看看哪些位置存储了 Reference 类型。如果发现某个位置确实存的是 Reference 类型,就意味着它所引用的对象这一次不能被回收。但问题是,栈上的本地变量表里面只有一部分数据是 Reference 类型的(它们是我们所需要的),那些非 Reference 类型的数据对我们而言毫无用处,但我们还是不得不对整个栈全部扫描一遍,这是对时间和资源的一种浪费。

          一个很自然的想法是,能不能用空间换时间,在某个时候把栈上代表引用的位置全部记录下来,这样到真正 gc 的时候就可以直接读取,而不用再一点一点的扫描了。事实上,大部分主流的虚拟机也正是这么做的,比如 HotSpot ,它使用一种叫做 OopMap 的数据结构来记录这类信息。

          我们知道,一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一编程客栈个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。

          OopMap(Ordinary Object Pointer Map)是 JVM 用于​​快速定位 GC Roots​​ 的关键数据结构,通过记录栈帧和寄存器中的对象引用位置,显著减少垃圾回收时的停顿时间(Stopjs-The-World, STW)。以下是其生成时机及作用机制的详细解析:

          OopMap 的生成时机​

          OopMap 的生成与 ​​JIT 编译器​​ 和 ​​安全点(Safe Point)​​ 密切相关,主要发生在以下场景:

          方法编译时(JIT 阶段)​

          ​​即时编译(JIT)​​:

          当方法被 JIT 编译器编译为本地机器码时,编译器会分析方法的栈帧布局,并生成对应的 OopMap。

          ​​记录内容​​:

          栈帧中哪些位置(偏移量)存储了对象引用(Oop,Ordinary Object Pointer)。如:局部变量表、方法参数、this 指针等。

          ​​示例​​:

          若方法的局部变量表第 3 个槽位是 Object obj,则 OopMap 会记录该槽位的偏移量。

          安全点(Safe Point)​

          • ​​安全点触发​​:当 JVM 需要执行 GC、代码反优化等操作时,所有用户线程必须暂停在安全点。
          • ​​安全点位置​​:通常插入在方法调用、循环回边(如 for 循环)、异常抛出等位置(避免长时间不进入安全点)。
          • ​​OopMap 更新​​:在安全点处,JVM 会生成或更新当前线程的 OopMap,确保准确记录此时栈帧和寄存器中的引用。

          GC 仅是触发安全点的一种场景,其他操作(如偏向锁撤销)也会触发安全点。

          ​​并非所有 OopMap 都在 GC 前生成​​,但 GC 前必须依赖安全点更新 OopMap。

          特定指令插入​

          显式生成指令​​:JIT 编译器会在生成的机器码中插入特殊指令(如 test 指令),用于检查是否需要进入安全点并生成 OopMap。

          OopMap 如何协助 JVM 获取 GC Roots​

          GC Roots 是垃圾回收的起点,包括​​栈帧中的局部变量、静态变量、JNI 引用等​​。

          OopMap 的作用是快速枚举这些根引用,避免全栈扫描。

          快速定位引用位置​

          • 直接映射​​:OopMap 明确记录了栈帧中哪些位置存储了对象引用(如局部变量、方法参数)。
          • ​​寄存器记录​​:部分引用可能存储在寄存器中(如 this 指针),OopMap 也会记录这些寄存器的名称。

          结合安全点减少 STW 时间​

          • ​​暂停线程​​:当 GC 触发时,所有线程需快速暂停在安全点。
          • ​​遍历 OopMap​​:GC 线程直接读取各线程的 OopMap,遍历记录的引用位置,收集所有 GC Roots。
          • ​​无需全栈扫描​​:避免逐字节检查整个栈内存,极大缩短暂停时间。

          与卡表(Card Table)协作​

          • 维护跨代引用​​:卡表用于记录老年代到新生代的引用,OopMap 帮助快速定位这些引用所在的栈或寄存器位置。
          • ​​写屏障支持​​:当用户线程修编程改对象引用时,写屏障会更新卡表,而 OopMap 确保这些修改在 GC 时被正确识别。

          OopMap 与 GC 流程的协作​

          以 ​​Young GC​​ 为例,流程如下:

          1.​​触发 GC​​:新生代空间不足,需回收。

          ​​2.进入安全点​​:所有用户线程暂停,生成 OopMap。

          3.​​枚举 GC Roots​​:

          • —根据 OopMap 遍历所有线程的栈帧和寄存器,收集指向新生代的引用(如局部变量 obj)。
          • —结合卡表,找到老年代中指向新生代的对象。

          4.​​标记存活对象​​:从 GC Roots 出发,标记所有可达对象。

          5.​​恢复线程​​:完成 GC 后,线程继续执行。

          总结

          以上为个人经验,希望能给大家一个参考,也希望大js家多多支持编程客栈(www.devze.com)。

          0

          精彩评论

          暂无评论...
          验证码 换一张
          取 消

          关注公众号