前言
大多数编程语言都会有自身的垃圾回收机制,php也不例外。经常听很多人说gc,也就是垃圾回收器,全程为Garbage Collection。
在php5.3之前,是不包括垃圾回收机制的,也没有专门的垃圾回收器,实现垃圾回收就是简单判断一下变量的zval的refcount是否为0,是的话就释放。
但是如果这么简单的判断垃圾回收的话,很容易引起程序过程中内存溢出。如果存在"自身指向自身"的情况的话,那么变量将无法回收早成内存泄露,所以从php5.3开始就出现了专门负责清理垃圾数据防止内存泄露的垃圾回收器。
引用计数的基本知识
我们要了解GC,那么首先要了解引起垃圾回收的基数是什么。
在php中,每个变量存在一个叫“zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括另外两个字节的额外信息。第一个是"is_ref"。第二个是"refcount"。
is_ref是一个布尔类型的值,用来标示这个变量是否属于引用集合。通过这个字节,php引擎才能把普通变量和引用变量区分开来,由于php允许用户通过"&"来使用自定义的引用,所以zval中还有一个内部引用计数机制,来进行优化内存。
refcount用来表示这个zval变量容器的变量的个数。所有符号存在一个符号表当中,每个符号都有作用域。
通俗的讲:
- refcount就是多少个变量是一样的用了相同的值,那么refcount就是这个值
- is_ref就是当有变量用了&的形式进行赋值,那么is_ref的值就会增加
减少引用计数
复合类型
当变量的类型为array或object这样的复合类型时,array和object类型的变量把他们的成员或属性存在自己的符号表中。
添加一个已经存在的元素到数组中:
从数组中删除一个元素:
将数组作为一个元素添加给自身:
我们可以看到数组a同时也是这个数组第二个元素,指向的变量容器中refcount的值为2,上面输出的“...”说明发生了递归操作,意味着"..."指向原始数组。
尽管不再有某个作用域中的任何符号指向这个结构(就是变量容器),由于数组元素“1”仍然指向数组本身,所以这个容器不能被清除 。因为没有另外的符号指向它,用户没有办法清除这个结构,结果就会导致内存泄漏。
垃圾回收周期
在5.3之前的版本中,php无法处理循环的引用内存泄露。但是自5.3之后php使用引用计数系统中同步周期回收的同步算法,仅处理这个内存泄露问题。
基本思想是如果一个引用计数增加那么将继续被使用,当然就不再是垃圾。如果引用计数减少到零,所在变量容器将被清除。那么也就是说只有在引用计数减少到非零值时,才会产生垃圾周期。在一个垃圾周期中通过检查引用计数是否减1,并且检查哪些变量容器的引用次数为零,来发现哪些是垃圾。
我们就拿这张图举例(来自php官网)。为了避免不得不检查所有引用计数可能减少的垃圾周期,同步算法将所有可能根放在了根缓冲区(root buffer)中(在图中用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根在缓冲区中只出现一次。仅当根缓冲区满了时,才对缓冲区中所有不同的变量容器执行垃圾回收操作,在图中体现为步骤A。
在步骤B中,模拟删除每个紫色的变量。模拟删除时可能将不是紫色的不同变量引用数减1,如果某个普通变量引用计数变成0时,就对这个普通变量在做一次模拟删除。每个变量只能被模拟删除一次,模拟删除后标记为灰色。
在步骤C中,模拟恢复每个紫色变量。当然这个恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样的每个变量只能恢复一次,恢复后标记为黑色,这样生下一对没能恢复的就是该删除的蓝色节点了,在步骤D中遍历出来真正的删除掉。
在php中垃圾回收机制默认是打开的,在你的php.ini中可以手动设置,通过zend.enable_gc这个属性进行开启或关闭垃圾回收机制。当开启了垃圾回收机制后,每当根缓存区存满时,就会执行上面描述的循环查找算法。根缓存区具有固定的大小,当然你可以通过修改php源码文件Zend/zend_gc.c中常量GC_ROOT_BUFFER_MAX_ENTRIES来修改根缓存区的大小(注意修改后需要重新编译php)。当关闭垃圾回收机制后,这个循环查找算法将不会执行,然而可能根会一直存在于根缓冲区中,不管在配置中是否激活了垃圾回收机制。
当然你也可以通过调用gc_enable()和gc_disable()函数来打开和关闭垃圾回收机制,效果和修改配置项相同。即使根缓冲区还没有满,也能强制执行周期回收。
php的内存管理机制
现在我们已经知道了zval是怎么回事了。那么现在我们需要知道php的内存管理机制是怎么一回事。
当在执行
- 为变量名分配内存,并存入符号表
- 为变量值分配内存
我们再看代码:
因为php的核心结构Hashtable,在定义的时候不可能一次性分配足够多的内存块,所以初始化的时候只会分配一小块,等不够的时候在进行扩容,而Hashtable只扩容不减少,所以当存入100个变量的时候符号表不够用了就进行一次扩容,当unset()时只是放了为变量值分配的内存,但是为变量名分配的内存还是在符号表中的,符号表并没有缩小,所以没收回来的内存是被符号表占去了。
php并不是只要内存不够就去向OS申请内存,而是先申请一大块内存,然后将其中一部分分给申请者,这样再有逻辑需要申请内存的时候,就不需要再向OS申请内存了,避免了重复申请,只有当一大块内存不够用的时候再去申请。而当释放内存时,php并非把内存还给了OS,而是把内存轨道自己维护的空闲内存列表,以便重复利用。
新版本的php(5.3版本之后)是如何处理垃圾内存的?
刚刚上面我们已经讲了,针对在php中环形引用导致的垃圾,产生了新的同步算法(GC算法),对于官网上的理论,我进行了理解:
如果一个zval的refcount增加,那么表明该变量的zval还在使用,不属于垃圾
如果一个zval的refcount减少到0,那么zval可以被释放掉,可以清除,不是垃圾
如果在经过模拟删除后一个zval的refcount减1,如果该zval的引用次数为是大于0,那么此zval不能被释放,可能是一个垃圾
关于垃圾回收的小知识点
unset():unset()只是断开一个变量到一块内存区域的连接,同时将该内存区域的引用计数减1,内存是否回收主要还是看refcount是否到0了。
null:将null赋值给一个变量是直接将该变量指向的数据结构置空,同时将其引用计数归0。
脚本执行结束:该脚本中所有内存都会被释放,无论是否有环引用。