因为 之前的文章 有讲过新的数组实现,所以这里就不再详细描述了。虽然最近有些变化导致之前的描述不是十分准确了,但是基本的概念还是一致的。
这里要说的是之前的文章中没有提到的数组相关的概念:不可变数组。其本质上和保留字符类似:没有引用计数且在请求结束之前一直存在(也可能在请求结束之后还存在)。
因为某些内存管理方便的原因,不可变数组只会在开启 opcache 时会使用到。我们来看看实际使用的例子,先看以下的脚本:
<?php for ($i = 0; $i < 1000000; ++$i) { $array[] = ['foo']; } var_dump(memory_get_usage());
开启 opcache 时,以上代码会使用 32MB 的内存,不开启的情况下因为 $array 每个元素都会复制一份 ['foo'] ,所以需要 390MB。这里会进行完整的复制而不是增加引用计数值的原因是防止 zend 虚拟机操作符执行的时候出现共享内存出错的情况。我希望不使用 opcache 时内存暴增的问题以后能得到改善。
PHP5 中的对象
在了解 PHP7 中的对象实现直线我们先看一下 PHP5 的并且看一下有什么效率上的问题。PHP5 中的 zval 会存储一个 zend_object_value 结构,其定义如下:
typedef struct _zend_object_value { zend_object_handle handle; const zend_object_handlers *handlers; } zend_object_value;
handle 是对象的唯一 ID,可以用于查找对象数据。 handles 是保存对象各种属性方法的虚函数表指针。通常情况下 PHP 对象都有着同样的 handler 表,但是 PHP 扩展创建的对象也可以通过操作符重载等方式对其行为自定义。
对象句柄(handler)是作为索引用于『对象存储』,对象存储本身是一个存储容器(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;
这个结构体包含了很多东西。前三个成员只是些普通的元数据(对象的析构函数是否被调用过、bucke 是否被使用过以及对象被递归调用过多少次)。接下来的联合体用于区分 bucket 是处于使用中的状态还是空闲状态。上面的结构中最重要的是 struct _store_object 子结构体:
第一个成员 object 是指向实际对象(也就是对象最终存储的位置)的指针。对象实际并不是直接嵌入到对象存储的 bucket 中的,因为对象不是定长的。对象指针下面是三个用于管理对象销毁、释放与克隆的操作句柄(handler)。这里要注意的是 PHP 销毁和释放对象是不同的步骤,前者在某些情况下有可能会被跳过(不完全释放)。克隆操作实际上几乎几乎不会被用到,因为这里包含的操作不是普通对象本身的一部分,所以(任何时候)他们在每个对象中他们都会被单独复制(duplicate)一份而不是共享。
这些对象存储操作句柄后面是一个普通的对象 handlers 指针。存储这几个数据是因为有时候可能会在 zval 未知的情况下销毁对象(通常情况下这些操作都是针对 zval 进行的)。
bucket 也包含了 refcount 的字段,不过这种行为在 PHP5 中显得有些奇怪,因为 zval 本身已经存储了引用计数。为什么还需要一个多余的计数呢?问题在于虽然通常情况下 zval 的『复制』行为都是简单的增加引用计数即可,但是偶尔也会有深度复制的情况出现,比如创建一个全新的 zval 但是保存同样的 zend_object_value 。这种情况下两个不同的 zval 就用到了同一个对象存储的 bucket,所以 bucket 自身也需要进行引用计数。这种『双重计数』的方式是 PHP5 的实现内在的问题。GC 根缓冲区中的 buffered 指针也是由于同样的原因才需要进行完全复制(duplicate)。
现在看看对象存储中指针指向的实际的 object 的结构,通常情况下用户层面的对象定义如下:
typedef struct _zend_object { zend_class_entry *ce; HashTable *properties; zval **properties_table; HashTable *guards; } zend_object;
zend_class_entry 指针指向的是对象实现的类原型。接下来的两个元素是使用不同的方式存储对象属性。动态属性(运行时添加的而不是在类中定义的)全部存在 properties 中,不过只是属性名和值的简单匹配。