更多《PHP7核心剖析》系列文章:https://github.com/pangudashu/php7-internal
3.4.2 對象
對象是類的執行個體,PHP中要建立一個類的執行個體,必須使用 new 關鍵字。類應在被執行個體化之前定義(某些情況下則必須這樣,比如3.4.1最後那幾個例子)。
3.4.2.1 對象的資料結構
對象的資料結構非常簡單:
typedef struct _zend_object zend_object;
struct _zend_object {
zend_refcounted_h gc; //引用計數
uint32_t handle;
zend_class_entry *ce; //所屬類
const zend_object_handlers *handlers; //對象的一些操作接口
HashTable *properties;
zval properties_table[]; //普通屬性值數組
};
幾個主要的成員:
(1)handle: 一次request期間對象的編号,每個對象都有一個唯一的編号,與建立先後順序有關,主要在垃圾回收時用,下面會詳細說明。
(2)ce: 所屬類的zend_class_entry。
(3)handlers: 這個儲存的對象相關操作的一些函數指針,比如成員屬性的讀寫、成員方法的擷取、對象的銷毀/克隆等等,這些操作接口都有預設的函數。
struct _zend_object_handlers {
int offset;
zend_object_free_obj_t free_obj; //釋放對象
zend_object_dtor_obj_t dtor_obj; //銷毀對象
zend_object_clone_obj_t clone_obj;//複制對象
zend_object_read_property_t read_property; //讀取成員屬性
zend_object_write_property_t write_property;//修改成員屬性
...
}
//預設值處理handler
ZEND_API zend_object_handlers std_object_handlers = {
,
zend_object_std_dtor, /* free_obj */
zend_objects_destroy_object, /* dtor_obj */
zend_objects_clone_obj, /* clone_obj */
zend_std_read_property, /* read_property */
zend_std_write_property, /* write_property */
zend_std_read_dimension, /* read_dimension */
zend_std_write_dimension, /* write_dimension */
zend_std_get_property_ptr_ptr, /* get_property_ptr_ptr */
NULL, /* get */
NULL, /* set */
zend_std_has_property, /* has_property */
zend_std_unset_property, /* unset_property */
zend_std_has_dimension, /* has_dimension */
zend_std_unset_dimension, /* unset_dimension */
zend_std_get_properties, /* get_properties */
zend_std_get_method, /* get_method */
NULL, /* call_method */
zend_std_get_constructor, /* get_constructor */
zend_std_object_get_class_name, /* get_class_name */
zend_std_compare_objects, /* compare_objects */
zend_std_cast_object_tostring, /* cast_object */
NULL, /* count_elements */
zend_std_get_debug_info, /* get_debug_info */
zend_std_get_closure, /* get_closure */
zend_std_get_gc, /* get_gc */
NULL, /* do_operation */
NULL, /* compare */
}
(4)properties: 普通成員屬性哈希表,對象建立之初這個值為NULL,主要是在動态定義屬性時會用到,與properties_table有一定關系,下一節我們将單獨說明,這裡暫時忽略。
(5)properties_table: 成員屬性數組,還記得我們在介紹類一節時提過非靜态屬性存儲在對象結構中嗎?就是這個properties_table!注意,它是一個數組,
zend_object
是個變長結構體,配置設定時會根據非靜态屬性的數量确定其大小。
3.4.2.2 對象的建立
PHP中通過
new + 類名
建立一個類的執行個體,我們從一個例子分析下對象建立的過程中都有哪些操作。
class my_class
{
const TYPE = ;
public $name = "pangudashu";
public $ids = array();
}
$obj = new my_class();
類的定義就不用再說了,我們隻看
$obj = new my_class();
這一句,這條語句包括兩部分:執行個體化類、指派,下面看下執行個體化類的文法規則:
new_expr:
T_NEW class_name_reference ctor_arguments
{ $$ = zend_ast_create(ZEND_AST_NEW, $, $); }
| T_NEW anonymous_class
{ $$ = $; }
;
從文法規則可以很直覺的看出此文法的兩個主要部分:類名、參數清單,編譯器在解析到執行個體化類時就建立一個
ZEND_AST_NEW
類型的節點,後面編譯為opcodes的過程我們不再細究,這裡直接看下最終生成的opcodes。

