PHP 7 と PHP 5 のオブジェクトの簡単な比較

青灯夜游
リリース: 2023-04-10 16:30:01
転載
2929 人が閲覧しました

この記事では、PHP 7 と PHP 5 のオブジェクトを理解し、それらを比較して両者の違いを確認します。

PHP 7 と PHP 5 のオブジェクトの簡単な比較

1. クラスの概要

PHP のクラス、インターフェイス、特性はすべて、次の zend_class_entry 構造体を使用して実装されます。最下層

struct _zend_class_entry {
	char type;
	const char *name;
	zend_uint name_length;
	struct _zend_class_entry *parent;
	int refcount;
	zend_uint ce_flags;

	HashTable function_table;
	HashTable properties_info;
	zval **default_properties_table;
	zval **default_static_members_table;
	zval **static_members_table;
	HashTable constants_table;
	int default_properties_count;
	int default_static_members_count;

	union _zend_function *constructor;
	union _zend_function *destructor;
	union _zend_function *clone;
	union _zend_function *__get;
	union _zend_function *__set;
	union _zend_function *__unset;
	union _zend_function *__isset;
	union _zend_function *__call;
	union _zend_function *__callstatic;
	union _zend_function *__tostring;
	union _zend_function *serialize_func;
	union _zend_function *unserialize_func;

	zend_class_iterator_funcs iterator_funcs;

	/* handlers */
	zend_object_value (*create_object)(zend_class_entry *class_type TSRMLS_DC);
	zend_object_iterator *(*get_iterator)(zend_class_entry *ce, zval *object, int by_ref TSRMLS_DC);
	int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type TSRMLS_DC); /* a class implements this interface */
	union _zend_function *(*get_static_method)(zend_class_entry *ce, char* method, int method_len TSRMLS_DC);

	/* serializer callbacks */
	int (*serialize)(zval *object, unsigned char **buffer, zend_uint *buf_len, zend_serialize_data *data TSRMLS_DC);
	int (*unserialize)(zval **object, zend_class_entry *ce, const unsigned char *buf, zend_uint buf_len, zend_unserialize_data *data TSRMLS_DC);

	zend_class_entry **interfaces;
	zend_uint num_interfaces;
	
	zend_class_entry **traits;
	zend_uint num_traits;
	zend_trait_alias **trait_aliases;
	zend_trait_precedence **trait_precedences;

	union {
		struct {
			const char *filename;
			zend_uint line_start;
			zend_uint line_end;
			const char *doc_comment;
			zend_uint doc_comment_len;
		} user;
		struct {
			const struct _zend_function_entry *builtin_functions;
			struct _zend_module_entry *module;
		} internal;
	} info;
};
ログイン後にコピー

zend_class_entry 構造体には多数のポインターとハッシュテーブルが含まれているため、構造体自体が大量のメモリ空間を占有することになります。さらに、構造内のポインタは対応するメモリ空間を個別に割り当てる必要があるため、ある程度のメモリ空間が消費されます。

⒈ 開発者定義クラスと PHP 内部定義クラスの比較

# いわゆる開発者定義クラスは、PHP 言語を使用して定義されたクラスであり、PHP 内部的には定義されたクラスは、PHP ソース コードで定義されたクラス、または PHP 拡張機能で定義されたクラスを指します。 2 つの間の最も重要な違いは、ライフ サイクル異なる:

  • php-fpm を例に挙げます。リクエストが来ると、PHP は開発者によって定義されたクラスを解析します。対応するメモリ空間を割り当てます。その後、リクエストの処理中に、PHP はこれらのクラスに対応する呼び出しを行い、最終的に、リクエストの処理後、これらのクラスは破棄され、それらに割り当てられたメモリ領域が解放されます。

メモリ領域を節約するために、実際には使用されない一部のクラスをコード内で定義しないでください。オートロードを使用すると、実際には使用されないクラスを保護できます。オートロードはクラスが使用されるときにのみロードして解析するためです。ただし、これにより、コードのコンパイル段階からコードの実行までのクラスの解析およびロードのプロセスが遅延します。コードの段階、パフォーマンスに影響します

OPCache 拡張機能がオンになっている場合でも、開発者がカスタマイズしたクラスはリクエストの到着時に解析およびロードされ、完了時に破棄されることにも注意してください。 OPCache はこれら 2 つの段階の速度を向上させただけです

  • #PHP の内部定義クラスは異なります。引き続き php-fpm を例に挙げると、php-fpm プロセスが開始されると、PHP は php-fpm プロセスが終了するまで一度にこれらのクラスにメモリ領域を永続的に割り当てます (メモリ リークを避けるため、php-fpm は処理後一定期間破棄してリクエスト数後に再起動)
if (EG(full_tables_cleanup)) {
	zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function_full TSRMLS_CC);
	zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class_full TSRMLS_CC);
} else {
	zend_hash_reverse_apply(EG(function_table), (apply_func_t) clean_non_persistent_function TSRMLS_CC);
	zend_hash_reverse_apply(EG(class_table), (apply_func_t) clean_non_persistent_class TSRMLS_CC);
}

