Jvm-垃圾回收算法

垃圾回收算法

回收对象算法

引用计数算法

引用计数算法的核心作用是自动管理内存。它能够实时地追踪每个对象在程序中被引用的状态,一旦确定某个对象不再被任何部分使用(即引用计数为 0),就立即回收其占用的内存,从而防止内存泄漏,实现资源的自动化管理。

机制

引用计数算法的原理非常直观,可以概括为"计数归零即回收"。

1、维护计数器 :为堆中的每一个对象关联一个整型的引用计数器,用于记录当前有多少个指针或引用指向该对象。

2、计数增减

  • 增加 (Retain/Increment):每当有一个新的引用指向该对象时(例如,将对象赋值给一个新变量、作为参数传递给方法、或放入一个容器中),其引用计数器就加1。
  • 减少 (Release/Decrement):每当一个指向该对象的引用失效时(例如,引用变量被重新赋值、超出作用域、或被显式删除),其引用计数器就减 1。

3、触发回收 :当对象的引用计数器值变为 0 时,意味着程序中再无任何地方可以访问到它,该对象即成为"垃圾",可以被立即回收。

代码示例
java 复制代码
class ReferenceCountedObject {
    // 引用计数器,初始值为1(对象创建时自身持有一个引用)
    private int refCount = 1;

    // 增加引用
    public void retain() {
        refCount++;
    }

    // 减少引用
    public void release() {
        refCount--;
        // 计数为0时,可被回收
        if (refCount == 0) {
            System.out.println("对象可被回收");
        }
    }
}
优点

1、回收及时 :对象的引用计数一旦归零,内存就可以被立即回收,无需等待特定的垃圾回收周期。这使得内存能够被快速复用,实时性非常高。

2、无全局停顿 (STW) :垃圾回收的动作是分散在程序运行过程中的,每次引用增减都可能触发回收,因此不会出现为了执行全局垃圾回收而暂停所有用户线程(Stop-The-World)的情况,程序的停顿时间极短且平滑。

3、实现简单:算法逻辑直观,易于理解和实现。

缺点

1、无法解决循环引用 :这是引用计数算法最致命的缺陷。如果两个或多个对象之间相互引用 ,但已经不再被程序的其他部分所使用,它们的引用计数将永远不会变为 0,导致这些"垃圾"对象无法被回收,从而造成内存泄漏

  • 示例:对象 A 引用了对象 B,同时对象 B 也引用了对象 A。即使外部对 A 和 B 的引用都消失了,它们彼此的引用计数也至少为 1,导致无法回收。

2、性能开销 :每次对对象进行引用赋值或使其失效时,都需要同步地更新其引用计数器(加 1 或减 1)。在多线程环境下,为了保证计数的准确性,这个操作还需要是原子性的,这带来了额外的性能开销。

3、空间开销:每个对象都需要额外的空间来存储其引用计数器,这在对象数量庞大的场景下会占用可观的内存。

可达性分析算法(GC Roots)

可达性分析算法的核心作用是精准地识别堆内存中"已死亡"的对象。它通过判断一个对象是否还能被程序中的"活动部分"访问到,来决定该对象是否可以被垃圾回收器安全地回收,从而有效管理内存,防止内存泄漏。

机制

算法将内存中所有存活的对象看作一个复杂的对象图。它从一组被称为 "GC Roots" 的根对象出发,像图遍历一样,沿着对象之间的引用关系向下搜索。所有能被 GC Roots 直接或间接访问到的对象都被认为是"可达的"(即存活的),而那些无法从任何 GC Roots 到达的对象则被标记为"不可达的"(即死亡的)。

GC Roots内容:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象:例如,当前正在执行的方法中的局部变量。
  • 方法区中静态属性引用的对象 :例如,被 static 关键字修饰的类变量。
  • 方法区中常量引用的对象:例如,字符串常量池中的常量。
  • 本地方法栈中 JNI(Native 方法)引用的对象
  • 正在运行的线程对象

