当引用计数无法解决问题时,PHP 会触发标记清除算法,从根节点开始标记所有可达对象。
·
当 引用计数(Reference Counting) 无法解决循环引用问题时,PHP 的垃圾回收机制会触发 标记清除算法(Mark-and-Sweep Algorithm)。这是 PHP 5.3 引入的一项改进,用于更有效地管理内存并避免内存泄漏。
1. 引用计数的基本原理
1.1 定义
-
引用计数 是一种简单的垃圾回收机制,每个对象都有一个引用计数器(
refcount),记录有多少地方引用了该对象。 -
工作方式:
- 创建对象时,引用计数加 1。
- 复制对象或引用时,引用计数加 1。
- 销毁对象或超出作用域时,引用计数减 1。
-
释放内存:
- 当引用计数降为 0 时,对象的内存会被立即释放。
1.2 局限性
-
循环引用问题:
- 当两个或多个对象互相引用时,即使它们已经不再被外部引用,引用计数也不会降为 0,导致内存泄漏。
-
示例:
class A { public $b; } class B { public $a; } $a = new A(); $b = new B(); $a->b = $b; $b->a = $a; unset($a, $b); // 即使 unset,$a 和 $b 的引用计数仍为 1
2. 标记清除算法的引入
2.1 解决循环引用问题
-
定义:
- 标记清除算法通过从根节点开始标记所有可达对象,并清理未被标记的对象,解决了循环引用问题。
-
适用场景:
- 当引用计数无法解决问题时,标记清除算法会介入。
2.2 触发条件
-
垃圾回收器的阈值:
- PHP 的垃圾回收器会维护一个双向链表(称为“GC Roots”),用于跟踪可能包含循环引用的对象。
- 当链表中的对象数量达到一定阈值(默认为 10,000 个)时,垃圾回收器会触发标记清除算法。
-
手动触发:
- 开发者可以通过
gc_collect_cycles()手动触发标记清除算法。
- 开发者可以通过
3. 标记清除算法的具体实现
3.1 标记阶段
-
定义:
- 从根节点(如全局变量、活动符号表等)出发,递归标记所有可达对象。
-
实现步骤:
- 遍历 GC Roots 链表中的所有对象。
- 对于每个对象,检查其是否可以通过根节点直接或间接访问。
- 如果对象可达,则将其标记为“存活”。
-
示例:
Root -> Object A -> Object B ↘ -> Object C- 假设
Object A是根节点,则Object B和Object C会被标记为可达。
- 假设
3.2 清除阶段
-
定义:
- 遍历堆内存,查找未被标记的对象,并释放它们占用的内存。
-
实现步骤:
- 遍历 GC Roots 链表中的所有对象。
- 如果对象未被标记,则将其视为垃圾并释放内存。
- 清理标记状态,为下一次垃圾回收做准备。
-
示例:
Heap: Object A (Marked) Object B (Marked) Object D (Unmarked)- 在清除阶段,
Object D被释放。
- 在清除阶段,
4. PHP 垃圾回收的优化
4.1 分代垃圾回收
-
定义:
- 尽管 PHP 没有明确的分代垃圾回收机制,但它通过区分短期对象和长期对象来优化性能。
-
实现方式:
- 短期对象(如局部变量)通常很快变为不可达,优先被清理。
- 长期对象(如全局变量)会在多次垃圾回收中保留。
4.2 增量式垃圾回收
-
定义:
- PHP 的垃圾回收器将标记清除过程分成多个小步骤,逐步完成,以减少程序暂停时间。
-
优点:
- 减少 Stop-the-World 时间,提高程序响应速度。
5. 示例代码分析
以下是一个完整的 PHP 示例,展示引用计数和标记清除算法的工作过程:
class A {
public $b;
}
class B {
public $a;
}
$a = new A();
$b = new B();
$a->b = $b; // $a 引用 $b
$b->a = $a; // $b 引用 $a
// 此时,$a 和 $b 的引用计数均为 2
unset($a, $b); // 引用计数降为 1,但仍然存在循环引用
// 手动触发垃圾回收
gc_collect_cycles(); // 标记清除算法介入,清理 $a 和 $b
6. 总结
PHP 的垃圾回收机制结合了 引用计数 和 标记清除算法:
- 引用计数 用于快速释放不再使用的对象。
- 标记清除算法 用于解决循环引用问题,确保内存不会泄漏。
理解这两种机制的工作方式及其关系,有助于开发者编写更高效的代码,并避免常见的内存管理问题(如内存泄漏、性能瓶颈)。
更多推荐
所有评论(0)