static int clean_non_persistent_class(zend_class_entry **ce TSRMLS_DC)
{
	return ((*ce)->type == ZEND_INTERNAL_CLASS) ? ZEND_HASH_APPLY_STOP : ZEND_HASH_APPLY_REMOVE;
}
ログイン後にコピー

上記のコードからもわかるように、PHP内で定義されたクラスはリクエスト終了時に破棄されません。また、PHP 拡張機能で定義されているクラスも PHP 内で定義されているクラスの範疇に属するため、メモリ容量節約の観点から、使用しない一部の拡張機能は開かないでください。拡張機能が有効になると、php-fpm プロセスの開始時に拡張機能で定義されたクラスが解析されてロードされるためです。

便宜上、\Exception を継承して例外をカスタマイズすることがよくあります。ただし、zend_class_entry 構造体は非常に大きいため、利便性を向上させる一方で多くのメモリを消費します。

#⒉ クラス バインディング

クラス バインディング ポインタ 最も重要事はクラスデータの準備処理です

PHPの内部で定義されているクラスは、クラスを登録した時点でバインディング処理が完了します。このプロセスは、PHP スクリプトが実行される前に発生し、php-fpm プロセス全体の存続期間中に 1 回だけ発生します。

親クラスを継承せず、インターフェイスも実装せず、特性も使用しないクラスの場合、バインド プロセスは PHP コードの編集段階で発生し、あまり多くのリソースを消費しません。この種のクラス バインディングでは通常、クラスを class_table に登録し、そのクラスに抽象メソッドが含まれているが抽象型として宣言されていないかどうかを確認するだけで済みます。

void zend_do_early_binding(TSRMLS_D) /* {{{ */
{
	zend_op *opline = &CG(active_op_array)->opcodes[CG(active_op_array)->last-1];
	HashTable *table;

	while (opline->opcode == ZEND_TICKS && opline > CG(active_op_array)->opcodes) {
		opline--;
	}

	switch (opline->opcode) {
		case ZEND_DECLARE_FUNCTION:
			if (do_bind_function(CG(active_op_array), opline, CG(function_table), 1) == FAILURE) {
				return;
			}
			table = CG(function_table);
			break;
		case ZEND_DECLARE_CLASS:
			if (do_bind_class(CG(active_op_array), opline, CG(class_table), 1 TSRMLS_CC) == NULL) {
				return;
			}
			table = CG(class_table);
			break;
		case ZEND_DECLARE_INHERITED_CLASS:
			{
				/*... ...*/
			}
		case ZEND_VERIFY_ABSTRACT_CLASS:
		case ZEND_ADD_INTERFACE:
		case ZEND_ADD_TRAIT:
		case ZEND_BIND_TRAITS:
			/* We currently don't early-bind classes that implement interfaces */
			/* Classes with traits are handled exactly the same, no early-bind here */
			return;
		default:
			zend_error(E_COMPILE_ERROR, "Invalid binding type");
			return;
	}

/*... ...*/
}

void zend_verify_abstract_class(zend_class_entry *ce TSRMLS_DC)
{
	zend_abstract_info ai;

	if ((ce->ce_flags & ZEND_ACC_IMPLICIT_ABSTRACT_CLASS) && !(ce->ce_flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS)) {
		memset(&ai, 0, sizeof(ai));

		zend_hash_apply_with_argument(&ce->function_table, (apply_func_arg_t) zend_verify_abstract_class_function, &ai TSRMLS_CC);

		if (ai.cnt) {
			zend_error(E_ERROR, "Class %s contains %d abstract method%s and must therefore be declared abstract or implement the remaining methods (" MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT MAX_ABSTRACT_INFO_FMT ")",
				ce->name, ai.cnt,
				ai.cnt > 1 ? "s" : "",
				DISPLAY_ABSTRACT_FN(0),
				DISPLAY_ABSTRACT_FN(1),
				DISPLAY_ABSTRACT_FN(2)
				);
		}
	}
}
ログイン後にコピー