为了高效地执行可达性分析,现代 JVM 通常采用三色标记法来优化,尤其是在并发标记阶段。它将对象标记为三种颜色:

  • 白色 (White):表示对象尚未被垃圾回收器访问过。在分析的初始阶段,所有对象都是白色的。分析结束后,所有仍是白色的对象即为不可达对象,可以被回收。
  • 灰色 (Grey):表示对象已被垃圾回收器访问过,但它所引用的其他对象还没有被完全扫描。
  • 黑色 (Black):表示对象已被垃圾回收器完全访问过,它自身以及它所引用的所有对象都已被标记为非白色(灰色或黑色)。
标记流程

1、将所有 GC Roots 对象标记为灰色,并放入一个待处理的队列中。

2、从队列中取出一个灰色对象,将其引用的所有白色对象标记为灰色,并加入队列。然后,将该对象自身标记为黑色。

3、重复步骤 2,直到灰色队列为空。此时,所有可达对象都已被标记为黑色,所有白色对象即为不可达对象,可以被回收。

优点

1、解决循环引用:这是可达性分析算法相比引用计数算法最大的优势。即使两个或多个对象之间相互引用,只要它们整体上与 GC Roots 断开连接,算法就能正确地识别出它们都是不可达的,从而一并回收,彻底解决了循环引用导致的内存泄漏问题。

2、准确性高:通过从程序的"根"开始全面扫描,能够非常准确地判断出哪些对象是真正在使用的,哪些是垃圾,减少了误回收的风险。

缺点

1、需要全局停顿 (STW):为了保证可达性分析结果的准确性,算法必须在一个"一致性快照"中进行。这意味着在分析的初始阶段(枚举 GC Roots),必须暂停所有用户线程(Stop-The-World),以防止对象的引用关系在分析过程中发生变化,导致结果不准确。这会造成应用程序的短暂停顿。

2、实现复杂:相比于简单的引用计数,可达性分析的实现要复杂得多,尤其是在处理并发标记、对象移动等问题时,需要复杂的算法(如三色标记、写屏障等)来保证效率和正确性。

3、分析耗时:当堆内存中的对象数量非常庞大、对象图非常复杂时,遍历整个对象图需要消耗一定的时间,这可能会影响垃圾回收的整体效率。

对象收集算法

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

作用: 识别并回收堆内存中不再使用的对象。

机制与原理: 算法分为两个阶段:

1、标记 (Mark): 从 GC Roots 出发,遍历并标记出所有存活的对象。

2、清除 (Sweep): 遍历整个堆内存,回收所有未被标记的对象。

优点:

  • 实现简单: 逻辑清晰,易于实现。
  • 无需移动对象: 存活对象的位置不变,减少了部分开销。

缺点:

  • 产生内存碎片: 回收后会留下大量不连续的空闲内存块,可能导致后续无法分配大对象,从而提前触发下一次 GC。
  • 效率问题: 标记和清除两个阶段都需要遍历内存,当存活对象或垃圾对象很多时,效率会下降。

标记-复制算法 (Mark-Copy)

为了解决"标记-清除"算法的内存碎片问题而诞生。

作用: 在回收垃圾的同时,保证内存空间的连续性,消除碎片。

机制与原理:

1、将可用内存划分为两块大小相等的区域(通常称为 From 区和 To 区)。

2、每次只使用 From 区。当 From 区满了,就将其中所有存活的对象复制到 To 区。

3、然后一次性清空 From 区的所有内存。

4、交换 From 区和 To 区的角色,下次 GC 时重复此过程。

优点:

  • 无内存碎片: 存活对象被连续地复制到新区域,内存分配变得非常简单高效。
  • 效率高: 只需复制存活对象,在存活对象很少的场景下(如新生代)效率极高。

缺点:

  • 空间利用率低: 需要预留一倍的内存空间,实际可用内存只有 50%。
  • 复制开销: 当对象存活率很高时,复制操作的成本会非常昂贵。

