上次说到了 refcount 和 is_ref, 这里来说说内存泄露的情况
代码如下:
- $a = array(1, 2, &$a);
- unset($a);
在老的 PHP 版本中, 这里就会出现内存泄露, 分析如下:
执行第一行, 可以知道 $a 和 $a[2] 指向的 zval refcount=2,is_ref=1
然后执行第二行,$a 将会从符号表中被删除, 同时指向的 zval 的 refcount--, 此时 refcount=1, 因为 refcount!=0, 故此 zval 不会被当做垃圾回收, 但是此时我们却失去了 $a[2] 指向这个 zval 的入口, 因此这个 zval 成了一块内存垃圾
同样的道理可以发生在类内部引用里, 例如
代码如下:
- $a = new Man();
- $a->self = &$a;
- unset($a);
那么如何解决这种问题呢, 新的 GC 机制采用了一个算法来解决这个问题
PHP 有一个 root buffer 用来存储 zval 的节点信息, 当 root buffer 满了或者手动调用 gc 函数时, GC 算法启动
对于一个数组或者类类型的 zval 而言, 在垃圾回收机制启动时, 算法会对该 zval 的数组 / 类内部的元素 / 成员的 zval 进行一次遍历并将 refcount 减 1, 如果说遍历完成后该 zval 的 refcount 被减为 0, 则说明这个 zval 是一个内存垃圾, 他将被销毁, 见下面的例子
代码如下:
- $a = array(1, 2, &$a, &$a);
- unset($a);
容易知道 $a 指向的 zval, 假设为 z1 的 refcount=3,is_ref=1
当 unset($a) 执行的时候,$a 就已经从符号表中删去, 同时我们也失去了访问 z1 的入口, 此时 z1 refcount=2,is_ref=1
当 GC 启动时, 会对该 z1 的数组元素的 zval 的 refcount 进行遍历减 1, 遍历到 a[2] 时, z1 refcount--, a[3] 时 z1 refcount--, 此时 z1 refcount = 0, 即可将 z1 标记为内存垃圾, 算法后将其回收
总结来说可以这么表述: 若一个数组类型的 zval, 对他的元素 zval 进行一次遍历, 同时将遍历到的 zval 的 refcount--, 如果最后 refcount=0 的 zval, 就是垃圾, 需要被回收
来源: https://www.php1.cn/detail/php-65a37b05e3.html