引用计数
为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。而PHP中内存对象就是zval,而计数器就是refcount__gc
PHP5.3 Zval
每个zval表示一个变量的内存对象。除了value和type,可以看到_zval_struct中还有两个字段refcountgc和is_refgc,从其后缀就可以断定这两个家伙与垃圾回收有关。没错,PHP的垃圾回收全靠这俩字段了。其中refcountgc表示当前有几个变量引用此zval,而is_refgc表示当前zval是否被按引用引用,这话听起来很拗口,这和PHP中zval的“Write-On-Copy”机制有关
PHP5.3 回收机制
每个PHP的变量都存在于一个叫做zval的容器中,一个zval容器,除了包含变量名和值,还包括两个字节的额外信息,一个叫做'is_ref',是个布尔值,用来表示这个变量是否属于引用集合,通过这个字节,我们php才能把普通变量和引用变量区分开来.第二个额外字节就是'refcount',用来表示指向这个容器的变量的个数
首先PHP会分配一个固定大小的“根缓冲区”,这个缓冲区用于存放固定数量的zval,这个数量默认是10,000,如果需要修改则需要修改源代码Zend/zend_gc.c中的常量GC_ROOT_BUFFER_MAX_ENTRIES然后重新编译。
当根缓冲区满额时,PHP就会执行垃圾回收,此回收算法如下:
1、对每个根缓冲区中的根zval按照深度优先遍历算法遍历所有能遍历到的zval,并将每个zval的refcount减1,同时为了避免对同一zval多次减1(因为可能不同的根能遍历到同一个zval),每次对某个zval减1后就对其标记为“已减”。
2、再次对每个缓冲区中的根zval深度优先遍历,如果某个zval的refcount不为0,则对其加1,否则保持其为0。
3、清空根缓冲区中的所有根(注意是把这些zval从缓冲区中清除而不是销毁它们),然后销毁所有refcount为0的zval,并收回其内存。
如果不能完全理解也没有关系,只需记住PHP5.3的垃圾回收算法有以下几点特性:
1、并不是每次refcount减少时都进入回收周期,只有根缓冲区满额后在开始垃圾回收。
2、可以解决循环引用问题。
3、可以总将内存泄露保持在一个阈值以下。
PHP7
- php7的垃圾回收包含两个部分,一个是垃圾收集器,一个是垃圾回收算法。
- 最基础的变化就是 zval 需要的内存 不再是单独从堆上分配,不再由 zval 存储引用计数。复杂数据类型(比如字符串、数组和对象)的引用计数由其自身来存储
- php7的对于refcount的位置也已经从zval结构移到zval_value内了,所以不是所有的数据类型都会有引用计数操作,比如整型、浮点型、布尔、NULL这些简单数据类型都没有refcount了
- 垃圾回收只适用于array、object这两种类型。
- 变量的refcount减少到0,那么此变量可以被释放掉,不属于垃圾。
- 变量的refcount减少之后大于0,那么此变量还不能释放,此变量可能成为一个垃圾。
- 第一种情况就是可以理解为正常的unset操作,不属于垃圾。
- 第二种情况垃圾回收器才会将变量收集起来。
参考
垃圾回收是指当php运行状态结束时,比如遇到了exit/die/致命错误/脚本运行结束时,php需要回收运行过程中创建的变量、资源的内存。 ZEND引擎维护了一个栈zval,每个创建的变量和资源都会压入这个栈中,每个压入的数组结构都类似:[refcount => int, is_ref => 0|1, value => union, type => string],变量被unset时,ref_count如果变成0,则被回收。
当遇到变量循环引用自身时,使用同步回收算法回收。
备注:PHP7已经重写了zal的结构体。