PHP 垃圾回收机制
PHP 变量与内存对象
垃圾回收是对变量及所关联内存对象的操作。
PHP 变量分为两类:
- 标量类型:布尔型、整型、浮点型、字符串
- 复杂类型:数组、对象、资源
- NULL(特殊,不划分为任何类型)
所有上述类型均用 zval
的结构表示。
源码 Zend/zend.h
5 个字段存储 8 种类型。
_zvalue_value
用于表示 PHP 中所有变量的值。
typedef union _zvalue_value { |
- lval:存储布尔型、整型、资源
- dval:存储浮点型
- str:存储字符串
- ht:存储数组(PHP 中的数组是哈希表)
- obj:存储对象
- 所有字段设置为0:NULL
_zval_struct
由 _zval_struct
中的 type
决定 _zvalue_value
究竟是哪种类型。
struct _zval_struct { |
refcount__gc
表示当前有几个变量在引用此zval
is_ref__gc
表示当前zval
是否被 按引用 引用
PHP5.2
PHP5.2 中的垃圾回收算法是:Reference Counting,即”引用计数”。
算法原理
- 为每个内存对象分配一个计数器
- 当一个内存对象建立时计数器初始化为 1
- 以后每有一个新变量引用此内存对象,则计数器加1
- 每当减少一个引用此内存对象的变量则计数器减 1
- 当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存
内存对象即 zval
,计数器是 refcount__gc
。
代码模拟
|
赋值
|
引用赋值
|
数组型变量
|
销毁变量
|
*注: unset
并非一定会释放内存,当有两个变量指向的时候,并非会释放变量占用的内存,只是 refcount
减 1。
缺陷
存在内存泄漏问题。
看一段代码:
|
- 建立了数组 a
- 让 a 的第一个元素按引用指向 a,此时
a.zval.refcount = 2
- 销毁变量 a,此时
a.zval.refcount = 1
,但没有办法对其进行操作,形成了一个循环自引用
PHP5.3
PHP5.3 的垃圾回收算法依然以引用计数为基础,但不再用简单的计数作为回收准则,而是使用了一种同步回收算法:Concurrent Cycle Collection in Reference Counted Systems。
原理
一个 zval
如果有引用,要么被全局符号表中的符号引用,要么被其它表示复杂类型的 zval
中的符号引用。因此在 zval
中存在一些可能根(root
)。
- PHP 会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的
zval
,这个数量默认是 10,000 - 对每个根缓冲区中的根
zval
按照深度优先遍历算法遍历所有能遍历到的zval
,并将每个zval
的refcount
减 1(同时为了避免对同一zval
多次减 1(因为可能不同的根能遍历到同一个zval
),每次对某个zval
减 1 后就对其标记为“已减”) - 再次对每个缓冲区中的根
zval
深度优先遍历zval
的refcount
不为0,对其加 1
- 清空根缓冲区中的所有根,然后销毁所有
refcount
为 0 的zval
,并收回其内存
总结:对此 zval
中的每个元素进行一次 refcount
减 1 操作,操作完成之后,如果 zval
的 refcount=0
,那么这个 zval
就是一个辣鸡。
图解
A. 节点被放入缓冲区 root
,标记为紫色
B. 缓冲区满后,以深度优先对每个节点所包含的 zval
做减 1 操作。操作过的节点标记为灰色(避免重复操作)
C. 再次深度优先判断每个节点包含的 zval
值。refcount=0
:标记为白色(垃圾),refcount>0
:对非垃圾还原操作,zval + 1
,标记为黑色
D. 再次遍历,将白色节点 zval
释放
算法特点
- 不是每次
refcount
减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收 - 可以解决循环引用问题
- 可以总将内存泄露保持在一个阈值以下
参考资料
- 本文链接:http://jalan.space/2018/12/23/2018/php-gc/
- 版权声明:本博客所有文章除特别声明外,均采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。
分享