简书简述

php5和7的对象赋值及zval粗谈


今天看php发展历史,了解到在php4的年代,对象的赋值默认是创建副本,并非是增加对象的引用。就试了下在php5的环境下,怎样将对象一般赋值、引用赋值和拷贝。然后在sf里看到有人提了这么个问题:

class User {     public $name;     public function __construct($name)     {         $this->name = $name;     }     public function __destruct()     {         echo 'I was released~~';     } }  $instance = new User('xiao ming'); $assignment = $instance; $reference = &$instance; $instance = null; var_dump($instance);    // 输出 null var_dump($assignment);  // public 'name' => string 'xiao ming' (length=9) var_dump($reference);   // 输出 null 

发现这个问题挺有意思,就查了下php手册,将引用和垃圾回收机制了解了下,才明白这些代码背后发生了什么。
先解释下为什么$instance置为null后,$reference也变成null了。$reference是对$instance的引用,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.

从这段话我们就明白了,$instance存着指向小明对象的指针的值(这个后面详细讲),而$reference单纯的只是$instance的另一个别名而已,并不像C那样是实例的指针。因此$instance置为null,释放了对user对象的引用后,$reference也自然无法access the same variable了。
而$assignment还能同样输出小明这个对象,是因为$assignment存的内容和$instance一样,都是这个对象的内存地址。这就又有一个问题了,这个小明对象在什么时候会被释放呢?

A PHP variable is stored in a container called a “zval”. A zval container contains, besides the variable’s type and value, two additional bits of information. The first is called “is_ref” and is a boolean value indicating whether or not the variable is part of a “reference set”. This second piece of additional information, called “refcount”, contains how many variable names (also called symbols) point to this one zval container.

这下我们明白了,PHP使用了引用计数(reference count)垃圾回收机制。每个对象都内含一个引用计数器,当有reference连接到对象时,计数器加1。当reference离开生存空间或被设为null时,计数器减1。当某个对象的引用计数器为零时,PHP知道你将不再使用这个对象,释放其所占的内存空间。我们将上边的代码稍作修改,在$instance = null前加几句话(ps:需要装xdebug)

$instance = new User('xiao ming'); $assignment = $instance; $reference = &$instance;  //php5输出结果 xdebug_debug_zval('instance');   // instance:  (refcount=2, is_ref=1) xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0) xdebug_debug_zval('reference');  // reference: (refcount=2, is_ref=1) // php7输出结果 xdebug_debug_zval('instance');   // instance:  (refcount=2, is_ref=1) xdebug_debug_zval('assignment'); // assignment:(refcount=2, is_ref=0) xdebug_debug_zval('reference');  // reference: (refcount=2, is_ref=1)  $instance = null; $assignment = null;   // 这句话执行后立即触发User类的析构函数,输出 I was released~~ 

由于家里和公司的电脑环境一个是php5.6,另个一是php7.1,同样的demo中突然发现php5和php7里assignment的refcount值不一样,就去看了下php对于变量的实现方式,由于我的C功底实在有点差,源码看的很吃力,不足的地方还望各位能多多指教。先看下在php5和php7里对象赋值时的不同处理方式(简化后的模型):

php5和7的对象赋值及zval粗谈

php5.6对象赋值

php5和7的对象赋值及zval粗谈

php7.1对象赋值

若在变量输出前执行unset($instance)和unset($reference),就更容易看出上图所示不同之处了。

$instance = new User('xiao ming'); $assignment = $instance; $reference = &$instance;  unset($instance); //php5输出结果 xdebug_debug_zval('instance');   // instance:  no such symbol xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0) xdebug_debug_zval('reference');  // reference: (refcount=1, is_ref=0) // php7输出结果 xdebug_debug_zval('instance');   // instance:  uninitialized xdebug_debug_zval('assignment'); // assignment:(refcount=2, is_ref=0) xdebug_debug_zval('reference');  // reference: (refcount=1, is_ref=1)  unset($reference); //php5输出结果 xdebug_debug_zval('instance');   // instance:  no such symbol xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0) xdebug_debug_zval('reference');  // reference: no such symbol // php7输出结果 xdebug_debug_zval('instance');   // instance:  uninitialized xdebug_debug_zval('assignment'); // assignment:(refcount=1, is_ref=0) xdebug_debug_zval('reference');  // reference: uninitialized  // unset的作用仅仅是将变量从符号表中删除,并减少所指结构体的refcount值。 

