前言

无。


测试

就用之前的php7.4.33,测试用代码:

<?php
class B {
  function __destruct() {
    echo "Destruct";
  }
}
unserialize('a:2:{i:0;O:1:"B":0:{}i:0;i:0;}');
throw new Exception('Error');

运行会发现,由于数组键的冲突,反序列化的过程中就触发了B类的构析函数。

源码

找到反序列化源码文件var_unserializer.c,找到处理类和数组反序列化的函数process_nested_data,该函数反序列化出键后会查看已有数据中是否存在该键:

if (Z_TYPE(key) == IS_LONG) {
  idx = Z_LVAL(key);
  if (UNEXPECTED((old_data = zend_hash_index_find(ht, idx)) != NULL)) {
    //??? update hash
    var_push_dtor(var_hash, old_data);
    data = zend_hash_index_update(ht, idx, &d);
  } else {
    data = zend_hash_index_add_new(ht, idx, &d);
  }
}

如果发生了键冲突,则会调用var_push_dtor函数:

if (Z_REFCOUNTED_P(rval)) {
  zval *tmp_var = var_tmp_var(var_hashx);
  if (!tmp_var) {
    return;
  }
  ZVAL_COPY(tmp_var, rval);
}

Z_REFCOUNTED_P宏判断的是变量的type_flags标志位不为0,此时B对象的type_flags值为3:

(gdb) p rval.u1.v.type_flags 
$3 = 3 '\003

其在定义中可能的值如下:

/* zval.u1.v.type_flags */
#define IS_TYPE_REFCOUNTED            (1<<0)
#define IS_TYPE_COLLECTABLE            (1<<1)

简单来说就是判断是否需要引用计数。然后会调用var_tmp_var函数生成并返回一个临时变量,并将B对象保存了进去,这个临时变量会挂在var_hashx这个php_unserialize_data_t下面:

return &var_hash->data[used_slots];

回到PHP的unserialize函数,即var.c的PHP_FUNCTION(unserialize),最后销毁多余变量:

PHP_VAR_UNSERIALIZE_DESTROY(var_hash);

一路找到var_destroy函数,里面会调用i_zval_ptr_dtor开始销毁变量:

if (Z_REFCOUNTED_P(zval_ptr)) {
  zend_refcounted *ref = Z_COUNTED_P(zval_ptr);
  if (!GC_DELREF(ref)) {
    rc_dtor_func(ref);
  } else {
    gc_check_possible_root(ref);
  }
}

再到rc_dtor_func函数:

ZEND_ASSERT(GC_TYPE(p) <= IS_CONSTANT_AST);
zend_rc_dtor_func[GC_TYPE(p)](p);

找到类型对应的销毁函数:

static const zend_rc_dtor_func_t zend_rc_dtor_func[] = {
    /* IS_UNDEF        */ (zend_rc_dtor_func_t)zend_empty_destroy,
    /* IS_NULL         */ (zend_rc_dtor_func_t)zend_empty_destroy,
    /* IS_FALSE        */ (zend_rc_dtor_func_t)zend_empty_destroy,
    /* IS_TRUE         */ (zend_rc_dtor_func_t)zend_empty_destroy,
    /* IS_LONG         */ (zend_rc_dtor_func_t)zend_empty_destroy,
    /* IS_DOUBLE       */ (zend_rc_dtor_func_t)zend_empty_destroy,
    /* IS_STRING       */ (zend_rc_dtor_func_t)zend_string_destroy,
    /* IS_ARRAY        */ (zend_rc_dtor_func_t)zend_array_destroy,
    /* IS_OBJECT       */ (zend_rc_dtor_func_t)zend_objects_store_del,
    /* IS_RESOURCE     */ (zend_rc_dtor_func_t)zend_list_free,
    /* IS_REFERENCE    */ (zend_rc_dtor_func_t)zend_reference_destroy,
    /* IS_CONSTANT_AST */ (zend_rc_dtor_func_t)zend_ast_ref_destroy
};

这里是zend_objects_store_del,该函数会调用类对应的构析函数销毁对象:

if (object->handlers->dtor_obj != zend_objects_destroy_object
    || object->ce->destructor) {
  GC_SET_REFCOUNT(object, 1);
  object->handlers->dtor_obj(object);
  GC_DELREF(object);
}

使用场景

根据参考文章所说,可以用于立即触发构析函数而不需要等到代码执行完毕后统一销毁。


参考

浅析PHP GC垃圾回收机制及常见利用方式