引用变量

尽量避免使用引用

概念

在PHP中引用意味着用不同的名字访问同一个变量内容。

定义方式

使用 & 符号

工作原理

写时复制(Copy on Write)

<?php
// 定义一个变量
$a = 1;
// 定义变量b,将a变量的值赋值给b
$b = $a;
// 对a进行修改
$a = 3

从内存的角度来分析一下这段代码“可能”是这样执行的:

  • 1.分配一块内存给a变量,里面存储一个1;

  • 2.再分配一块内存给bar变量,也存一个1,

  • 3.修改a变量的值。

其实并不是上面的步骤执行的,实际上再第2步的时候,a变量和b变量其实使用的是使用同一块内存(这样内存的使用就节省了一个对数值1的存储,并且,还省去了分配内存和管理内存地址的计算开销),只要在进行第3步进行修改的时候才会进行重新为a创建一块内存空间存储数值3,这个过程其实就是叫写时复制(Copy on Write,也缩写为COW)。

COW 的应用场景非常多, 比如Linux中对进程复制中内存使用的优化,在各种编程语言中,如C++的STL等等中均有类似的应用。 COW是常用的优化手段,可以归类于:资源延迟分配。只有在真正需要使用资源时才占用资源, 写时复制通常能减少资源的占用。

推迟内存复制的优化

正如前面所说,PHP中的COW可以简单描述为:如果通过赋值的方式赋值给变量时不会申请新内存来存放新变量所保存的值,而是简单的通过一个计数器来共用内存,只有在其中的一个引用指向变量的值发生变化时才申请新空间来保存值内容以减少对内存的占用。在很多场景下PHP都COW进行内存的优化。比如:变量的多次赋值、函数参数传递,并在函数体内修改实参等。

下面让我们看一个查看内存的例子,可以更容易看到COW在内存使用优化方面的明显作用:

<?php

$j = 1; 
var_dump(memory_get_usage());       // 1

$test = array_fill(0, 100000, 'php-internal'); 
var_dump(memory_get_usage());       // 2

$test_copy = $test; 
var_dump(memory_get_usage());      // 3

foreach($test_copy as $i){ 
    $j += count($i);  
} 
var_dump(memory_get_usage());   // 4

执行结果:

int(321688) // 1
int(5646176) // 2
int(5646232) // 3
int(5646280) //4

上面的代码比较典型的突出了COW的作用,在数组变量$test被赋值给$test_copy时,内存的使用并没有立刻增加一半,在循环遍历数$test_copy时也没有发生显著变化,在这里$test_copy和$test变量的数据共同指向同一块内存,而没有产生复制。

也就是说,即使我们不使用引用,一个变量被赋值后,只要我们不改变变量的值 ,也不会新申请内存用来存放数据。据此我们很容易就可以想到一些COW可以非常有效的控制内存使用的场景:只是使用变量进行计算而很少对其进行修改操作,如函数参数的传递,大数组的复制等等等不需要改变变量值的情形。

复制分离变化的值

多个相同值的变量共用同一块内存的确节省了内存空间,但变量的值是会发生变化的,如果在上面的例子中,指向同一内存的值发生了变化(或者可能发生变化),就需要将变化的值“分离”出去,这个“分离”的操作,就是“复制”。

示例:

<?php

// 定义一个变量
$a = range(0, 1000);
var_dump(memory_get_usage()); // 1

// 定义变量b,将a变量的值赋值给b
$b = $a;
var_dump(memory_get_usage()); //2

// 对a进行修改
$a = range(1000, 1000000);  
var_dump(memory_get_usage()); // 3

结果:

int(404824) //1
int(404872)  // 2
int(84519408) //3

第3步,内存明显发生变化,也就是上面说的值“分离”的操作,就是“复制”,也就是写时复制(Copy on Write,也缩写为COW)。

在PHP中,Zend引擎为了区别同一个zval地址是否被多个变量共享,引入了ref_count和is_ref两个变量进行标识,以下是PHP变量在C语言底层中的代码,:

typedef struct _zval_struct zval;
typedef unsigned int zend_uint;
typedef unsigned char zend_uchar;