现在来详细谈下php5和php7里zval的不同之处。
php7的zend_object如下:

struct _zend_object {     zend_refcounted   gc;     uint32_t          handle;     zend_class_entry *ce;     const zend_object_handlers *handlers;     HashTable        *properties;     zval              properties_table[1]; }; 

可以看出,php7中的zval已经变成了一个值指针,它要么保存着原始值,要么保存着一个指针(索引)handle,这个索引对应着一个保存原始对象的指针数组中某个元素的地址。也就是说,当对象被创建时,会有一个指针插入到对象存储中并且其索引会保存在 handle 中,当对象被释放时,索引也会被移除。

那么php7为什么还要隔一层handle去访问对象真正的内容呢,而不是直接存着内容的地址。The reason behind this is that during request shutdown, there comes a point where it is no longer safe to run userland code, because the executor is already partially shut down. To avoid this PHP will run all object destructors at an early point during shutdown and prevent them from running at a later point in time. For this a list of all active objects is needed.<摘自https://nikic.github.io/2015/06/19/Internal-value-representation-in-PHP-7-part-2.html,作者是php官方开发组成员Nikita Popov>

并且 handle 对于调试也是很有用的,它让每个对象都有了一个唯一的 ID,这样就很容易区分两个对象是同一个还是只是有相同的内容。虽然 HHVM 没有对象存储的概念,但它也存了对象的 handle。

php5和7的对象赋值及zval粗谈

若将$assignment = $instance;改成$assignment = clone $instance;会发现assignment的ID将变为7

php5中取对象就比较麻烦了。php5的zend_object_value结构如下

typedef struct _zend_object_value {     zend_object_handle handle;     const zend_object_handlers *handlers; } zend_object_value; 

对象句柄(handler)是对象的唯一ID,作为索引用于“对象存储”,对象存储本身是一个存储容器(bucket)的数组,bucket 定义如下:

typedef struct _zend_object_store_bucket {     zend_bool destructor_called;     zend_bool valid;     zend_uchar apply_count;     union _store_bucket {         struct _store_object {             void *object;             zend_objects_store_dtor_t dtor;             zend_objects_free_object_storage_t free_storage;             zend_objects_store_clone_t clone;             const zend_object_handlers *handlers;             zend_uint refcount;             gc_root_buffer *buffered;         } obj;         struct {             int next;         } free_list;     } bucket; } zend_object_store_bucket; 

第一个成员 object 是指向实际对象的指针,现在看下object所指向的结构体,对于普通用户区所定义的object内容通常如下:

typedef struct _zend_object {     zend_class_entry *ce;     HashTable *properties;     zval **properties_table;     HashTable *guards; } zend_object; 

现在我们要从对象 zval 中取出一个元素,先需要取出the object store bucket,然后是 zend object,然后才能通过指针找到对象属性表和 zval。这样这里至少就有 4 层间接访问(并且实际使用中可能最少需要7层)。

简单点说,在php7中的zval,相当于php5的zval*。只不过相对于zval*,直接存储zval,我们可以省掉一次指针解引用。

总结一下,php7新的zval设计不再单独从堆上分配内存并且不自己存储引用计数。需要使用 zval 指针的复杂类型(比如字符串、数组和对象)会自己存储引用计数(php5除了zval本身有引用计数以外,bucket也存储了refcount)。这样就可以有更少的内存分配、更少的间接指针以及更少的内存使用。

补充:
1、php7中简单数据类型不再单独分配内存,也不再计数。所以会有以下情况:

$a = 1; // php5输出结果 xdebug_debug_zval('a);  // (refcount=1, is_ref=0)  // php7输出结果 xdebug_debug_zval('a);  // (refcount=0, is_ref=0) 

2、php5中refcount等于1时,is_ref永远等于0;php7则不会


版权声明:本文为原创文章,转载请注明出处,谢谢~~