标记-整理算法 (Mark-Compact)

该算法结合了"标记-清除"和"标记-复制"的优点,旨在解决存活对象多且不希望有内存碎片的场景。

作用: 在避免内存碎片的同时,保持较高的内存利用率,特别适合处理长期存活的对象。

机制与原理:

1、标记 (Mark): 与"标记-清除"算法一样,先标记出所有存活的对象。

2、整理 (Compact): 将所有存活的对象向内存空间的一端移动,然后直接清理掉边界以外的所有内存。

优点:

  • 无内存碎片: 通过移动对象,保证了内存空间的连续性。
  • 内存利用率高: 不需要像复制算法那样预留额外空间。

缺点:

  • 移动开销大: 整理阶段需要移动大量存活对象,并更新所有引用这些对象的地址,这个过程成本较高,效率低于复制算法。

分代收集算法 (Generational Collection)

这并非一种独立的、具体的算法,而是一种基于对象生命周期理论的内存管理策略。现代 JVM 的垃圾回收器都采用这种思想。

作用: 根据对象存活周期的不同,将堆内存划分为不同区域,并对不同区域采用最合适的回收算法,以实现整体性能的最优化。

机制与原理:

基于"弱分代假说"(绝大多数对象都是朝生夕死的)和"强分代假说"(熬过越多次 GC 的对象越难以死亡)。

将堆内存划分为新生代 (Young Generation)老年代 (Old Generation)

1、新生代 : 对象"朝生夕死",存活率极低。因此采用标记-复制算法,效率极高。新生代内部又细分为 Eden 区和两个 Survivor 区(From/To)。

2、老年代 : 存放长期存活的对象,存活率高。因此采用标记-整理标记-清除算法,避免高昂的复制成本。

优点:

  • 综合性能最优: 充分利用了不同算法的优势,将高频、低成本的 GC(Minor GC)和低频、高成本的 GC(Major/Full GC)分离开。
  • 适应性强: 完美契合了程序中对象的实际生命周期特征。

缺点:

  • 实现复杂: 需要维护不同代之间的对象引用和晋升机制,实现起来比单一算法复杂得多。

对比总结

算法 核心思想 优点 缺点 主要应用场景
标记-清除 标记存活对象,然后清除垃圾 实现简单,不移动对象 产生内存碎片,效率不稳定 老年代(早期或特定回收器)
标记-复制 将存活对象复制到新区,清空旧区 无内存碎片,存活对象少时效率极高 空间利用率低(50%),存活对象多时开销大 新生代
标记-整理 标记存活对象,然后将其向一端移动整理 无内存碎片,内存利用率高 移动对象开销大,效率较低 老年代
分代收集 按对象生命周期分代,不同代用不同算法 综合性能最优,符合对象生存规律 实现复杂 现代JVM的整体策略
相关推荐
耶叶1 小时前
Android开发实战:通过网络电子书阅读器实践运用fragment知识
android·jvm
Java面试题总结1 小时前
新人笔记之模板方法模式
java·笔记·模板方法模式
LCG元1 小时前
STM32嵌入式开发:基于PID算法的直流电机闭环调速控制
stm32·嵌入式硬件·算法
2401_831824961 小时前
RESTful API设计最佳实践(Python版)
jvm·数据库·python
NGC_66111 小时前
深入理解 Java 线程池:从原理到实战
java·开发语言·python
测试_AI_一辰1 小时前
Agent & RAG 测试工程笔记 14:RAG门控层拆解:什么时候该答?什么时候必须拒绝?
人工智能·算法·ai·自动化·ai编程
Σίσυφος19001 小时前
多频相位展开(Multi-frequency Phase Unwrapping)”可以替代格雷码?
算法
暮冬-  Gentle°1 小时前
更优雅的测试:Pytest框架入门
jvm·数据库·python
xuxie991 小时前
N10 ARM中断
jvm