インターフェースを実装したクラスのバインド処理は非常に複雑で、一般的な処理は次のとおりです。インターフェイスは実装されています これはクラスであり、インターフェイス自体ではありません (クラス、インターフェイス、トレイトの基礎となるデータ構造はすべて zend_class_entry です)

    定数をコピーし、競合の可能性を確認します
  • コピー競合に加えて、アクセス制御もチェックする必要があります。
  • インターフェースを zend_class_entry
  • # の
  • **interfaces
  • に追加します。
  • #なお、いわゆるコピーとは、定数、属性、メソッドの参照数に1を加えるだけであり、対応するメソッドの参照数をインクリメントするだけでなく、参照数もインクリメントします。メソッド内で定義された静的変数。 <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface TSRMLS_DC) { /* ... ... */ } else { if (ce-&gt;num_interfaces &gt;= current_iface_num) { /* resize the vector if needed */ if (ce-&gt;type == ZEND_INTERNAL_CLASS) { /*对于内部定义的 class,使用 realloc 分配内存,所分配的内存在进程的生命周期中永久有效*/ ce-&gt;interfaces = (zend_class_entry **) realloc(ce-&gt;interfaces, sizeof(zend_class_entry *) * (++current_iface_num)); } else { /*对于开发者定义的 class,使用 erealloc 分配内存,所分配的内存只在请求的生命周期中有效*/ ce-&gt;interfaces = (zend_class_entry **) erealloc(ce-&gt;interfaces, sizeof(zend_class_entry *) * (++current_iface_num)); } } ce-&gt;interfaces[ce-&gt;num_interfaces++] = iface; /* Add the interface to the class */ /* Copy every constants from the interface constants table to the current class constants table */ zend_hash_merge_ex(&amp;ce-&gt;constants_table, &amp;iface-&gt;constants_table, (copy_ctor_func_t) zval_add_ref, sizeof(zval *), (merge_checker_func_t) do_inherit_constant_check, iface); /* Copy every methods from the interface methods table to the current class methods table */ zend_hash_merge_ex(&amp;ce-&gt;function_table, &amp;iface-&gt;function_table, (copy_ctor_func_t) do_inherit_method, sizeof(zend_function), (merge_checker_func_t) do_inherit_method_check, ce); do_implement_interface(ce, iface TSRMLS_CC); zend_do_inherit_interfaces(ce, iface TSRMLS_CC); } }</pre><div class="contentsignin">ログイン後にコピー</div></div> インターフェースを実装したクラスのバインディングでは、複数のループ走査やチェックが必要なため、通常は多くの CPU リソースを消費しますが、メモリ空間は節約されます。
この段階では、PHP は、リクエストが行われるたびにこれらの操作が実行されると考え、インターフェイスのコード実行段階へのバインドを延期します。

  对于 class 继承的绑定,过程与 interface 的绑定类似,但更为复杂。另外有一个值得注意的地方,如果 class 在绑定时已经解析到了父类,则绑定发生在代码编译阶段;否则发生在代码执行阶段。

// A 在 B 之前申明,B 的绑定发生在编译阶段
class A { }
class B extends A { }

// A 在 B 之后申明,绑定 B 时编译器无法知道 A 情况,此时 B 的绑定只能延后到代码执行时
class B extends A { }
class A { }

// 这种情况会报错:Class B doesn&#39;t exist
// 在代码执行阶段绑定 C,需要解析 B,但此时 B 有继承了 A,而 A 此时还是未知状态
class C extends B { }
class B extends A { }
class A { }
ログイン後にコピー

如果使用 autoload,并且采用一个 class 对应一个文件的模式,则所有 class 的绑定都只会发生在代码执行阶段

二、PHP 5 中的 object

⒈ object 中的方法

  方法与函数的底层数据结构均为 zend_function。PHP 编译器在编译时将方法编译并添加到 zend_class_entry 的 function_table 属性中。所以,在 PHP 代码运行时,方法已经编译完成,PHP 要做的只是通过指针找到方法并执行。

typedef union _zend_function {
	zend_uchar type;

	struct {
		zend_uchar type;
		const char *function_name;
		zend_class_entry *scope;
		zend_uint fn_flags;
		union _zend_function *prototype;
		zend_uint num_args;
		zend_uint required_num_args;
		zend_arg_info *arg_info;
	} common;

	zend_op_array op_array;
	zend_internal_function internal_function;
} zend_function;
ログイン後にコピー

  当 object 尝试调用方法时,首先会在其对应的 class 的 function_table 中查找该方法,同时还会检查方法的访问控制。如果方法不存在或方法的访问控制不符合要求,object 会尝试调用莫属方法 __call

static inline union _zend_function *zend_get_user_call_function(zend_class_entry *ce, const char *method_name, int method_len) 
{
	zend_internal_function *call_user_call = emalloc(sizeof(zend_internal_function));
	call_user_call->type = ZEND_INTERNAL_FUNCTION;
	call_user_call->module = (ce->type == ZEND_INTERNAL_CLASS) ? ce->info.internal.module : NULL;
	call_user_call->handler = zend_std_call_user_call;
	call_user_call->arg_info = NULL;
	call_user_call->num_args = 0;
	call_user_call->scope = ce;
	call_user_call->fn_flags = ZEND_ACC_CALL_VIA_HANDLER;
	call_user_call->function_name = estrndup(method_name, method_len);

	return (union _zend_function *)call_user_call;
}

static union _zend_function *zend_std_get_method(zval **object_ptr, char *method_name, int method_len, const zend_literal *key TSRMLS_DC)
{
	zend_function *fbc;
	zval *object = *object_ptr;
	zend_object *zobj = Z_OBJ_P(object);
	ulong hash_value;
	char *lc_method_name;
	ALLOCA_FLAG(use_heap)

	if (EXPECTED(key != NULL)) {
		lc_method_name = Z_STRVAL(key->constant);
		hash_value = key->hash_value;
	} else {
		lc_method_name = do_alloca(method_len+1, use_heap);
		/* Create a zend_copy_str_tolower(dest, src, src_length); */
		zend_str_tolower_copy(lc_method_name, method_name, method_len);
		hash_value = zend_hash_func(lc_method_name, method_len+1);
	}

