PHP垃圾回收机制

前言

前几天面试的时候,面试官让我讲一讲PHP的垃圾回收机制,我一脸懵逼,事后觉得非常有必要了解一下,查阅了很多资料,自己做一下总结。
PHP5和PHP7的垃圾回收机制发生了很大的变化,先讲PHP5,再讲PHP7。

引用

引用是这一话题的万恶之源,先来看看引用是怎么一回事。
举个栗子

<?php
$a = "hello";
$b = &$a;
$c = $a;
var_dump($a);
$a = null;
var_dump($b);
var_dump($c);

代码的输出结果是

string(5) "hello"  NULL  string(5) "hello"

PHP官网对引用的原话是

References in PHP are a means to access the same variable content by
different names. They are not like C pointers; for instance, you
cannot perform pointer arithmetic using them, they are not actual
memory addresses, and so on. Instead, they are symbol table aliases.
Note that in PHP, variable name and variable content are different, so
the same content can have different names.

PHP中引用是用不同名称访问相同变量内容的方法。与C中的指针不同,它不是实际的内存地址,而是一种符号表别名。
上面的栗子中,b就是a的别名而已,给a赋其他值,就是将符号表指向了其他值。
而c却仍能输出原本的内容,说明c和a一样,保存的都是内存地址。
举第二个栗子:

<?php
$a = "hello";
$b = &$a;
$c = $a;
var_dump($a);
unset($a);
var_dump($b);
var_dump($c);

输出结果

string(5) "hello"  string(5) "hello"  string(5) "hello"

这里对变量a使用了unset(),这实际上只是将a从符号表中删除,并不影响符号表中其他元素的使用。

PHP更换版本

使用中需要不停地更换PHP版本,因此我就记录一下Ubuntu中更换PHP版本的方法
Ubuntu16.04
PHP5.6
PHP7.0
安装PHP5.6的方法:

sudo apt-get install php5.6

更换版本:

sudo update-alternatives --config php

在这里插入图片描述
输入相应的序号即可更换版本。

安装Xdebug

sudo apt-get install php5.6-xdebug

修改PHP5.6的配置文件php.ini
加入一行:

zend_extension=xdebug.so

PHP7.0类似操作,略过了。

zval容器

创建变量时,PHP会自动创建一个zval容器,用于回收内存,使用的内存回收算法是Reference Counting,这个算法翻译叫做“引用计数”,其思想非常直观和简洁:为每个内存对象分配一个计数器,当一个内存对象建立时计数器初始化为1(因此此时总是有一个变量引用此对象),以后每有一个新变量引用此内存对象,则计数器加1,而每当减少一个引用此内存对象的变量则计数器减1,当垃圾回收机制运作的时候,将所有计数器为0的内存对象销毁并回收其占用的内存。

PHP5.6

zval单独从堆中分配内存,返回的是一个指针。
zval容器包含变量的类型和值,还包含了两个字节的额外信息,分别是is_ref和refcount。
is_ref bool 用以标识变量是否属于引用集合,区分普通变量和引用变量。
refcount 用以表示指向这个zval容器的变量个数。

我要开始举好多?
先讲普通变量(标量)

普通变量(标量)

example 1

<?php
$a = "hello";
xdebug_debug_zval('a');
?>

PHP5输出:

a: (refcount=1, is_ref=0)='hello' 

这个很好理解,指向该zval容器的只有变量a,未被引用。

example2
赋值一次

<?php
$a = "hello";
$b = $a;
xdebug_debug_zval('a');
?>

PHP5输出:

a: (refcount=2, is_ref=0)='hello'

指向该zval容器的有变量a和变量b。php在这里做了一个优化,由于是普通变量(标量),因此没有为b单独开辟新的zval容器,这么做节省了内存。这种优化方法叫做COW(Copy On Write)。

example 3
引用一次

<?php
$a = "hello";
$b = &$a;
xdebug_debug_zval('a');
?>

PHP5:

a: (refcount=2, is_ref=1)='hello'

指向该zval容器的有变量a,b,b又引用了a

example 4
引用加赋值一次

<?php
$a = "hello";
$b = &$a;
$c = $a;
xdebug_debug_zval('a');
?>

PHP5输出:

a: (refcount=2, is_ref=1)='hello' 

这里有点奇怪,是因为c与a不再共享一个zval容器了

example 4.1
引用加赋值一次

<?php
$a = "hello";
$b = &$a;
$c = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
?>

PHP5:

a: (refcount=2, is_ref=1)='hello'
b: (refcount=2, is_ref=1)='hello'
c: (refcount=1, is_ref=0)='hello'   //c与a不再共享一个zval容器

c与a不再共享一个zval容器,单独创建了一个zval容器给变量c

example 4.2
同理,赋值之后修改原来的变量也可以导致a,b不再共享同一个zval容器

<?php
$a = "hello";
$b = $a;
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
$a = "world";
xdebug_debug_zval( 'a' );
xdebug_debug_zval( 'b' );
?>

PHP5输出:

a: (refcount=2, is_ref=0)='hello'
b: (refcount=2, is_ref=0)='hello'
a: (refcount=1, is_ref=0)='world'
b: (refcount=1, is_ref=0)='hello'

example 5
引用两次

<?php
$a = "hello";
$b = &$a;
$c = &$a;
xdebug_debug_zval('a');
?>

PHP5:

a: (refcount=3, is_ref=1)='hello' 

这里很合理,被引用了所以is_ref为True

example 6
赋值两次

<?php
$a = "hello";
$b = $a;
$c = $a;
xdebug_debug_zval('a');
?>

