前言php
大多数编程语言都会有自身的垃圾回收机制,php也不例外。常常听不少人说gc,也就是垃圾回收器,全程为Garbage Collection。html
在php5.3以前,是不包括垃圾回收机制的,也没有专门的垃圾回收器,实现垃圾回收就是简单判断一下变量的zval的refcount是否为0,是的话就释放。node
可是若是这么简单的判断垃圾回收的话,很容易引发程序过程当中内存溢出。若是存在"自身指向自身"的状况的话,那么变量将没法回收早成内存泄露,因此从php5.3开始就出现了专门负责清理垃圾数据防止内存泄露的垃圾回收器。算法
引用计数的基本知识编程
咱们要了解GC,那么首先要了解引发垃圾回收的基数是什么。数组
在php中,每一个变量存在一个叫“zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括另外两个字节的额外信息。第一个是"is_ref"。第二个是"refcount"。缓存
is_ref是一个布尔类型的值,用来标示这个变量是否属于引用集合。经过这个字节,php引擎才能把普通变量和引用变量区分开来,因为php容许用户经过"&"来使用自定义的引用,因此zval中还有一个内部引用计数机制,来进行优化内存。php7
refcount用来表示这个zval变量容器的变量的个数。全部符号存在一个符号表当中,每一个符号都有做用域。数据结构
通俗的讲:编程语言
一、 refcount就是多少个变量是同样的用了相同的值,那么refcount就是这个值
二、 is_ref就是当有变量用了&的形式进行赋值,那么is_ref的值就会增长
<?php$a = "new string"; ?>
在上面的代码中,变量a是在当前做用于中生成的,而且生成了类型为String和值为"new string"的变量容器。这个时候is_ref被默认的设置成了false,由于如今没有任何自定义的引用生成。refcount被设置成了1。咱们能够用php来看到这些计数的变化,首先须要用到xdebug,因此php没有装上xdebug扩展的须要先装一下。
<?php
$a = "new string"; xdebug_debug_zval('a');
输出:a: (refcount=1, is_ref=0)='new string'
?>
增长zval的引用计数
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' ); 输出:a: (refcount=2, is_ref=0)='new string'
?>
这时的引用次数是2,由于同一个变量容器被变量 a 和变量 b关联.当不必时,php不会去复制已生成的变量容器。变量容器在”refcount“变成0时就被销毁. 当任何关联到某个变量容器的变量离开它的做用域(好比:函数执行结束),或者对变量调用了函数 unset()时,”refcount“就会减1。
减小引用计数
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
unset( $b, $c );
xdebug_debug_zval( 'a' );
输出: a: (refcount=3, is_ref=0)='new string' a: (refcount=1, is_ref=0)='new string'
?>
执行 unset($a);,包含类型和值的这个变量容器就会从内存中删除。
复合类型
当变量的类型为array或object这样的复合类型时,array和object类型的变量把他们的成员或属性存在本身的符号表中。
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );
输出: a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=1, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42 )
?>
根据上面的代码,咱们能够理解,对于数组来当作一个总体,对于内部的值来看又是一个独立的总体,各自都有着一套zval的refcount和is_ref。下面这张图是从官网上扒下来的:
添加一个已经存在的元素到数组中:
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning']; xdebug_debug_zval( 'a' );
输出: a: (refcount=1, is_ref=0)=array ( 'meaning' => (refcount=2, is_ref=0)='life', 'number' => (refcount=1, is_ref=0)=42, 'life' => (refcount=2, is_ref=0)='life' )
?>
以下图解释:
从数组中删除一个元素:
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );
输出: a: (refcount=1, is_ref=0)=array ( 'life' => (refcount=1, is_ref=0)='life' ) ?>
删除数组中一个元素,就是相似从做用于中删除一个变量,删除后数组中这个元素所在容器的refcount的值减小,当refcount为0时,这个变量容器就从内存中被删除。
将数组做为一个元素添加给自身:
<?php
$a = array( 'one' );
$a[] =& $a; xdebug_debug_zval( 'a' );
输出: a: (refcount=2, is_ref=1)=array ( 0 => (refcount=1, is_ref=0)='one', 1 => (refcount=2, is_ref=1)=... )
?>
咱们能够看到数组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的内存管理机制是怎么一回事。
var_dump(memory_get_usage());
$test = "这是测试啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());
输出(php5.6): /var/www/html/node_test/phptest/phptest.php:51: int(361896) /var/www/html/node_test/phptest/phptest.php:53: int(361928) /var/www/html/node_test/phptest/phptest.php:55: int(361896)
过程是:定义变量->内存增长->清除变量->内存恢复
var_dump(memory_get_usage());
$test = "这是测试啊";
var_dump(memory_get_usage());
unset($name);
var_dump(memory_get_usage());
输出(php7.1): /var/www/html/node_test/phptest/phptest.php:51: int(361896) /var/www/html/node_test/phptest/phptest.php:53: int(361928) /var/www/html/node_test/phptest/phptest.php:55: int(361928)
而我在用php7时发现了这个问题,这就要说道php5和php7的内存管理机制和垃圾回收机制的不一样了,这里暂且不表。咱们继续往下走。
当在执行
$test = "这是测试啊";
内存的分配作了两件事:
咱们再看代码:
var_dump(memory_get_usage());
for($i=0;$i<100;$i++) {
$a = "test".$i; $$a = "hello";
}
var_dump(memory_get_usage());
for($i=0;$i<100;$i++) {
$a = "test".$i; unset($$a);
}
var_dump(memory_get_usage());
输出: /var/www/html/node_test/phptest/phptest.php:57: int(363520) /var/www/html/node_test/phptest/phptest.php:63: int(372384) /var/www/html/node_test/phptest/phptest.php:69: int(369216)
为何内存没有所有收回来呢?
由于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。
脚本执行结束:该脚本中全部内存都会被释放,不管是否有环引用。