	if (UNEXPECTED(zend_hash_quick_find(&zobj->ce->function_table, lc_method_name, method_len+1, hash_value, (void **)&fbc) == FAILURE)) {
		if (UNEXPECTED(!key)) {
			free_alloca(lc_method_name, use_heap);
		}
		if (zobj->ce->__call) {
			return zend_get_user_call_function(zobj->ce, method_name, method_len);
		} else {
			return NULL;
		}
	}

	/* Check access level */
	if (fbc->op_array.fn_flags & ZEND_ACC_PRIVATE) {
		zend_function *updated_fbc;

		/* Ensure that if we&#39;re calling a private function, we&#39;re allowed to do so.
		* If we&#39;re not and __call() handler exists, invoke it, otherwise error out.
		*/
		updated_fbc = zend_check_private_int(fbc, Z_OBJ_HANDLER_P(object, get_class_entry)(object TSRMLS_CC), lc_method_name, method_len, hash_value TSRMLS_CC);
		if (EXPECTED(updated_fbc != NULL)) {
			fbc = updated_fbc;
		} else {
			if (zobj->ce->__call) {
				fbc = zend_get_user_call_function(zobj->ce, method_name, method_len);
			} else {
				zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context &#39;%s&#39;", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : "");
			}
		}
	} else {
		/* Ensure that we haven&#39;t overridden a private function and end up calling
		* the overriding public function...
		*/
		if (EG(scope) &&
		    is_derived_class(fbc->common.scope, EG(scope)) &&
		    fbc->op_array.fn_flags & ZEND_ACC_CHANGED) {
			zend_function *priv_fbc;

			if (zend_hash_quick_find(&EG(scope)->function_table, lc_method_name, method_len+1, hash_value, (void **) &priv_fbc)==SUCCESS
				&& priv_fbc->common.fn_flags & ZEND_ACC_PRIVATE
				&& priv_fbc->common.scope == EG(scope)) {
				fbc = priv_fbc;
			}
		}
		if ((fbc->common.fn_flags & ZEND_ACC_PROTECTED)) {
			/* Ensure that if we&#39;re calling a protected function, we&#39;re allowed to do so.
			* If we&#39;re not and __call() handler exists, invoke it, otherwise error out.
			*/
			if (UNEXPECTED(!zend_check_protected(zend_get_function_root_class(fbc), EG(scope)))) {
				if (zobj->ce->__call) {
					fbc = zend_get_user_call_function(zobj->ce, method_name, method_len);
				} else {
					zend_error_noreturn(E_ERROR, "Call to %s method %s::%s() from context &#39;%s&#39;", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : "");
				}
			}
		}
	}

	if (UNEXPECTED(!key)) {
		free_alloca(lc_method_name, use_heap);
	}
	return fbc;
}
ログイン後にコピー

  这里需要指出的是:

  • 由于 PHP 对大小写不敏感,所以所有的方法名称都会被转为小写(zend_str_tolower_copy())
  • 为了避免不必要的资源消耗,PHP 5.4 开始引入了 zend_literal 结构体,即参数 key
typedef struct _zend_literal {
	zval       constant;
	zend_ulong hash_value;
	zend_uint  cache_slot;
} zend_literal;
ログイン後にコピー

  其中,constant 记录了转为小写后的字符串,hash_value 则是预先计算好的 hash。这样就避免了 object 每次调用方法都要将方法名称转为小写并计算 hash 值。

class Foo { public function BAR() { } }
$a = new Foo;
$b = &#39;bar&#39;;

$a->bar(); /* good */
$a->$b(); /* bad */
ログイン後にコピー

  在上例中,在代码编译阶段,方法 BAR 被转换成 bar 并添加到 zend_class_entry 的 function_table 中。当发生方法调用时:

  • 第一种情形,在代码编译阶段,方法名称 bar 确定为字符串常量,编译器可以预先计算好其对应的 zend_literal 结构,即 key 参数。这样,代码在执行时相对会更快。
  • 第二种情形,由于在编译阶段编译器对 $b 一无所知,这就需要在代码执行阶段现将方法名称转为小写,然后计算 hash 值。

⒉ object 中的属性

  当对一个 class 进行实例化时,object 中的属性只是对 class 中属性的引用。这样,object 的创建操作就会相对轻量化,并且会节省一部分内存空间。

PHP 7 と PHP 5 のオブジェクトの簡単な比較

  如果要对 object 中的属性进行修改,zend 引擎会单独创建一个 zval 结构,只对当前 object 的当前属性产生影响。

PHP 7 と PHP 5 のオブジェクトの簡単な比較

  class 的实例化对应的会在底层创建一个 zend_obejct 数据结构,新创建的 object 会注册到 zend_objects_store 中。zend_objects_store 是一个全局的 object 注册表,同一个对象在该注册表中只能注册一次。

typedef struct _zend_object {
	zend_class_entry *ce;
	HashTable *properties;
	zval **properties_table;
	HashTable *guards; /* protects from __get/__set ... recursion */
} zend_object;