struct _zval_struct {
    zvalue_value value;      /*注意这里,这个里面存的才是变量的值*/
    zend_uint refcount;  /*引用计数*/
    zend_uchar type;        /* 变量当前的数据类型 */
    zend_uchar is_ref;   /*变量是否引用*/
};

typedef union _zvalue_value {
    long lval;      /*PHP中整型的值*/
    double dval;    /*PHP的浮点数值*/
    struct {     
        char *val;
        int len;
    } str;               /*PHP的字符串*/
    HashTable *ht;     /*数组*/
    zend_object_value obj;  /*对象*/
} zvalue_value;
  • is_ref标识是不是用户使用 &的强制引用;布尔类型

  • ref_count是引用计数,用于标识此zval被多少个变量引用,即COW的自动引用,为0时会被销毁;

注:由此可见, $a=$b; 与 $a=&$b; 在PHP对内存的使用上没有区别(值不变化时);

安装xdebug,可以利用利用xdebug_debug_zval(),可以看到zval结构

<?php

// zval变量容器
$a = range(0, 3);
xdebug_debug_zval('a');

// 定义变量b,把a的值赋值给b
$b = $a;
xdebug_debug_zval('a');

// 修改a
$a = range(0, 3);
xdebug_debug_zval('a');

结果:

a: // 1
(refcount=1, is_ref=0),
array (size=4)
  0 => (refcount=1, is_ref=0),int 0
  1 => (refcount=1, is_ref=0),int 1
  2 => (refcount=1, is_ref=0),int 2
  3 => (refcount=1, is_ref=0),int 3
a: // 2
(refcount=2, is_ref=0),
array (size=4)
  0 => (refcount=1, is_ref=0),int 0
  1 => (refcount=1, is_ref=0),int 1
  2 => (refcount=1, is_ref=0),int 2
  3 => (refcount=1, is_ref=0),int 3
a: // 3
(refcount=1, is_ref=0),
array (size=4)
  0 => (refcount=1, is_ref=0),int 0
  1 => (refcount=1, is_ref=0),int 1
  2 => (refcount=1, is_ref=0),int 2
  3 => (refcount=1, is_ref=0),int 3

从结果可以看到,

  • 1.refcount = 1:有一个内存空间,并且有一个变量指向这个内存空间;is_ref = 0: 不是引用。

  • 2.refcount = 2:有一个内存空间,并且有两个变量指向这个内存空间;is_ref = 0: 不是引用。

  • 3.refcount = 1:有一个内存空间,并且有一个变量指向这个内存空间;is_ref = 0: 不是引用。

采用引用方式:

<?php

$a = range(0, 3);
xdebug_debug_zval('a');

$b = &$a;
xdebug_debug_zval('a');

$a = range(0, 3);
xdebug_debug_zval('a');

结果:

a:
(refcount=1, is_ref=0),
array (size=4)
  0 => (refcount=1, is_ref=0),int 0
  1 => (refcount=1, is_ref=0),int 1
  2 => (refcount=1, is_ref=0),int 2
  3 => (refcount=1, is_ref=0),int 3
a:
(refcount=2, is_ref=1),
array (size=4)
  0 => (refcount=1, is_ref=0),int 0
  1 => (refcount=1, is_ref=0),int 1
  2 => (refcount=1, is_ref=0),int 2
  3 => (refcount=1, is_ref=0),int 3
a:
(refcount=2, is_ref=1),
array (size=4)
  0 => (refcount=1, is_ref=0),int 0
  1 => (refcount=1, is_ref=0),int 1
  2 => (refcount=1, is_ref=0),int 2
  3 => (refcount=1, is_ref=0),int 3
  • 1.refcount = 1:有一个内存空间,并且有一个变量指向这个内存空间;is_ref = 0: 不是引用。

  • 2.refcount = 2:有一个内存空间,并且有两个变量指向这个内存空间;is_ref = 1: 是引用。

  • 3.refcount = 2:有一个内存空间,并且有两个变量指向这个内存空间;is_ref = 1: 是引用。

$a指向一个内存空间,$a将内存地址给了$b,不在有COW机制,因为两个变量指向的同一片内存地址空间,修改$a的值,也相当于修改$b的值

需要注意的点

