php gc机制 引用计数 写时复制

简介

如果自己实现一个自动 GC,最简单的方式:在函数定义变量时分配一块内存,用于保存 zval 及对应的 value 结构,在函数返回时将内存释放,如果在函数执行期间该变量作为参数调用了其它函数或赋值给了其它变量,则把变量复制一份,变量间相互独立,不会出现冲突。

这种方式是可行的,内存管理简单,但是带来的问题是:深拷贝效率,而且内存浪费严重,解决问题的方案就是:引用计数 + 写时复制

PS:zval: 变量存在 zval 的变量容器中,除了包含变量的类型和值,还有 is_ref 是 bool 值,用来区分普通变量和引用变量。

引用计数

引用计数用来记录当前有多少 zval 指向同一个 zend_value,有新 zval 指向这个 value,计数加1,相反 zval 销毁时,计数减1,当引用计数为0时,对 value 进行释放。

引用计数记在哪

在 PHP7 中将变量的引用计数保存在了 zend_value。

并不是所有的类型都会用到引用计数。比如 整形、浮点型、布尔型、NULL,它们的值直接通过 zval 保存,因此不会共用 value,而是进行深拷贝。

当然了也有一些类型在特殊情况下也不会用引用计数,这里就不细说了。

写时复制 (copy on write)

写时复制在计算机系统中有着非常广泛的应用。

变量使用了引用计数就会出现其中一个变量修改 value 的情况,这时候要对 value 进行分离,发生修改的变量会复制一份数据出来修改,同断开原来 value 的指向,指向新的 value。

写时复制
PHP7 对象,资源无法进行复制。

循环引用问题

引用计数实现了变量自动 GC 机制,但有一种情况机制无法解决。

$a   = [];
$a[] = &$a;
unset($a);

unset 前,引用计数 refcount = 2,unset($a) 后,减少了一次引用 refcount,此时已经没有任何外部引用了,但是数组中仍然有一元素指向引用。

因变量无法回收导致内存始终得不到释放,造成内存泄漏,这种情况就是循环引用。

为了解决这种无法释放的变量垃圾,PHP 引入了另外一种机制 垃圾回收

垃圾回收

怎么知道变量是垃圾呢?
  1. 如果一个变量 value 的 refcount 减少到0,那么此 value 可以被释放,不属于垃圾
  2. 如果一个变量 value 的 refcount 减少后大于0,那么此 value 还不能被释放,此 value 可能 成为一个垃圾

第一种情况垃圾回收器不会处理

只有第二种情况垃圾回收器会把可能成为垃圾的 value 收集起来,保存到一个 buffer 缓存区中。等达到一定数量后启动垃圾鉴定程序,真正的垃圾释放。

既然是把垃圾收集起来,那就要有算法做回收 回收算法

回收算法

垃圾回收池中,gc_root_buffer 它是一个双向链表,同时记录引用计数相关信息,zend_gc_globals 维护着 gc 的整信息,里有很多信息就不细说了!

三色标记

  • 紫色 称为疑似垃圾
  • 黑色:对象正在被使用
  • 灰色:可能被回收
  • 白色:对象垃圾
简单描述垃圾鉴定过程
  • 步骤1: 遍历垃圾回收器的 buffer 缓存区,把当前 value 标记为灰色,然后对当前 value 的成员进行深度优先遍历,成员 refcount 减 1,并标记为灰色。
  • 步骤2: 重复遍历 buffer,检查当前 value 引用是否为 0,为 0 则表示为垃圾,则标记为白色,如果不为 0 则排除引用全部来与自身成员的可能,表示还有外部引用,并不是垃圾。这时候因为步骤1 对成员进行了 refcount 减1 操作,需要在还回去,再对所有成员深度遍历,把成员 refcount 加1,同时标记为黑色。
  • 步骤3: 再次遍历 buffer 将非 白色节点从 buffer 中删除,最终 buffer 缓存区中全部为真正的垃圾,最后将这些垃圾释放,回收完成。
垃圾回收算法流转图

其实说了这么多,就是为了筛选对象是否有自身引用,遍历对象所有成员对其 refcount 减1。遍历完如果 refcount 为0,说明有自身引用,则为垃圾。
那些不是自身引用或有外部引用的,从 buffer 中删除。
最后 buffer 中只留下了真正的垃圾,对其释放内存。

你可能感兴趣的:(php gc机制 引用计数 写时复制)