typedef struct _zend_objects_store {/*本质上是一个动态 object_bucket 数组*/
	zend_object_store_bucket *object_buckets;
	zend_uint top; /*下一个可用的 handle,handle 取值从 1 开始。对应的在 *object_buckets 中的 index 为 handle - 1*/
	zend_uint size; /*当前分配的 *object_buckets 的最大长度*/
	int free_list_head; /*当 *object_bucket 中的 bucket 被销毁后,该 bucket 在 *object_buckets 中的 index 会被有序加入 free_list 链表。free_list_head 即为该链表中的第一个值*/
} zend_objects_store;

typedef struct _zend_object_store_bucket {
	zend_bool destructor_called;
	zend_bool valid; /*值为 1 表示当前 bucket 被使用,此时 store_bucket 中的 store_object 被使用;值为 0 表示当前 bucket 并没有存储有效的 object,此时 store_bucket 中的 free_list 被使用*/
	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; /*第一个未被使用的 bucket 的 index 永远存储在 zend_object_store 的 free_list_head 中,所以 next 只需要记录当前 bucket 之后第一个未被使用的 bucket 的 index*/
		} free_list;
	} bucket;
} zend_object_store_bucket;

ZEND_API zend_object_value zend_objects_new(zend_object **object, zend_class_entry *class_type TSRMLS_DC)
{
	zend_object_value retval;

	*object = emalloc(sizeof(zend_object));
	(*object)->ce = class_type;
	(*object)->properties = NULL;
	(*object)->properties_table = NULL;
	(*object)->guards = NULL;
	retval.handle = zend_objects_store_put(*object, (zend_objects_store_dtor_t) zend_objects_destroy_object, (zend_objects_free_object_storage_t) zend_objects_free_object_storage, NULL TSRMLS_CC);
	retval.handlers = &std_object_handlers;
	return retval;
}
ログイン後にコピー

   将 object 注册到 zend_objects_store 中以后,将会为 object 创建属性(对相应 class 属性的引用)

ZEND_API void object_properties_init(zend_object *object, zend_class_entry *class_type) 
{
	int i;

	if (class_type->default_properties_count) {
		object->properties_table = emalloc(sizeof(zval*) * class_type->default_properties_count);
		for (i = 0; i < class_type->default_properties_count; i++) {
			object->properties_table[i] = class_type->default_properties_table[i];
			if (class_type->default_properties_table[i]) {
#if ZTS
				ALLOC_ZVAL( object->properties_table[i]);
				MAKE_COPY_ZVAL(&class_type->default_properties_table[i], object->properties_table[i]);
#else
				Z_ADDREF_P(object->properties_table[i]);
#endif
			}
		}
		object->properties = NULL;
	}
}
ログイン後にコピー

  需要指出的是,在创建属性时,如果是非线程安全模式的 PHP,仅仅是增加相应属性的引用计数;但如果是线程安全模式的 PHP,则需要对属性进行深度复制,将 class 的属性全部复制到 object 中的 properties_table 中。

这也说明,线程安全的 PHP 比非线程安全的 PHP 运行慢,并且更耗费内存

每个属性在底层都对应一个 zend_property_info 结构:

typedef struct _zend_property_info {
    zend_uint flags;
    const char *name;
    int name_length;
    ulong h;
    int offset;
    const char *doc_comment;
    int doc_comment_len;
    zend_class_entry *ce;
} zend_property_info;
ログイン後にコピー

  class 中声明的每个属性,在 zend_class_entry 中的 properties_table 中都有一个zend_property_info 与之相对应。properties_table 可以帮助我们快速确定一个 object 所访问的属性是否存在:

  • 如果属性不存在,并且我们尝试向 object 写入该属性:如果 class 定义了 __set 方法,则使用 __set 方法写入该属性;否则会向 object 添加一个动态属性。但无论以何种方式写入该属性,写入的属性都将添加到 object 的 properties_table 中。
  • 如果属性存在,则需要检查相应的访问控制;对于 protected 和 private 类型,则需要检查当前的作用域。

在创建完 object 之后,只要我们不向 object 中写入新的属性或更新 object 对应的 class 中的属性的值,则 object 所占用的内存空间不会发生变化。

属性的存储/访问方式:
zend_class_entry->properties_info 中存储的是一个个的 zend_property_info。而属性的值实际以 zval 指针数组的方式存储在 zend_class_entry->default_properties_table 中。object 中动态添加的属性只会以 property_name => property_value 的形式存储在 zend_object->properties_table 中。而在创建 object 时,zend_class_entry->properties_table 中的值会被逐个传递给 zend_object->properties_table。
zend_literal->cache_slot 中存储的 int 值为 run_time_cache 中的索引 index。run_time_cache 为数组结构,index 对应的 value 为访问该属性的 object 对应的 zend_class_entry;index + 1 对应的 value 为该属性对应的 zend_property_info 。在访问属性时,如果 zend_literal->cache_slot 中的值不为空,则可以通过 zend_literal->cache_slot 快速检索得到 zend_property_info 结构;如果为空,则在检索到 zend_property_info 的信息之后会初始化 zend_literal->cache_slot。

