垃圾回收算法
回收对象算法
引用计数算法
引用计数算法的核心作用是自动管理内存。它能够实时地追踪每个对象在程序中被引用的状态,一旦确定某个对象不再被任何部分使用(即引用计数为 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的整体策略 |