PHP 底层1-引用计数器

讲到引用计数器之前,先说一下PHP变量的C语言实现。如下,几个变量的结构体和联合体:

zvalue_value 联合体:

typedef union _zvalue_value {  
    long lval;
    double dval;
    struct {  
        char *val;  
 	int len;  
    } str;  
    HashTable *ht;
    zend_object_value obj;  
} zvalue_value;

zval 结构:

struct _zval_struct {  
    zvalue_value value; /* 存储了变量的实际数据 */
    zend_uint refcount; /* 引用计数,记录引用数 */
    zend_uchar type; /* 表示这个变量的类型 */
    zend_uchar is_ref; /* 标志这个容器是否真正的引用 */
};
zval可以看成一个容器,zvalue_value是该容器存储变量值的联合体,zend根据type值来决定访问value的哪个成员,可用值如下:

IS_NULL

N/A

IS_LONG

对应value.lval

IS_DOUBLE

对应value.dval

IS_STRING

对应value.str

IS_ARRAY

对应value.ht

IS_OBJECT

对应value.obj

IS_BOOL

对应value.lval.

IS_RESOURCE

对应value.lval


根据这个表格可以发现:
1) PHP的数组其实就是一个HashTable,这就解释了为什么PHP能够支持关联数组了;
2) Resource就是一个long值,它里面存放的通常是个指针、一个内部数组的index或者其它什么只有创建者自己才知道的东西,可以将其视作一个handle。

写复制(copy on write)

<?php
$var = "laruence";
$var_dup = $var;
$var = 1;
?>
PHP在修改一个变量以前,会首先查看这个变量的refcount,如果 refcount大于1,PHP就会执行一个分离的例程, 对于上面的代码,当执行到第三行的时候,PHP发现$var指向的zval的 refcount大于1,那么PHP就会复制一个新的zval出来,将原zval的 refcount减1,并修改symbol_table,使得$var和$var_dup分离(Separation)。这个机制就是所谓的copy on write(写时复制)。

写改变(change on write)

<?php
$var = "laruence";
$var_ref = &$var;
$var_ref = 1;
?>
开始在zval里面我们看到一个字段is_ref,到底如何是怎样产生作用的呢?
现在我们知道,当使用变量复制的时候 ,PHP内部并不是真正的复制,而是采用指向相同的结构来尽量节约开销。那么,对于PHP中的引用,那又是如何实现呢?
以上代码结束后,$var也会被间接的修改为1,这个过程称作(change on write:写时改变)。那么zend是怎么知道,这次的复制是不需要Separation的呢?
这个时候就要用到zval中的 is_ref字段了:
对于上面的代码,当第二行执行以后,$var所代表的zval的refcount变为2,并且同时置is_ref为1。
当使用引用时,PHP会把is_ref即程序会如下判断该引用是否真实引用,PHP先检查var_ref代表的zval的is_ref字段,如果为1,则不分离,大体逻辑示意如下:
<?php
if((*val)->is_ref || (*val)->refcount<2){
    //不执行Separation
    ... ; //process
}
?>

两种方式到底怎样使用,啥时候使用?

<?php
$var = "laruence";
$var_dup = $var;
$var_ref = &$var;
?>
对于上面的代码,存在一对copy on write的变量$var和$var_dup,和一对change on write机制的变量对$var和$var_ref,这个情况又是如何运作的呢?
当第二行执行的时候,和前面讲过的一样,$var_dup 和 $var 指向相同的zval, refcount为2。
当执行第三行的时候,PHP发现要操作的zval的refcount大于1,则,PHP会执行Separation,将$var_dup分离出去,并将$var和$var_ref做change on write关联。即refcount=2,is_ref=1。

数值巨大变量处理

如果我们在php中进行参数传递的时候,是否有必要对传递内容巨大的数组心存警惕,担心内存的大量流失?
<?php
function countChina($persons) {
    $count=0;
    foreach($persons as $person) {
        if($person['nation'] == 'china') $count++;
    }
    return $count;
}
?>
例如这段统计一群人中有多少个中国国籍的时候,如果有一个10W的人的数组要传进去,你是否会担心被复制后传到函数里,导致内存占用瞬间翻倍?也许你会在参数那里加一个&表示引用?
php的设计者在这里有一个很巧妙的设计,引入了一个copy on write的概念,如果不去修改$perons对象的内容,并不会有复制行为发生,内存也不会double。 但如果想在函数里面修改数据,便于函数后面的处理,这时内存会double吗?如下:
<?php
function countChina($persons){
    $count=0;
    foreach($persons as $person){
        if($person['nation'] == 'china'){
            $person['score'] = 10;
            $count++;
        }
    }
    //利用score做一些处理...
    return $count;
}
如果你不在参数里加&的话,内存消耗会逐步增加,在返回以前,内存的占用会double。因为为了满足write需要,PHP进行了copy,这个copy不是一次完成的,在改变第一个$persons成员的时候,会将$persons数组和其成员的地址复制过来,并不会把所有成员内容都复制过来,随着$persons成员的一个个被write,一个个也都被copy过来,内存占用线性增加,到循环结束时,double了!如果确实要修改内容,又想避免内存消耗,又不怕影响参数变量在其他地方的使用,那就在参数里加&吧。
要释放被占用的内存变量,用unset($persons)或者$persons=null都可以迅速地释放掉,你可以使用memory_get_usage进行监控。

unset的作用

unset()并非一个函数,而是一种语言结构,这个可以通过查看编译生成的opcode看到区别,unset对应的不是一个函数调用的opcode。那么unset到底做了什么? 在unset对应的opcode的handler中可以看到相关内容,主要的操作时从当前符号表中删除参数中的符号,比如在全局代码中执行unset($a),那么将会在全局符号表中删除a这个符号。全局符号表是一张哈希表,建立这张表的时候会提供一个表中的项的析构函数,当我们从符号表中删除a的时候,会对符号a指向的项(这里是zval的指针)调用这个析构函数,这个析构函数的主要功能是将a对应的zval的 refcount1,如果 refcount变成了0,那么释放这个zval。所以当我们调用unset的时候,不一定能释放变量所占的内存空间,只有当这个变量对应的zval没有别的变量指向它的时候,才会释放掉zval,否则只是对 refcount进行减1操作。

转载于:https://my.oschina.net/wzwitblog/blog/156257

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值