属性名称的存储方式
private 属性:"\0class_name\0property_name"
protected 属性:"\0*\0property_name"
public 属性:"property_name"

   执行以下代码,看看输出结果

class A {
    private $a = &#39;a&#39;;
    protected $b = &#39;b&#39;;
    public $c = &#39;c&#39;;
}

class B extends A {
    private $a = &#39;aa&#39;;
    protected $b = &#39;bb&#39;;
    public $c = &#39;cc&#39;;
}

class C extends B {
    private $a = &#39;aaa&#39;;
    protected $b = &#39;bbb&#39;;
    public $c = &#39;ccc&#39;;
}

var_dump(new C());
ログイン後にコピー

zend_object 中 guards 的作用
guards 的作用是对 object 的重载提供递归保护。

class Foo {
    public function __set($name, $value) {
        $this->$name = $value;
    }
}

$foo = new Foo;
$foo->bar = &#39;baz&#39;;
var_dump($foo->bar);
ログイン後にコピー

   以上代码中,当为 foo动态设置foo 动态设置bar 属性时会调用 __set 方法。但 $bar 属性在 Foo 中并不存在,按照常理,此时又会递归调用 __set 方法。为了避免这种递归调用,PHP 会使用 zend_guard 来判断当前是否已经处于重载方法的上下文中。

typedef struct _zend_guard {
    zend_bool in_get;
    zend_bool in_set;
    zend_bool in_unset;
    zend_bool in_isset;
    zend_bool dummy; /* sizeof(zend_guard) must not be equal to sizeof(void*) */
} zend_guard;
ログイン後にコピー

⒊ object 的引用传递

  首先需要申明:object 并不是引用传递。之所以会出现 object 是引用传递的假象,原因在于我们传递给函数的参数中所存储的只是 object 在 zend_objects_store 中的 ID(handle)。通过这个 ID,我们可以在 zend_objects_store 中查找并加载真正的 object,然后访问并修改 object 中的属性。

PHP 中,函数内外是两个不同的作用域,对于同一变量,在函数内部对其修改不会影响到函数外部。但通过 object 的 ID(handle)访问并修改 object 的属性并不受此限制。

$a = 1;

function test($a) {
    $a = 3;
    echo $a; // 输出 3
}

test($a);

echo $a; // 输出 1
ログイン後にコピー

PHP 7 と PHP 5 のオブジェクトの簡単な比較

同一个 object 在 zend_objects_store 中只存储一次。要向 zend_objects_store 中写入新的对象,只能通过 new 关键字、unserialize 函数、反射、clone 四种方式。

⒋ $this

  $this 在使用时会自动接管当前对象,PHP 禁止对 this进行赋值操作。任何对this 进行赋值操作。任何对this 的赋值操作都会引起错误

static zend_bool opline_is_fetch_this(const zend_op *opline TSRMLS_DC)
{
	if ((opline->opcode == ZEND_FETCH_W) && (opline->op1_type == IS_CONST)
	    && (Z_TYPE(CONSTANT(opline->op1.constant)) == IS_STRING)
	    && ((opline->extended_value & ZEND_FETCH_STATIC_MEMBER) != ZEND_FETCH_STATIC_MEMBER)
	    && (Z_HASH_P(&CONSTANT(opline->op1.constant)) == THIS_HASHVAL)
	    && (Z_STRLEN(CONSTANT(opline->op1.constant)) == (sizeof("this")-1))
	    && !memcmp(Z_STRVAL(CONSTANT(opline->op1.constant)), "this", sizeof("this"))) {
	    return 1;
	} else {
	    return 0;
	}
}

/* ... ... */
if (opline_is_fetch_this(last_op TSRMLS_CC)) {
	zend_error(E_COMPILE_ERROR, "Cannot re-assign $this");
}
/* ... ... */
ログイン後にコピー

   在 PHP 中进行方法调用时,对应执行的 OPCode 为 INIT_METHOD_CALL。以 $a->foo() 为例,在 INIT_METHOD_CALL 中,Zend 引擎知道是由 $a 发起的方法调用,所以 Zend 引擎会把 $a 的值存入全局空间。在实际执行方法调用时,对应执行的 OPCode 为 DO_FCALL。在 DO_FCALL 中,Zend 引擎会将之前存入全局空间的 $a 赋值给 $this 的指针,即 EG(This):

if (fbc->type == ZEND_USER_FUNCTION || fbc->common.scope) {
    should_change_scope = 1;
    EX(current_this) = EG(This);
    EX(current_scope) = EG(scope);
    EX(current_called_scope) = EG(called_scope);
    EG(This) = EX(object); /* fetch the object prepared in previous INIT_METHOD opcode and affect it to EG(This) */
    EG(scope) = (fbc->type == ZEND_USER_FUNCTION || !EX(object)) ? fbc->common.scope : NULL;
    EG(called_scope) = EX(call)->called_scope;
}
ログイン後にコピー

   在实际执行方法体中的代码时,如果出现使用 $this 进行方法调用或属性赋值的情况,如 $this->a = 8 对应的将执行 OPCode ZEND_ASSIGN_OBJ,此时将从 EG(This) 取得 $this 的值

