写时复制 首先,php的变量复制用的是写时复制方式,举个例子.
1 2 3 4 5 6 7 8 9 10 $a ='蛋蛋' .time();$b =$a ;$c =$a ;//这个时候内存占用相同,$b ,$c 都将指向$a 的内存,无需额外占用 $b ='蛋蛋1号' ;//这个时候$b 的数据已经改变了,无法再引用$a 的内存,所以需要额外给$b 开拓内存空间 $a ='蛋蛋2号' ;//$a 的数据发生了变化,同样的,$c 也无法引用$a 了,需要给$a 额外开拓内存空间
引用计数 既然变量会引用内存,那么删除变量的时候,就会出现一个问题了:
1 2 3 4 5 6 7 8 9 10 $a='蛋蛋' ; $b=$a; $c=$a; $b='蛋蛋1号' ; unset ($c);
很明显,当$c引用$a的时候,删除$c,不能把$a的数据直接给删除,那么该怎么做呢? 这个时候,php底层就使用到了引用计数这个概念 引用计数,给变量引用的次数进行计算,当计数不等于0时,说明这个变量已经被引用,不能直接被回收,否则可以直接回收,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $a = '蛋蛋' .time();$b = $a ;$c = $a ; xdebug_debug_zval('a' ); xdebug_debug_zval('b' ); xdebug_debug_zval('c' ); $b ='蛋蛋2号' ;xdebug_debug_zval('a' ); xdebug_debug_zval('b' ); echo "脚本结束\n" ;//输出 a: (refcount=3, is_ref=0)='蛋蛋1578154814' b: (refcount=3, is_ref=0)='蛋蛋1578154814' c: (refcount=3, is_ref=0)='蛋蛋1578154814' a: (refcount=2, is_ref=0)='蛋蛋1578154814' b: (refcount=1, is_ref=0)='蛋蛋2号' 脚本结束
注意,xdebug_debug_zval函数是xdebug扩展的,使用前必须安装xdebug扩展
引用计数特殊情况 当变量值为整型,浮点型时,在赋值变量时,php7底层将会直接把值存储(php7的结构体将会直接存储简单数据类型),refcount将为0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $a = 1111 ; $b = $a; $c = 22.222 ; $d = $c; xdebug_debug_zval('a' ); xdebug_debug_zval('b' ); xdebug_debug_zval('c' ); xdebug_debug_zval('d' ); echo "脚本结束\n" ;a: (refcount=0 , is_ref=0 )=1111 b: (refcount=0 , is_ref=0 )=1111 c: (refcount=0 , is_ref=0 )=22.222 d: (refcount=0 , is_ref=0 )=22.222 脚本结束
当变量值为interned string字符串型(变量名,函数名,静态字符串,类名等)时,变量值存储在静态区,内存回收被系统全局接管,引用计数将一直为1(php7.3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $str = '蛋蛋' ; $str = '蛋蛋' . time(); $a = 'aa' ; $b = $a; $c = $b; $d = 'aa' .time(); $e = $d; $f = $d; xdebug_debug_zval('a' ); xdebug_debug_zval('d' ); echo "脚本结束\n" ;a: (refcount=1 , is_ref=0 )='aa' d: (refcount=3 , is_ref=0 )='aa1578156506' 脚本结束
当变量值为以上几种时,复制变量将会直接拷贝变量值,所以将不存在多次引用的情况
###引用时引用计数变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $a = 'aa' ;$b = &$a ;$c = $b ; xdebug_debug_zval('a' ); xdebug_debug_zval('b' ); xdebug_debug_zval('c' ); echo "脚本结束\n" ;//输出 a: (refcount=2, is_ref=1)='aa' b: (refcount=2, is_ref=1)='aa' c: (refcount=1, is_ref=0)='aa' 脚本结束
当引用时,被引用变量的value以及类型将会更改为引用类型,并将引用值指向原来的值内存地址中.
之后引用变量的类型也会更改为引用类型,并将值指向原来的值内存地址,这个时候,值内存地址被引用了2次,所以refcount=2.
而$c并非是引用变量,所以将值复制给了$c,$c引用还是为1
详细引用计数知识,底层原理可查看:https://www.cnblogs.com/sohuhome/p/9800977.html
php生命周期 php将每个运行域作为一次生命周期,每次执行完一个域,将回收域内所有相关变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <?php echo "php文件的全局开始\n" ; class A { protected $a; function __construct ($a) { $this ->a = $a; echo "类A{$this->a}生命周期开始\n" ; } function test () { echo "类test方法域开始\n" ; echo "类test方法域结束\n" ; } function __destruct () { echo "类A{$this->a}生命周期结束\n" ; } } function a1 () { echo "a1函数域开始\n" ; $a = new A(1 ); echo "a1函数域结束\n" ; } a1(); $a = new A(2 ); echo "php文件的全局结束\n" ;
可看出,每个方法/函数都作为一个作用域,当运行完该作用域时,将会回收这里面的所有变量.
再看看这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 echo "php文件的全局开始\n" ; class A { protected $a; function __construct ($a) { $this ->a = $a; echo "类{$this->a}生命周期开始\n" ; } function test () { echo "类test方法域开始\n" ; echo "类test方法域结束\n" ; } function __destruct () { echo "类{$this->a}生命周期结束\n" ; } } $arr = []; $i = 0 ; while (1 ) { $arr[] = new A('arr_' . $i); $obj = new A('obj_' . $i); $i++; echo "数组大小:" . count($arr).'\n' ; sleep(1 ); } echo "php文件的全局结束\n" ;
全局变量只有在脚本结束后才会回收,而在这份代码中,脚本永远不会被结束,也就说明变量永远不会回收,$arr还在不断的增加变量,直到内存溢出.
内存泄漏 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function a () { class A { public $ref; public $name; public function __construct ($name) { $this ->name = $name; echo ($this ->name.'->__construct();' .PHP_EOL); } public function __destruct () { echo ($this ->name.'->__destruct();' .PHP_EOL); } } $a1 = new A('$a1' ); $a2 = new A('$a2' ); $a3 = new A('$3' ); $a1->ref = $a2; $a2->ref = $a1; unset ($a1); unset ($a2); echo ('exit(1);' .PHP_EOL); } a(); echo ('exit(2);' .PHP_EOL);
当$a1和$a2的属性互相引用时,unset($a1,$a2) 只能删除变量的引用,却没有真正的删除类的变量,这是为什么呢?
首先,类的实例化变量分为2个步骤,1:开辟类存储空间,用于存储类数据,2:实例化一个变量,类型为class,值指向类存储空间.
当给变量赋值成功后,类的引用计数为1,同时,a1->ref指向了a2,导致a2类引用计数增加1,同时a1类被a2->ref引用,a1引用计数增加1
当unset时,只会删除类的变量引用,也就是-1,但是该类其实还存在了一次引用(类的互相引用),
这将造成这2个类内存永远无法释放,直到被gc机制循环查找回收,或脚本终止回收(域结束无法回收).
手动回收机制 在上面,我们知道了脚本回收,域结束回收2种php回收方式,那么可以手动回收吗?答案是可以的.
手动回收有以下几种方式:
unset,赋值为null,变量赋值覆盖,gc_collect_cycles函数回收
unset 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class A { public $ref; public $name; public function __construct ($name) { $this ->name = $name; echo ($this ->name . '->__construct();' . PHP_EOL); } public function __destruct () { echo ($this ->name . '->__destruct();' . PHP_EOL); } } $a = new A('$a' ); $b = new A('$b' ); unset ($a);echo ('exit(1);' . PHP_EOL);$a->__construct(); $b->__construct(); $a->__destruct(); exit (1 );$b->__destruct();
unset的回收原理其实就是引用计数-1,当引用计数-1之后为0时,将会直接回收该变量,否则不做操作(这就是上面内存泄漏的原因,引用计数-1并没有等于0)
=null回收 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class A { public $ref; public $name; public function __construct ($name) { $this ->name = $name; echo ($this ->name . '->__construct();' . PHP_EOL); } public function __destruct () { echo ($this ->name . '->__destruct();' . PHP_EOL); } } $a = new A('$a' ); $b = new A('$b' ); $c = new A('$c' ); unset ($a);$c=null ; xdebug_debug_zval('a' ); xdebug_debug_zval('b' ); xdebug_debug_zval('c' ); echo ('exit(1);' . PHP_EOL);
=null和unset($a),作用其实都为一致,null将变量值赋值为null,原先的变量值引用计数-1,而unset是将变量名从php底层变量表中清理,并将变量值引用计数-1,唯一的区别在于,=null,变量名还存在,而unset之后,该变量就没了:
1 2 3 4 5 6 7 8 9 10 $a->__construct(); $b->__construct(); $c->__construct(); $a->__destruct(); $c->__destruct(); a: no such symbol //$a已经不在符号表 b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' } c: (refcount=0, is_ref=0)=NULL //c还存在,只是值为null exit(1); $b->__destruct();
变量覆盖回收 通过给变量赋值其他值(例如null)进行回收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 class A { public $ref; public $name; public function __construct($name) { $this->name = $name; echo($this->name . '->__construct();' . PHP_EOL); } public function __destruct() { echo($this->name . '->__destruct();' . PHP_EOL); } } $a = new A('$a'); $b = new A('$b'); $c = new A('$c'); $a=null; $c= '练习时长两年半的个人练习生'; xdebug_debug_zval('a'); xdebug_debug_zval('b'); xdebug_debug_zval('c'); echo('exit(1);' . PHP_EOL); //输出 $a->__construct(); $b->__construct(); $c->__construct(); $a->__destruct(); $c->__destruct(); a: (refcount=0, is_ref=0)=NULL b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' } c: (refcount=1, is_ref=0)='练习时长两年半的个人练习生' exit(1); $b->__destruct();
可以看出,c由于覆盖赋值,将原先A类实例的引用计数-1,导致了$c的回收,但是从程序的内存占用来说,覆盖变量并不是意义上的内存回收,只是将变量的内存修改为了其他值.内存不会直接清空.
gc_collect_cycles 回到之前的内存泄漏章节,当写程序不小心造成了内存泄漏,内存越来越大,可是php默认只能脚本结束后回收,那该怎么办呢?我们可以使用gc_collect_cycles 函数,进行手动回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function a(){ class A { public $ref; public $name; public function __construct($name) { $this->name = $name; echo($this->name.'->__construct();'.PHP_EOL); } public function __destruct() { echo($this->name.'->__destruct();'.PHP_EOL); } } $a1 = new A('$a1'); $a2 = new A('$a2'); $a1->ref = $a2; $a2->ref = $a1; $b = new A('$b'); $b->ref = $a1; echo('$a1 = $a2 = $b = NULL;'.PHP_EOL); $a1 = $a2 = $b = NULL; echo('gc_collect_cycles();'.PHP_EOL); echo('// removed cycles: '.gc_collect_cycles().PHP_EOL); //这个时候,a1,a2已经被gc_collect_cycles手动回收了 echo('exit(1);'.PHP_EOL); } a(); echo('exit(2);'.PHP_EOL); //输出 $a1->__construct(); $a2->__construct(); $b->__construct(); $a1 = $a2 = $b = NULL; $b->__destruct(); gc_collect_cycles(); $a1->__destruct(); $a2->__destruct(); // removed cycles: 4 exit(1); exit(2);
注意,gc_colect_cycles 函数会从php的符号表,遍历所有变量,去实现引用计数的计算并清理内存,将消耗大量的cpu资源,不建议频繁使用
另外,除去这些方法,php内存到达一定临界值时,会自动调用内存清理(我猜的),每次调用都会消耗大量的资源,可通过gc_disable 函数,去关闭php的自动gc