你會發現執行個體化類産生了兩條opcode(實際可能還會更多):ZEND_NEW、ZEND_DO_FCALL,除了建立對象的操作還有一條函數調用的,沒錯,那條就是調用
構造方法
的操作。
根據opcode、操作數類型可知
ZEND_NEW
對應的處理handler為
ZEND_NEW_SPEC_CONST_HANDLER()
:
static int ZEND_NEW_SPEC_CONST_HANDLER(zend_execute_data *execute_data)
{
zval object_zval;
zend_function *constructor;
zend_class_entry *ce;
...
//第1步:根據類名查找zend_class_entry
ce = zend_fetch_class_by_name(Z_STR_P(EX_CONSTANT(opline->op1)), ...);
...
//第2步:建立&初始化一個這個類的對象
if (UNEXPECTED(object_init_ex(&object_zval, ce) != SUCCESS)) {
HANDLE_EXCEPTION();
}
//第3步:擷取構造方法
//擷取構造方法函數,實際就是直接取zend_class_entry.constructor
//get_constructor => zend_std_get_constructor()
constructor = Z_OBJ_HT(object_zval)->get_constructor(Z_OBJ(object_zval));
if (constructor == NULL) {
...
//此opcode之後還有傳參、調用構造方法的操作
//是以如果沒有定義構造方法則直接跳過這些操作
ZEND_VM_JMP(OP_JMP_ADDR(opline, opline->op2));
}else{
//定義了構造方法
//初始化調用構造函數的zend_execute_data
zend_execute_data *call = zend_vm_stack_push_call_frame(...);
call->prev_execute_data = EX(call);
EX(call) = call;
...
}
}
從上面的建立對象的過程看整個流程主要分為三步:首先是根據類名在EG(class_table)中查找對應zend_class_entry、然後是建立并初始化一個對象、最後是初始化調用構造函數的zend_execute_data。
我們再具體看下第2步建立、初始化對象的操作,
object_init_ex(&object_zval, ce)
最終調用的是
_object_and_properties_init()
。
//zend_API.c
ZEND_API int _object_and_properties_init(zval *arg, zend_class_entry *class_type, ...)
{
//檢查類是否可以執行個體化
...
//使用者自定義的類create_object都是NULL
//隻有PHP幾個内部的類有這個值,比如exception、error等
if (class_type->create_object == NULL) {
//配置設定一個對象
ZVAL_OBJ(arg, zend_objects_new(class_type));
...
//初始化成員屬性
object_properties_init(Z_OBJ_P(arg), class_type);
} else {
//調用自定義的建立object的鈎子函數
ZVAL_OBJ(arg, class_type->create_object(class_type));
}
return SUCCESS;
}
還記得上一節介紹zend_class_entry時有幾個自定義的鈎子函數嗎?如果定義了
create_object
這個地方就會調用自定義的函數來建立zend_object,這種情況通常發生在核心或擴充中定義的内部類(當然使用者自定義類也可以修改,但一般不會那樣做);使用者自定義類在這個地方又具體分了兩步:配置設定對象結構、初始化成員屬性,我們繼續看下這裡面的處理。
(1)配置設定對象結構:zend_object
//zend_objects.c
ZEND_API zend_object *zend_objects_new(zend_class_entry *ce)
{
//配置設定zend_object
zend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));
zend_object_std_init(object, ce);
//設定對象的操作handler為std_object_handlers
object->handlers = &std_object_handlers;
return object;
}
有個地方這裡需要特别注意:配置設定對象結構的記憶體并不僅僅是zend_object的大小。我們在3.4.2.1介紹properties_table時說過這是一個變長數組,它用來存放非靜态屬性的值,是以配置設定zend_object時需要加上非靜态屬性所占用的記憶體大小:
zend_object_properties_size()
(實際就是zend_class_entry.default_properties_count)。
另外這裡還有一個關鍵操作:将object編号并插入EG(objects_store).object_buckets數組。zend_object有個成員:handle,這個值在一次request期間所有執行個體化對象的編号,每調用
zend_objects_new()
執行個體化一個對象就會将其插入到object_buckets數組中,其在數組中的下标就是handle。這個過程是在
zend_objects_store_put()
中完成的。
//zend_objects_API.c
ZEND_API void zend_objects_store_put(zend_object *object)
{
int handle;
if (EG(objects_store).free_list_head != -) {
//這種情況主要是gc中會将中間一些object銷毀,空出一些bucket位置
//然後free_list_head就指向了第一個可用的bucket位置
//後面可用的儲存在第一個空閑bucket的handle中
handle = EG(objects_store).free_list_head;
EG(objects_store).free_list_head = GET_OBJ_BUCKET_NUMBER(EG(objects_store).object_buckets[handle]);
} else {
if (EG(objects_store).top == EG(objects_store).size) {
//擴容
}
//遞增加1
handle = EG(objects_store).top++;
}
object->handle = handle;
//存入object_buckets數組
EG(objects_store).object_buckets[handle] = object;
}
typedef struct _zend_objects_store {
zend_object **object_buckets; //對象數組
uint32_t top; //目前全部object數
uint32_t size; //object_buckets大小
int free_list_head; //第一個可用object_buckets位置
} zend_objects_store;
将所有的對象儲存在
EG(objects_store).object_buckets
中的目的是用于垃圾回收(不确定是不是還有其它的作用),防止出現循環引用而導緻記憶體洩漏的問題,這個機制後面章節會單獨介紹,這裡隻要記得有這麼個東西就行了。
(2)初始化成員屬性
ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type)
{
if (class_type->default_properties_count) {
zval *src = class_type->default_properties_table;
zval *dst = object->properties_table;
zval *end = src + class_type->default_properties_count;
//将非靜态屬性值從:
//zend_class_entry.default_properties_table複制到zend_object.properties_table
do {
ZVAL_COPY(dst, src);
src++;
dst++;
} while (src != end);
object->properties = NULL;
}
}
這一步操作是将非靜态屬性的值從
zend_class_entry.default_properties_table -> zend_object.properties_table
,當然這裡不是硬拷貝,而是淺複制(增加引用),兩者目前指向的value還是同一份,除非對象試圖改寫指向的屬性值,那時将觸發寫時複制機制重新拷貝一份。
上面那個例子,類有兩個普通屬性: name、 ids,假如我們執行個體化了兩個對象,那麼zend_class_entry與zend_object中普通屬性值的關系如下圖所示。
以上就是執行個體化一個對象的過程,總結一下具體的步驟:
* step1: 首先根據類名去EG(class_table)中找到具體的類,即zend_class_entry
* step2: 配置設定zend_object結構,一起配置設定的還有普通非靜态屬性值的記憶體
* step3: 初始化對象的非靜态屬性,将屬性值從zend_class_entry淺複制到對象中
* step4: 查找目前類是否定義了構造函數,如果沒有定義則跳過執行構造函數的opcode,否則為調用構造函數的執行進行一些準備工作(配置設定zend_execute_data)
* step5: 執行個體化完成,傳回新執行個體化的對象(如果傳回的對象沒有變量使用則直接釋放掉了)
3.4.2.3 對象的複制
PHP中普通變量的複制可以通過直接指派完成,比如:
$a = array();
$b = $a;
但是對象無法這麼進行複制,僅僅通過指派傳遞對象,它們指向的都是同一個對象,修改時也不會發生硬拷貝。比如上面這個例子,我們把
$a
指派給
$b
,然後如果我們修改
$b
的内容,那麼這時候會進行value分離,
$a
的内容是不變的,但是如果是把一個對象指派給了另一個變量,這倆對象不管哪一個修改另外一個都随之改變。
class my_class
{
public $arr = array();
}
$a = new my_class;
$b = $a;
$b->arr[] = ;
var_dump($a === $b);
====================
輸出:bool(true)
還記得我們在《2.1.3.2 寫時複制》一節講過zval有個類型掩碼: type_flag 嗎?其中有個是否可複制的辨別:IS_TYPE_COPYABLE ,copyable的意思是當value發生duplication時是否需要或能夠copy,而object的類型是不能複制(不清楚的可以翻下前面的章節),是以我們不能簡單的通過指派語句進行對象的複制。
PHP提供了另外一個關鍵詞來實作對象的複制:clone。
$copy_of_object = clone $object;
clone
出的對象就與原來的對象完全隔離了,各自修改都不會互相影響,另外如果類中定義了
__clone()
魔術方法,那麼在
clone
時将調用此函數。
clone
的實作比較簡單,通過
zend_object.clone_obj
(即:
zend_objects_clone_obj()
)完成。
//zend_objects.c
ZEND_API zend_object *zend_objects_clone_obj(zval *zobject)
{
zend_object *old_object;
zend_object *new_object;
old_object = Z_OBJ_P(zobject);
//重新配置設定一個zend_object
new_object = zend_objects_new(old_object->ce);
//淺複制properties_table、properties
//如果定義了__clone()則調用此方法
zend_objects_clone_members(new_object, old_object);
return new_object;
}
3.4.2.4 對象比較
當使用比較運算符(==)比較兩個對象變量時,比較的原則是:如果兩個對象的屬性和屬性值 都相等,而且兩個對象是同一個類的執行個體,那麼這兩個對象變量相等;而如果使用全等運算符(===),這兩個對象變量一定要指向某個類的同一個執行個體(即同一個對象)。
PHP中對象間的”==”比較通過函數
zend_std_compare_objects()
處理。
static int zend_std_compare_objects(zval *o1, zval *o2)
{
...
if (zobj1->ce != zobj2->ce) {
return ; /* different classes */
}
if (!zobj1->properties && !zobj2->properties) {
//逐個比較properties_table
...
}else{
//比較properties
return zend_compare_symbol_tables(zobj1->properties, zobj2->properties);
}
}
“===”的比較通過函數
zend_is_identical()
處理,比較簡單,這裡不再展開。
3.4.2.5 對象的銷毀
object與string、array等類型不同,它是個符合類型,是以它的銷毀過程更加複雜,指派、函數調用結束或主動unset等操作中如果發現object引用計數為0則将觸發銷毀動作。
//情況1
$obj1 = new my_function();
$obj1 = ; //此時将斷開對zend_object的引用,如果refcount=0則銷毀
//情況2
function xxxx(){
$obj1 = new my_function();
...
return null; //清理局部變量時如果發現$obj1引用為0則銷毀
}
//情況3
$obj1 = new my_function();
//整個腳本結束,清理全局變量時
//情況4
$obj1 = new my_function();
unset($obj1);
上面這幾個都是比較常見的會進行變量銷毀的情況,銷毀一個對象由
zend_objects_store_del()
完成,銷毀的過程主要是清理成員屬性、從EG(objects_store).object_buckets中删除、釋放zend_object記憶體等等。
//zend_objects_API.c
ZEND_API void zend_objects_store_del(zend_object *object)
{
//這個函數if嵌套寫的很挫...
...
if (GC_REFCOUNT(object) > ) {
GC_REFCOUNT(object)--;
return;
}
...
//調用dtor_obj,預設zend_objects_destroy_object()
//接着調用free_obj,預設zend_object_std_dtor()
object->handlers->dtor_obj(object);
object->handlers->free_obj(object);
...
ptr = ((char*)object) - object->handlers->offset;
efree(ptr);
}
另外,在減少refcount時如果發現object的引用計數大于0那麼并不是什麼都不做了,還記得2.1.3.4介紹的垃圾回收嗎?PHP變量類型有的會因為循環引用導緻正常的gc無法生效,這種類型的變量就有可能成為垃圾,是以會對這些類型的
zval.u1.type_flag
打上
IS_TYPE_COLLECTABLE
标簽,然後在減少引用時即使refcount大于0也會啟動垃圾檢查,目前隻有object、array兩種類型會使用這種機制。