static zend_always_inline zval **_get_obj_zval_ptr_ptr_unused(TSRMLS_D)
{
	if (EXPECTED(EG(This) != NULL)) {
		return &EG(This);
	} else {
		zend_error_noreturn(E_ERROR, "Using $this when not in object context");
		return NULL;
	}
}
ログイン後にコピー

  Zend 引擎在构建方法堆栈时,$this 会被存入符号表,就像其他的变量一样。这样,当使用 $this 进行方法调用或将 $this 作为方法的参数时,Zend 引擎将从符号表中获取 $this

if (op_array->this_var != -1 && EG(This)) {
    Z_ADDREF_P(EG(This)); /* For $this pointer */
    if (!EG(active_symbol_table)) {
        EX_CV(op_array->this_var) = (zval **) EX_CV_NUM(execute_data, op_array->last_var + op_array->this_var);
        *EX_CV(op_array->this_var) = EG(This);
    } else {
        if (zend_hash_add(EG(active_symbol_table), "this", sizeof("this"), &EG(This), sizeof(zval *), (void **) EX_CV_NUM(execute_data, op_array->this_var))==FAILURE) {
            Z_DELREF_P(EG(This));
        }
    }
}
ログイン後にコピー

   最后是关于作用域的问题,当进行方法调用时,Zend 引擎会将作用域设置为 EG(scope)。EG(scope) 是 zend_class_entry 类型,也就是说,在方法中任何关于 object 的操作的作用域都是 object 对应的 class。对属性的访问控制的检查也是同样:

ZEND_API int zend_check_protected(zend_class_entry *ce, zend_class_entry *scope) 
{
	zend_class_entry *fbc_scope = ce;

	/* Is the context that&#39;s calling the function, the same as one of
	* the function&#39;s parents?
	*/
	while (fbc_scope) {
		if (fbc_scope==scope) {
			return 1;
		}
		fbc_scope = fbc_scope->parent;
	}

	/* Is the function&#39;s scope the same as our current object context,
	* or any of the parents of our context?
	*/
	while (scope) {
		if (scope==ce) {
			return 1;
		}
		scope = scope->parent;
	}
	return 0;
}

static zend_always_inline int zend_verify_property_access(zend_property_info *property_info, zend_class_entry *ce TSRMLS_DC)
{
	switch (property_info->flags & ZEND_ACC_PPP_MASK) {
		case ZEND_ACC_PUBLIC:
			return 1;
		case ZEND_ACC_PROTECTED:
			return zend_check_protected(property_info->ce, EG(scope));
		case ZEND_ACC_PRIVATE:
			if ((ce==EG(scope) || property_info->ce == EG(scope)) && EG(scope)) {
				return 1;
			} else {
				return 0;
			}
			break;
	}
	return 0;
}
ログイン後にコピー

  正是由于上述特性,所以以下代码可以正常运行

class A
{
	private $a;

	public function foo(A $obj)
	{
		$this->a = &#39;foo&#39;;
		$obj->a  = &#39;bar&#39;; /* yes, this is possible */
	}
}

$a = new A;
$b = new A;
$a->foo($b);
ログイン後にコピー

PHP 中 object 的作用域是 object 对应的 class

⒌ 析构方法 destruct

  在 PHP 中,不要依赖 destruct 方法销毁 object。因为当 PHP 发生致命错误时,destruct 方法并不会被调用。

ZEND_API void zend_hash_reverse_apply(HashTable *ht, apply_func_t apply_func TSRMLS_DC)
{
	Bucket *p, *q;

	IS_CONSISTENT(ht);

	HASH_PROTECT_RECURSION(ht);
	p = ht->pListTail;
	while (p != NULL) {
		int result = apply_func(p->pData TSRMLS_CC);

		q = p;
		p = p->pListLast;
		if (result & ZEND_HASH_APPLY_REMOVE) {
			zend_hash_apply_deleter(ht, q);
		}
		if (result & ZEND_HASH_APPLY_STOP) {
			break;
		}
	}
	HASH_UNPROTECT_RECURSION(ht);
}

static int zval_call_destructor(zval **zv TSRMLS_DC) 
{
	if (Z_TYPE_PP(zv) == IS_OBJECT && Z_REFCOUNT_PP(zv) == 1) {
		return ZEND_HASH_APPLY_REMOVE;
	} else {
		return ZEND_HASH_APPLY_KEEP;
	}
}

void shutdown_destructors(TSRMLS_D) 
{
	zend_try {
		int symbols;
		do {
			symbols = zend_hash_num_elements(&EG(symbol_table));
			zend_hash_reverse_apply(&EG(symbol_table), (apply_func_t) zval_call_destructor TSRMLS_CC);
		} while (symbols != zend_hash_num_elements(&EG(symbol_table)));
		zend_objects_store_call_destructors(&EG(objects_store) TSRMLS_CC);
	} zend_catch {
		/* if we couldn&#39;t destruct cleanly, mark all objects as destructed anyway */
		zend_objects_store_mark_destructed(&EG(objects_store) TSRMLS_CC);
	} zend_end_try();
}
ログイン後にコピー

  在调用 destruct 方法时,首先会从后往前遍历整个符号表,调用所有引用计数为 1 的 object 的 destruct 方法;然后从前往后遍历全局 object store,调用每个 object 的 destruct 方法。在此过程中如果有任何错误发生,就会停止调用 destruct 方法,然后将所有 object 的 destruct 方法都标记为已调用过的状态。

