>  기사  >  백엔드 개발  >  PHP 7과 PHP 5의 객체에 대한 간략한 비교

PHP 7과 PHP 5의 객체에 대한 간략한 비교

青灯夜游
青灯夜游앞으로
2021-09-03 19:05:492852검색

이 기사에서는 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 확장에 정의된 클래스입니다. 둘 사이의 가장 중요한 차이점은 생명주기입니다.

  • php-fpm을 예로 들어 보겠습니다. 요청이 오면 PHP는 개발자가 정의한 클래스를 구문 분석하고 이에 해당하는 메모리 공간을 할당합니다. 나중에 요청을 처리하는 동안 PHP는 이러한 클래스에 해당 호출을 수행합니다. 마지막으로 요청을 처리한 후 이러한 클래스는 삭제되고 할당된 메모리 공간이 해제됩니다.

메모리 공간을 절약하기 위해 실제로 사용되지 않는 일부 클래스를 코드에서 정의하지 마세요. 자동 로드는 클래스가 사용될 때만 로드하고 구문 분석하기 때문에 실제로 사용되지 않는 이러한 클래스를 보호하기 위해 자동 로드를 사용할 수 있습니다. 그러나 이렇게 하면 코드의 컴파일 단계에서 실행까지 클래스의 구문 분석 및 로드 프로세스가 지연됩니다. 성능에 영향을 미치는 코드 스테이지

또한, OPCache 확장이 켜져 있어도 개발자의 사용자 정의 클래스는 요청 도착과 함께 계속 구문 분석 및 로드되고, 완료와 함께 폐기된다는 점에 유의해야 합니다. OPCache는 이 두 가지만 향상시킵니다. 각 단계의 속도는

  • 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 프로세스 수명 동안 한 번만 발생합니다.

  부모 클래스를 상속하지도 않고 인터페이스를 구현하지도 않고 특성을 사용하지도 않는 클래스의 경우 바인딩 프로세스는 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만큼만 증가시킵니다. of method, do_inherit_method 해당 메소드의 참조 카운트를 증가시키는 것과 더불어 해당 메소드에 정의된 정적 변수의 참조 카운트도 증가시킨다.

ZEND_API void zend_do_implement_interface(zend_class_entry *ce, zend_class_entry *iface TSRMLS_DC)
{
	/* ... ... */
	
	} else {
		if (ce->num_interfaces >= current_iface_num) { /* resize the vector if needed */
			if (ce->type == ZEND_INTERNAL_CLASS) {
				/*对于内部定义的 class,使用 realloc 分配内存,所分配的内存在进程的生命周期中永久有效*/
				ce->interfaces = (zend_class_entry **) realloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num));
			} else {
				/*对于开发者定义的 class,使用 erealloc 分配内存,所分配的内存只在请求的生命周期中有效*/
				ce->interfaces = (zend_class_entry **) erealloc(ce->interfaces, sizeof(zend_class_entry *) * (++current_iface_num));
			}
		}
		ce->interfaces[ce->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(&ce->constants_table, &iface->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(&ce->function_table, &iface->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);
	}
}
  인터페이스를 구현하는 클래스 바인딩의 경우 다중 루프 순회 및 검사가 필요하기 때문에 일반적으로 많은 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'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're calling a private function, we're allowed to do so.
		* If we'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 '%s'", zend_visibility_string(fbc->common.fn_flags), ZEND_FN_SCOPE_NAME(fbc), method_name, EG(scope) ? EG(scope)->name : "");
			}
		}
	} else {
		/* Ensure that we haven'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're calling a protected function, we're allowed to do so.
			* If we'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 '%s'", 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 = 'bar';

$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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제