PHP5输出:
a: (refcount=3, is_ref=0)='hello' 这里也很合理

example 7
赋值两次 引用一次

<?php
$a = "hello";
$b = $a;
$c = $a;
$d =&$a;
xdebug_debug_zval('a');
?>

PHP5:
a: (refcount=2, is_ref=1)='hello' 引用之后,a,b,c不再共享同一个zval容器

example 7.1
赋值两次 引用一次

<?php
$a = "hello";
$b = $a;
$c = $a;
$d =&$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
$b = "world";
xdebug_debug_zval('b');
xdebug_debug_zval('c');
?>

PHP5输出:

a: (refcount=2, is_ref=1)='hello'
b: (refcount=2, is_ref=0)='hello'
c: (refcount=2, is_ref=0)='hello'
b: (refcount=1, is_ref=0)='world'
c: (refcount=1, is_ref=0)='hello'  

引用之后,a,b,c不再共享同一个zval容器,但b,c仍然共享同一个zval容器。

example 8
引用加赋值一次,使用unset
当refcount=0时,变量容器会被销毁,当变量离开了它的作用域,refcount就会减1,例如函数执行结束或者调用unset()时。

<?php
$a = "hello";
$b = &$a;
$c = $a;
xdebug_debug_zval('a');
unset($a);
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
?>

PHP5:

a: (refcount=2, is_ref=1)='hello'    //unset只是将变量a从符号表中删除,b仍在符号表中,c在另一个zval容器中。
a: no such symbol
b: (refcount=1, is_ref=0)='hello'
c: (refcount=1, is_ref=0)='hello'
复合类型

与标量(scalar)类型的值不同,数组array和对象object类型的成员和属性也会有一个zval容器,但这些容器是附属于自己的zval容器的。

example 9

<?php
$a = array( 'name' => ‘zcc', 'id' => 100 );
xdebug_debug_zval( 'a' );
?>

PHP5输出:

a: (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='zcc', 'id' => (refcount=1, is_ref=0)=100) 

//对每个zval容器而言,与普通变量的zval容器一样

example 10
添加已经存在的元素到数组

<?php
$a = array( 'name' => ‘zcc', 'id' => 100 );
$a['id2'] = $a['id'];
xdebug_debug_zval( 'a' );
?>

PHP5:

a : (refcount=1, is_ref=0)=array ('name' => (refcount=1, is_ref=0)='zcc', 'id' => (refcount=2, is_ref=0)=100, 'id2' => (refcount=2, is_ref=0)=100)

example 11
数组某元素引用数组本身

<?php
$a = array( 'name' => ‘zcc', 'id' => 100 );
$a[] = &$a;
xdebug_debug_zval( 'a' );
?>

PHP5:

a: (refcount=2, is_ref=1)=array ('name' => (refcount=1, is_ref=0)='zcc', 'id' => (refcount=1, is_ref=0)=100, 0 => (refcount=2, is_ref=1)=...)

这里的…表示递归调用

example 12
数组某元素引用数组本身,并执行unset

<?php
$a = array( 'name' => ‘zcc', 'id' => 100 );
$a[] = &$a;
unset($a);
xdebug_debug_zval( 'a' );
?>

PHP5:

a:  no such symbol  

unset a之前,a的refcount等于2,unset之后减1,也就是说这个zval容器并没有被清理掉,而是继续占用着内存。对于一些不会结束的脚本、进程,无法释放的内存就可能导致内存泄漏。

PHP7.0

PHP7.0的zval容器有了很大的改进,首先zval需要的内存不再是单独从堆上分配。复杂数据类型的引用计数由自身存储。

1简单数据类型不需要单独分配内存,也不需要计数
2不会再有两次计数的情况,在对象中,只有对象自身存储的计数是有效的
3计数由自身存储,就可以和非zval结果的数据共享,例如zval和hashtable key
4间接访问需要的指针数减少了

zval 需要的内存不再单独从堆上分配。但是显然总要有地方来存储它,所以会存在哪里呢?实际上大多时候它还是位于堆中(所以前文中提到的地方重点不是堆,而是单独分配,划重点单独分配),
只不过是嵌入到其他的数据结构中的,比如 hashtable 和 bucket 现在就会直接有一个 zval 字段而不是指针。所以函数表编译变量和对象属性在存储时会是一个 zval 数组并得到一整块内存而不是散落在各处的 zval 指针。之前的 zval * 现在都变成了 zval。

现在普通变量赋值时,不会共享一个zval容器,而是会直接分离成两个。
对于数组,php7对于不同数组变量生成两个zval容器,但数组中的内容使用的是同一个zval容器,直到里面的内容发生变化

<?php
$a = [];
$b = $a;   // $a = zval_1(type=IS_ARRAY) -> zend_array_1(refcount=2,value=[])
          // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=2,value=[])
$a[] = 1;   // $a = zval_1(type=IS_ARRAY) -> zend_array_2(refcount=2,value=[1])
          // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=2,value=[])
unset($a);  // $a = zval_1(type=IS_UNDEF)
          // $b = zval_2(type=IS_ARRAY) -> zend_array_1(refcount=2,value=[])

网上有很多关于zval容器底层的知识,但我觉得这些都不是我要的重点,我只需要知道怎么运作的就可以了(主要是我看C++源码看的头疼,看完好像也没什么收获,因此不写下来了)

参考的文章:
https://www.cnblogs.com/lovehappying/p/3679356.html
https://www.cnblogs.com/chenpingzhao/p/4498988.html 这篇写的特别好,建议重点看
还有一些误导我的文章,我就不贴了,头疼

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值