class Foo { public function __destruct() { var_dump("destroyed Foo"); } }
class Bar { public function __destruct() { var_dump("destroyed Bar"); } }

// 示例 1
$a = new Foo;
$b = new Bar;
"destroyed Bar"
"destroyed Foo"

// 示例 2
$a = new Bar;
$b = new Foo;
"destroyed Foo"
"destroyed Bar"

// 示例 3
$a = new Bar;
$b = new Foo;
$c = $b; /* $b 引用计数加 1 */
"destroyed Bar"
"destroyed Foo"

// 示例 4
class Foo { public function __destruct() { var_dump("destroyed Foo"); die();} } /* notice the die() here */
class Bar { public function __destruct() { var_dump("destroyed Bar"); } }

$a = new Foo;
$a2 = $a;
$b = new Bar;
$b2 = $b;

"destroyed Foo"
ログイン後にコピー

   另外,不要在 destruct 方法中添加任何重要的代码

class Foo
{
	public function __destruct() { new Foo; } /* PHP 最终将崩溃 */
}
ログイン後にコピー

PHP 中对象的销毁分为两个阶段:首先调用 destruct 方法(zend_object_store_bucket->bucket->obj->zend_objects_store_dtor_t),然后再释放内存(zend_object_store_bucket->bucket->obj->zend_objects_free_object_storage_t)。

之所以分为两个阶段执行是因为 destruct 中执行的是用户级的代码,即 PHP 代码;而释放内存的代码在系统底层运行。释放内存会破坏 PHP 的运行环境,为了使 destruct 中的 PHP 代码能正常运行,所以分为两个阶段,这样,保证在释放内存阶段 object 已经不被使用。

三、PHP 7 中的 object

  与 PHP 5 相比,PHP 7 中的 object 在用户层并没有基本没有什么变化;但在底层实现上,在内存和性能方面做了一些优化。

⒈ 在内存布局和管理上的优化

   ① 首先,在 zval 中移除了之前的 zend_object_value 结构,直接嵌入了 zend_object。这样,既节省了内存空间,同时提高了通过 zval 查找 zend_object 的效率

/*PHP 7 中的 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];
};

/*PHP 5 中的 zend_object_value*/
typedef struct _zend_object_value {
    zend_object_handle handle;
    const zend_object_handlers *handlers;
} zend_object_value;
ログイン後にコピー

   在 PHP 5 中通过 zval 访问 object,先要通过 zva 中的 zend_object_value 找到 handle,然后通过handle 在 zend_object_store 中找到 zend_object_store_bucket,然后从 bucket 中解析出 object。在 PHP 7 中,zval 中直接存储了 zend_object 的地址指针。

   ② 其次,properties_table 利用了 struct hack 特性,这样使得 zend_object 和 properties_table 存储在一块连续的内存空间。同时,properties_table 中直接存储了属性的 zval 结构。

   ③ guards 不再出现在 zend_object 中。如果 class 中定义了魔术方法( __set__get__isset__unset ),则 guards 存储在 properties_table 的第一个 slot 中;否则不存储 guards。

   ④ zend_object_store 及 zend_object_store_bucket 被移除,取而代之的是一个存储各个 zend_object 指针的 C 数组,handle 为数组的索引。此外,之前 bucket 中存储的 handlers 现在移入 zend_object 中;而之前 bucket 中的 dtor、free_storege、clone 现在则移入了 zend_object_handlers。

struct _zend_object_handlers {
    /* offset of real object header (usually zero) */
    int                                     offset;
    /* general object functions */
    zend_object_free_obj_t                  free_obj;
    zend_object_dtor_obj_t                  dtor_obj;
    zend_object_clone_obj_t                 clone_obj;
    /* individual object functions */
    // ... 其他与 PHP 5 相同
};
ログイン後にコピー

⒉ 底层自定义 object 的变化(PHP 扩展中会用到自定义 object)

/*PHP 5 中的 custom_object*/
struct custom_object {
    zend_object std;
    my_custom_type *my_buffer;
    // ...
};

/*PHP 7 中的 custom_object*/
struct custom_object {
    my_custom_type *my_buffer;
    // ...
    zend_object std;
};
ログイン後にコピー

   由于 PHP 7 的 zend_object 中使用了 struct hack 特性来保证 zend_object 内存的连续,所以自定义 object 中的 zend_object 只能放在最后。而 zval 中存储的只能是 zend_object,为了能通过 zend_object 顺利解析出 custom_object ,在 zend_object 的 handlers 中记录了 offset。

PHP 7 と PHP 5 のオブジェクトの簡単な比較

推荐学习:《PHP视频教程

以上がPHP 7 と PHP 5 のオブジェクトの簡単な比較の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:juejin.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート