看到网上很多文章在讨论这个问题,但是本人觉得他们的结论都不太准确,实际上在php循环内创建对象和在循环外创建对象的不同之处不在于内存上的消耗,而是时间上的消耗。
首先我们做一个简单的测试:
<?php
//echo 'memory:'.memory_get_usage().PHP_EOL;
//echo 'time:'.microtime(1).PHP_EOL;
class A {
public $p = 'hello world';
public function test(){
return $this->p;
}
}
//echo 'memory:'.memory_get_usage().PHP_EOL;
//echo 'time:'.microtime(1).PHP_EOL;
$t = new A();
//echo 'memory:'.memory_get_usage().PHP_EOL;
//echo 'time:'.microtime(1).PHP_EOL;
$t = new A();
//echo 'memory:'.memory_get_usage().PHP_EOL;
//echo 'time:'.microtime(1).PHP_EOL;
$t = new A();
//echo 'memory:'.memory_get_usage().PHP_EOL;
//echo 'time:'.microtime(1).PHP_EOL;
运行脚本得到如下结果:
从结果可以看出,第一次创建对象后内存就不再增加,而执行时间在逐渐增加,这是肯定的,php编译执行代码肯定是需要时间的。(忽略echo行的时间,增加重复new次数效果会更明显)
我们从php底层分析一下是如何出现上述结果的。
php是一门解释型语言,Zend引擎把php代码文件编译为opcode数组(op_array),然后顺序执行每个opcode对应的操作指令运行代码。
vld显示测试文件生成的opcode如下:
结果显示Zend生成了主函数和类两个op_array,其中主函数包含3组opcodes(NEW、DO_FCALL、QM_ASSIGN),也就是$t = new A()这行代码一共生成了3个opcode
NEW 创建一个对象
ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce)
{
/*内存增加*/
zend_object *object = emalloc(sizeof(zend_object) + zend_object_properties_size(ce));
//设置GC引用计数、GC回收类型,变量同一保存到object_buckets
_zend_object_std_init(object, ce);
//对象操作api
object->handlers = &std_object_handlers;
return object;
}
DO_FCALL 调用构造函数
QM_ASSIGN 赋值操作,即把等号右边的值复制给左边的变量。注:本人在调试时生成的是ASSIGN指令,不知道为什么vld生成的是QM_ASSIGN,不过不影响我们分析,只要知道这个指令是给$t变量赋值即可
vld显示Zend重复执行了三次相同的指令,这也就能解释时间消耗增加的原因,但是从这里还无法解释为什么内存在增加了一次后就不再增加。
其实内存消耗没有增加的原因在于php底层的内存管理中变量的自动GC机制。
对象类型的底层标识IS_OBJECT 可以用到自动GC机制的。
测试代码执行过程:
第一次new:创建了对象Ⅰ并且复制给了$t变量,对象Ⅰ引用计数+1,即object.refcount = 1,$t是对象Ⅰ的引用,此时增加的内存就是新创建的对象占用的内存;
第二次new:创建了对象Ⅱ并且复制给了$t,此时又占用了一个对象大小的内存,但是此时会触发自动GC机制,$t变成了对象Ⅱ的引用,而失去了对象Ⅰ的引用,对象Ⅰ的引用计数为0,触发垃圾回收机制释放对象Ⅰ的内存,其实对象Ⅰ和对象Ⅱ是同一个对象,只不过内存地址不一样,大小是一样的,所以测试结果显示内存占用无变化,而反复执行创建和回收动作会增加时间上的消耗。
第三次new:同第二次new。
static zend_always_inline zval* zend_assign_to_variable(zval *variable_ptr, zval *value, zend_uchar value_type, zend_bool strict)
{
zend_refcounted *ref = NULL;
...
do {
//首先判断赋值目标变量是否引用了其他变量,如果没有直接赋值,如果有则首先找到引用的变量也就是garbage,如果查到garbage的引用计数为0就会进行垃圾回收操作
if (UNEXPECTED(Z_REFCOUNTED_P(variable_ptr))) {
zend_refcounted *garbage;
...
garbage = Z_COUNTED_P(variable_ptr);
zend_copy_to_variable(variable_ptr, value, value_type, ref);
if (GC_DELREF(garbage) == 0) {
rc_dtor_func(garbage);
} else { /* we need to split */
/* optimized version of GC_ZVAL_CHECK_POSSIBLE_ROOT(variable_ptr) */
if (UNEXPECTED(GC_MAY_LEAK(garbage))) {
gc_possible_root(garbage);
}
}
return variable_ptr;
}
} while (0);
zend_copy_to_variable(variable_ptr, value, value_type, ref);
return variable_ptr;
}
所以,如果我们在编码中优化一些耗时操作时不妨把像new一样的重复操作提取到循环体外部。
由此可见,即便new操作写在循环体内不会增加内存消耗,但是我们也最好不要写在循环体内,因为重复执行同一指令真的不是最佳实践。