对于 PHP 这种需要同时处理多个请求的程序来说,申请和释放内存的时候应该慎之又慎,一不小心便会酿成大错。另一方面,除了要安全的申请和释放内存外,还应该做到内存的最小化使用,因为它可能要处理每秒钟数以千计的请求,为了提高系统整体的性能,每一次操作都应该只使用最少的内存,对于不必要的相同数据的复制则应该能免则免。我们来看下面这段 PHP 代码:
- <?php
- $a = 'Hello NowaMagic!';
- $b = $a;
- unset($a);
- ?>
第一条语句执行后,PHP 创建了 $a 这个变量,并为它申请了 12B 的内存来存放 "hello world" 这个字符串(最后加个 NULL 字符,你懂的)。紧接着把 $a 赋给了 $b,并释放掉 $a;
对于 PHP 来说,如果每一次变量赋值都执行一次内存复制的话,那需要额外申请 12B 的内存来存放这个重复的数据, 当然为了复制内存,还需要 cpu 执行某些计算,这当然会加重 cpu 的负载。当第三句执行后,$a 被释放了,我们刚才的设想突然变的这么滑稽,这次赋值显得好多余哦。如果早就知道 $a 不用了,那我们直接让 $b 用 $a 的内存不就行了,还赋值干嘛?如果你觉得 12B 没什么,那设想下如果 $a 是个 10M 的文件内容,或者 20M,是不是我们的计算机资源消耗的有点冤枉呢?
别担心,PHP 很聪明!
前面说过,PHP 变量的名称和值在内核中是保存在两个不同的地方的,值是通过一个与名字毫无关系的 zval 结构来保存,而这个变量的名字 a 则保存在符号表里,两者之间通过指针联系着。在我们上面的例子里,$a 是一个字符串,我们通过 zend_hash_add 把它添加到符号表里,然后又把它赋值给 $b, 两者拥有相同的内容!如果两者指向完全相同的内容,我们有什么优化措施吗?
- zval *helloval;
- MAKE_STD_ZVAL(helloval);
- ZVAL_STRING(helloval, "Hello NowaMagic!", 1);
- zend_hash_add(EG(active_symbol_table), "a", sizeof("a"),&helloval, sizeof(zval*), NULL);
- zend_hash_add(EG(active_symbol_table), "b", sizeof("b"),&helloval, sizeof(zval*), NULL);
- //通过这个例子我们看出了,我们可以把$a和$b都指向helloval~!
现在我们检查 $a 和 $b 两个变量,他们的值指向了 "Hello NowaMagic!" 这个字符串在内存中的位置。但是在第三行:unset($a); 这条语句释放了 $a。在这种情况下,unset 函数并不知道 $a 的值同时被 $b 用着,所以如果它直接释放内存,则会导致 $b 的值也被清空了,从而导致逻辑错误,甚至可能会导致系统崩溃。
呵呵,其实你心里明白,PHP 不会让上述问题发生的!回顾一下 zval 的四个成员 value、type、is_ref__gc、refcount__gc,我们对 value 和 type 已经很熟了,现在则是后两个成员发挥威力的时候了,这里我们主要讲解 refcount__gc 这个成员。当一个变量被第一次创建的时候,它对应的 zval 结构体的 refcount__gc 成员的值会被初始化为 1,理由很简单,因为只有这个变量自己在用它。但是当你把这个变量赋值给别的变量时,refcount__gc 属性便会加 1 变成 2,因为现在有两个变量在用这个 zval 结构了!
以上描述转为内核中的代码大体如下:
- zval * helloval;
- MAKE_STD_ZVAL(helloval);
- ZVAL_STRING(helloval, "Hello World", 1);
- zend_hash_add(EG(active_symbol_table), "a", sizeof("a"), &helloval, sizeof(zval * ), NULL);
- ZVAL_ADDREF(helloval); //这句很特殊,我们显示的增加了helloval结构体的refcount
- zend_hash_add(EG(active_symbol_table), "b", sizeof("b"), &helloval, sizeof(zval * ), NULL);
这个时候当我们再用 unset 删除 $a 的时候,它删除符号表里的 $a 的信息,然后清理它的值部分,这时它发现 $a 的值对应的 zval 结构的 refcount 值是 2,也就是有另外一个变量在一起用着这个 zval,所以 unset 只需把这个 zval 的 refcount 减去 1 就行了!
引用计数绝对是节省内存的一个超棒的模式!但是当我们修改 $b 的值,而且还需要继续使用 $a 时,该怎么办呢?
- $a = 1;
- $b = $a;
- $b += 5;
从代码逻辑来看,我们希望语句执行后 $a 仍然是 1,而 $b 则需要变成 6。我们知道在第二句完成后内核通过让 $a 和 $b 共享一个 zval 结构来达到节省内存的目的,但是现在第三句来了,这时 $b 的改变应该怎样在内核中实现呢?
答案非常简单,内核首先查看 refcount__gc 属性,如果它大于 1 则为这个变化的变量从原 zval 结构中复制出一份新的专属与 $b 的 zval 来,并改变其值。
- zval * get_var_and_separate(char * varname, int varname_len TSRMLS_DC) {
- zval * *varval,
- *varcopy;
- if (zend_hash_find(EG(active_symbol_table), varname, varname_len + 1, (void * *) & varval) == FAILURE) {
- /* 如果在符号表里找不到这个变量则直接return */
- return NULL;
- }
- if (( * varval) - >refcount < 2) {
- //如果这个变量的zval部分的refcount小于2,代表没有别的变量在用,return
- return * varval;
- }
- /* 否则,复制一份zval*的值 */
- MAKE_STD_ZVAL(varcopy);
- varcopy = *varval;
- /* Duplicate any allocated structures within the zval* */
- zval_copy_ctor(varcopy);
- /* 从符号表中删除原来的变量
- * This will decrease the refcount of varval in the process
- */
- zend_hash_del(EG(active_symbol_table), varname, varname_len + 1);
- /* 初始化新的zval的refcount,并在符号表中重新添加此变量信息,并将其值与我们的新zval相关联。*/
- varcopy - >refcount = 1;
- varcopy - >is_ref = 0;
- zend_hash_add(EG(active_symbol_table), varname, varname_len + 1, &varcopy, sizeof(zval * ), NULL);
- /* 返回新zval的地址 */
- return varcopy;
- }
现在 $b 变量拥有了自己的 zval,并且可以自由的修改它的值了。
如果用户在 PHP 脚本中显式的让一个变量引用另一个变量时,我们的内核是如何处理的呢?
- $a = 1;
- $b = &$a;
- $b += 5;
作为一个标准的 PHP 程序猿,我们都知道 $a 的值也变成 6 了。当我们更改 $b 的值时,内核发现 $b 是 $a 的一个用户端引用,也就是所它可以直接改变 $b 对应的 zval 的值,而无需再为它生成一个新的不同与 $a 的 zval。因为他知道 $a 和 $b 都想得到这次变化!
但是内核是怎么知道这一切的呢?简单的讲,它是通过 zval 的 is_ref__gc 成员来获取这些信息的。这个成员只有两个值,就像开关的开与关一样。它的这两个状态代表着它是否是一个用户在 PHP 语言中定义的引用。在第一条语句 ($a = 1;) 执行完毕后,$a 对应的 zval 的 refcount__gc 等于 1,is_ref__gc 等于 0;。 当第二条语句执行后($b = &$a;),refcount__gc 属性向往常一样增长为 2,而且 is_ref__gc 属性也同时变为了 1!
最后,在执行第三条语句的时候,内核再次检查 $b 的 zval 以确定是否需要复制出一份新的 zval 结构来,这次不需要复制,因为我们刚才上面的 get_var_and_separate 函数其实是个简化版,并且少写了一个条件:
- /* 如果这个zval在php语言中是通过引用的形式存在的,或者它的refcount小于2,则不许要复制。*/
- if (( * varval) - >is_ref || ( * varval) - >refcount < 2) {
- return * varval;
- }
这一次,尽管它的 refcount 等于 2,但是因为它的 is_ref 等于 1,所以也不会被复制。内核会直接的修改这个 zval 的值。
我们已经了解了 php 语言中变量的复制和引用的一些事,但是如果复制和引用这两个事件被组合起来使用了该怎么办呢?看下面这段代码:
- $a = 1;
- $b = $a;
- $c = &$a;
这里我们可以看到,$a,$b,$c 这三个变量现在共用一个 zval 结构,有两个属于 change-on-write 组合 ($a,$c), 有两个属于 copy-on-write 组合 ($a,$b), 我们的 is_ref__gc 和 refcount__gc 该怎样工作,才能正确的处理好这段复杂的关系呢?
The answer is: 不可能!在这种情况下,变量的值必须分离成两份完全独立的存在!$a 与 $c 共用一个 zval,$b 自己用一个 zval,尽管他们拥有同样的值,但是必须至少通过两个 zval 来实现。见下图【在引用时强制复制!】
同样,下面的这段代码同样会在内核中产生歧义,所以需要强制复制!
- //上图对应的代码
- $a = 1;
- $b = &$a;
- $c = $a;
需要注意的是,在这两种情况下,$b 都与原初的 zval 相关联,因为当复制发生时,内核还不知道第三个变量的名字。
来源: http://it.taocms.org/08/4539.htm