unset 只会取消引用,不会销毁空间

使用引用时, 变量同时指向同一片内存空间,当 unset 一个引用,只是断开了变量名和变量内容之间的绑定。这并不意味着变量内容被销毁了。

<?php

// unset 只会取消引用,不会销毁空间
$a = 1;

$b = &$a;

unset($b);

echo $a. "\n";

结果:

1

对象本身就是引用传递

<?php
class Person
{
    public $name = "abc";
}

$p1 = new Person;
xdebug_debug_zval('p1');

$p2 = $p1;
xdebug_debug_zval('p1');

$p2->name = "lisi";
xdebug_debug_zval('p1');

var_dump($p1);
var_dump($p2);

结果:

p1:
(refcount=1, is_ref=0),
object(Person)[1]
  public 'name' => (refcount=2, is_ref=0),string 'abc' (length=3)
p1:
(refcount=2, is_ref=0),
object(Person)[1]
  public 'name' => (refcount=2, is_ref=0),string 'abc' (length=3)
p1:
(refcount=2, is_ref=0),
object(Person)[1]
  public 'name' => (refcount=1, is_ref=0),string 'haha' (length=4)
object(Person)[1]
  public 'name' => string 'haha' (length=4)
object(Person)[1]
  public 'name' => string 'haha' (length=4)

你会发现虽然is_ref始终是0.但是通过refcount可看出两个变量同时指向的相同的内存空间,单独对一个对象进行属性修改,但是两个对象的属性值都发生了改变,没有进行COW.这也是对象的特殊性,这也是为什么对象会有克隆clone:

<?php
class Person
{
    public $name = "abc";
}

$p1 = new Person;
xdebug_debug_zval('p1');


$p2= clone $p1;  //这个才是正在重新开辟新的内存空间
xdebug_debug_zval('p1');

$p2->name = "haha";
xdebug_debug_zval('p1');

var_dump($p1);
var_dump($p2);

结果:

p1:
(refcount=1, is_ref=0),
object(Person)[1]
  public 'name' => (refcount=2, is_ref=0),string 'abc' (length=3)
p1:
(refcount=1, is_ref=0),
object(Person)[1]
  public 'name' => (refcount=3, is_ref=0),string 'abc' (length=3)
p1:
(refcount=1, is_ref=0),
object(Person)[1]
  public 'name' => (refcount=2, is_ref=0),string 'abc' (length=3)
object(Person)[1]
  public 'name' => string 'abc' (length=3)
object(Person)[2]
  public 'name' => string 'haha' (length=4)

避免使用引用

<?php 
$foo['love'] = 1; 
$bar  = &$foo['love']; 
$tipi = $foo; 
$tipi['love'] = '2'; 
echo $foo['love'];

结果:

2

大家肯定会有困惑,很明显造成的元音是$bar = &$foo['love'];这一行,也就是说数组中的第一个元素已经变成了引用类型。所以赋值时也是引用拷贝,而非值拷贝。

从这篇文章中也同样解析了这种现象,谨慎使用PHP的引用

思考:

写出如下程序的输出结果

 <?php

 $data = array('a', 'b', 'c');

 foreach($data as $key => $val)
 {
      $val = &$data[$key];
 }

程序运行时,每一次循环结束后变量$data的值是什么?请解释

程序执行完成后,变量$data的值是什么?请解释

array (size=3)
  0 => &string 'a' (length=1)
  1 => string 'b' (length=1)
  2 => string 'c' (length=1)
array (size=3)
  0 => string 'b' (length=1)
  1 => &string 'b' (length=1)
  2 => string 'c' (length=1)
array (size=3)
  0 => string 'b' (length=1)
  1 => string 'c' (length=1)
  2 => &string 'c' (length=1)
array (size=3)
  0 => string 'b' (length=1)
  1 => string 'c' (length=1)
  2 => &string 'c' (length=1)

资料

PHP官方对引用的解释

PHP 之 写时复制介绍(Copy On Write)

C语言角度解读php变量之写时复制机制(copy on write)

PHP中的写时复制(Copy On Write)

PHP源码分析-变量的引用计数、写时复制(Reference counting & Copy-on-Write)

Last updated