当前位置:首页 > PHP教程 > php高级应用 > 列表

探讨php的垃圾回收机制

发布:smiling 来源: PHP粉丝网  添加日期:2022-06-30 11:31:55 浏览: 评论:0 

在平时php-fpm的时候,可能很少人注意php的变量回收,但是到swoole常驻内存开发后,就不得不重视这个了,因为在常驻内存下,如果不了解变量回收机制,可能就会出现内存泄露的问题,本文将一步步带你了解php的垃圾回收机制,让你写出的代码不再内存泄漏

写时复制

首先,php的变量复制用的是写时复制方式,举个例子.

$a='仙士可'.time();

$b=$a;

$c=$a;

//这个时候内存占用相同,$b,$c都将指向$a的内存,无需额外占用

$b='仙士可1号';

//这个时候$b的数据已经改变了,无法再引用$a的内存,所以需要额外给$b开拓内存空间

$a='仙士可2号';

//$a的数据发生了变化,同样的,$c也无法引用$a了,需要给$a额外开拓内存空间

详细写时复制可查看:php写时复制

引用计数

既然变量会引用内存,那么删除变量的时候,就会出现一个问题了:

$a='仙士可';

$b=$a;

$c=$a;

//这个时候内存占用相同,$b,$c都将指向$a的内存,无需额外占用

$b='仙士可1号';

//这个时候$b的数据已经改变了,无法再引用$a的内存,所以需要额外给$b开拓内存空间

unset($c);

//这个时候,删除$c,由于$c的数据是引用$a的数据,那么直接删除$a?

很明显,当$c引用$a的时候,删除$c,不能把$a的数据直接给删除,那么该怎么做呢?

这个时候,php底层就使用到了引用计数这个概念

引用计数,给变量引用的次数进行计算,当计数不等于0时,说明这个变量已经被引用,不能直接被回收,否则可以直接回收,例如:

$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. $a = 1111; 
  2. $b = $a
  3. $c = 22.222; 
  4. $d = $c
  5.  
  6. xdebug_debug_zval('a'); 
  7. xdebug_debug_zval('b'); 
  8. xdebug_debug_zval('c'); 
  9. xdebug_debug_zval('d'); 
  10. 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. $str = '仙士可';    // 静态字符串 
  2.  
  3. $str = '仙士可' . time();//普通字符串 
  4.  
  5. $a = 'aa'
  6. $b = $a
  7. $c = $b
  8.  
  9. $d = 'aa'.time(); 
  10. $e = $d
  11. $f = $d
  12.  
  13. xdebug_debug_zval('a'); 
  14. xdebug_debug_zval('d'); 
  15. echo "脚本结束\n"

输出:

a: (refcount=1, is_ref=0)='aa'

d: (refcount=3, is_ref=0)='aa1578156506'

脚本结束

当变量值为以上几种时,复制变量将会直接拷贝变量值,所以将不存在多次引用的情况

引用时引用计数变化

如下代码:

  1. $a = 'aa'
  2. $b = &$a
  3. $c = $b
  4.  
  5. xdebug_debug_zval('a'); 
  6. xdebug_debug_zval('b'); 
  7. xdebug_debug_zval('c'); 
  8. 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. <?php 
  2. /** 
  3.  * Created by PhpStorm. 
  4.  * User: Tioncico 
  5.  * Date: 2020/1/6 0006 
  6.  * Time: 14:22 
  7.  */ 
  8.  
  9. echo "php文件的全局开始\n"
  10.  
  11. class A{ 
  12.     protected $a
  13.     function __construct($a
  14.     { 
  15.         $this->a = $a
  16.         echo "类A{$this->a}生命周期开始\n"
  17.     } 
  18.     function test(){ 
  19.         echo "类test方法域开始\n"
  20.         echo "类test方法域结束\n"
  21.     } 
  22. //通过类析构函数的特性,当类初始化或回收时,会调用相应的方法 
  23.     function __destruct() 
  24.     { 
  25.         echo "类A{$this->a}生命周期结束\n"
  26.         // TODO: Implement __destruct() method. 
  27.     } 
  28.  
  29. function a1(){ 
  30.     echo "a1函数域开始\n"
  31.     $a = new A(1); 
  32.     echo "a1函数域结束\n"
  33.     //函数结束,将回收所有在函数a1的变量$a 
  34. a1(); 
  35.  
  36. $a = new A(2); 
  37.  
  38. echo "php文件的全局结束\n"
  39. //全局结束后,会回收全局的变量$a 

可看出,每个方法/函数都作为一个作用域,当运行完该作用域时,将会回收这里面的所有变量.

再看看这个例子:

  1. echo "php文件的全局开始\n"
  2.  
  3. class A 
  4.     protected $a
  5.  
  6.     function __construct($a
  7.     { 
  8.         $this->a = $a
  9.         echo "类{$this->a}生命周期开始\n"
  10.     } 
  11.  
  12.     function test() 
  13.     { 
  14.         echo "类test方法域开始\n"
  15.         echo "类test方法域结束\n"
  16.     } 
  17.  
  18. //通过类析构函数的特性,当类初始化或回收时,会调用相应的方法 
  19.     function __destruct() 
  20.     { 
  21.         echo "类{$this->a}生命周期结束\n"
  22.         // TODO: Implement __destruct() method. 
  23.     } 
  24.  
  25. $arr = []; 
  26. $i = 0; 
  27. while (1) { 
  28.     $arr[] = new A('arr_' . $i); 
  29.     $obj = new A('obj_' . $i); 
  30.     $i++; 
  31.     echo "数组大小:"count($arr).'\n'
  32.     sleep(1); 
  33. //$arr 会随着循环,慢慢的变大,直到内存溢出 
  34.  
  35.  
  36. echo "php文件的全局结束\n"
  37. //全局结束后,会回收全局的变量$a 

全局变量只有在脚本结束后才会回收,而在这份代码中,脚本永远不会被结束,也就说明变量永远不会回收,$arr还在不断的增加变量,直到内存溢出.

内存泄漏

请看代码:

  1. function a(){ 
  2.     class A { 
  3.         public $ref
  4.         public $name
  5.  
  6.         public function __construct($name) { 
  7.             $this->name = $name
  8.             echo($this->name.'->__construct();'.PHP_EOL); 
  9.         } 
  10.  
  11.         public function __destruct() { 
  12.             echo($this->name.'->__destruct();'.PHP_EOL); 
  13.         } 
  14.     } 
  15.  
  16.     $a1 = new A('$a1'); 
  17.     $a2 = new A('$a2'); 
  18.     $a3 = new A('$3'); 
  19.  
  20.     $a1->ref = $a2
  21.     $a2->ref = $a1
  22.  
  23.     unset($a1); 
  24.     unset($a2); 
  25.  
  26.     echo('exit(1);'.PHP_EOL); 
  27. a(); 
  28. 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

unset为最常用的一种回收方式,例如:

  1. class A 
  2.     public $ref
  3.     public $name
  4.  
  5.     public function __construct($name
  6.     { 
  7.         $this->name = $name
  8.         echo($this->name . '->__construct();' . PHP_EOL); 
  9.     } 
  10.  
  11.     public function __destruct() 
  12.     { 
  13.         echo($this->name . '->__destruct();' . PHP_EOL); 
  14.     } 
  15.  
  16. $a = new A('$a'); 
  17. $b = new A('$b'); 
  18. unset($a); 
  19. //a将会先回收 
  20. echo('exit(1);' . PHP_EOL); 
  21. //b需要脚本结束才会回收 

输出:

  1. $a->__construct(); 
  2. $b->__construct(); 
  3. $a->__destruct(); 
  4. exit(1); 
  5. $b->__destruct(); 

unset的回收原理其实就是引用计数-1,当引用计数-1之后为0时,将会直接回收该变量,否则不做操作(这就是上面内存泄漏的原因,引用计数-1并没有等于0)

=null回收

  1. class A 
  2.     public $ref
  3.     public $name
  4.  
  5.     public function __construct($name
  6.     { 
  7.         $this->name = $name
  8.         echo($this->name . '->__construct();' . PHP_EOL); 
  9.     } 
  10.  
  11.     public function __destruct() 
  12.     { 
  13.         echo($this->name . '->__destruct();' . PHP_EOL); 
  14.     } 
  15.  
  16. $a = new A('$a'); 
  17. $b = new A('$b'); 
  18. $c = new A('$c'); 
  19. unset($a); 
  20. $c=null; 
  21. xdebug_debug_zval('a'); 
  22. xdebug_debug_zval('b'); 
  23. xdebug_debug_zval('c'); 
  24.  
  25. echo('exit(1);' . PHP_EOL); 

=null和unset($a),作用其实都为一致,null将变量值赋值为null,原先的变量值引用计数-1,而unset是将变量名从php底层变量表中清理,并将变量值引用计数-1,唯一的区别在于,=null,变量名还存在,而unset之后,该变量就没了:

  1. $a->__construct(); 
  2. $b->__construct(); 
  3. $c->__construct(); 
  4. $a->__destruct(); 
  5. $c->__destruct(); 
  6. a: no such symbol //$a已经不在符号表 
  7. b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' } 
  8. c: (refcount=0, is_ref=0)=NULL  //c还存在,只是值为null 
  9. exit(1); 
  10. $b->__destruct(); 

变量覆盖回收

通过给变量赋值其他值(例如null)进行回收:

  1. class A 
  2.     public $ref
  3.     public $name
  4.  
  5.     public function __construct($name
  6.     { 
  7.         $this->name = $name
  8.         echo($this->name . '->__construct();' . PHP_EOL); 
  9.     } 
  10.  
  11.     public function __destruct() 
  12.     { 
  13.         echo($this->name . '->__destruct();' . PHP_EOL); 
  14.     } 
  15.  
  16. $a = new A('$a'); 
  17. $b = new A('$b'); 
  18. $c = new A('$c'); 
  19. $a=null; 
  20. $c'练习时长两年半的个人练习生'
  21. xdebug_debug_zval('a'); 
  22. xdebug_debug_zval('b'); 
  23. xdebug_debug_zval('c'); 
  24.  
  25. echo('exit(1);' . PHP_EOL); 

将输出:

  1. $a->__construct(); 
  2. $b->__construct(); 
  3. $c->__construct(); 
  4. $a->__destruct(); 
  5. $c->__destruct(); 
  6. a: (refcount=0, is_ref=0)=NULL 
  7. b: (refcount=1, is_ref=0)=class A { public $ref = (refcount=0, is_ref=0)=NULL; public $name = (refcount=1, is_ref=0)='$b' } 
  8. c: (refcount=1, is_ref=0)='练习时长两年半的个人练习生' 
  9. exit(1); 
  10. $b->__destruct(); 

可以看出,c由于覆盖赋值,将原先A类实例的引用计数-1,导致了$c的回收,但是从程序的内存占用来说,覆盖变量并不是意义上的内存回收,只是将变量的内存修改为了其他值.内存不会直接清空.

gc_collect_cycles

回到之前的内存泄漏章节,当写程序不小心造成了内存泄漏,内存越来越大,可是php默认只能脚本结束后回收,那该怎么办呢?我们可以使用gc_collect_cycles 函数,进行手动回收

  1. function a(){ 
  2.     class A { 
  3.         public $ref
  4.         public $name
  5.  
  6.         public function __construct($name) { 
  7.             $this->name = $name
  8.             echo($this->name.'->__construct();'.PHP_EOL); 
  9.         } 
  10.  
  11.         public function __destruct() { 
  12.             echo($this->name.'->__destruct();'.PHP_EOL); 
  13.         } 
  14.     } 
  15.  
  16.     $a1 = new A('$a1'); 
  17.     $a2 = new A('$a2'); 
  18.  
  19.     $a1->ref = $a2
  20.     $a2->ref = $a1
  21.  
  22.     $b = new A('$b'); 
  23.     $b->ref = $a1
  24.  
  25.     echo('$a1 = $a2 = $b = NULL;'.PHP_EOL); 
  26.     $a1 = $a2 = $b = NULL; 
  27.     echo('gc_collect_cycles();'.PHP_EOL); 
  28.     echo('// removed cycles: '.gc_collect_cycles().PHP_EOL); 
  29.     //这个时候,a1,a2已经被gc_collect_cycles手动回收了 
  30.     echo('exit(1);'.PHP_EOL); 
  31.  
  32. a(); 
  33. echo('exit(2);'.PHP_EOL); 

输出:

  1. $a1->__construct(); 
  2. $a2->__construct(); 
  3. $b->__construct(); 
  4. $a1 = $a2 = $b = NULL; 
  5. $b->__destruct(); 
  6. gc_collect_cycles(); 
  7. $a1->__destruct(); 
  8. $a2->__destruct(); 
  9. // removed cycles: 4 
  10. exit(1); 
  11. exit(2); 

注意,gc_colect_cycles 函数会从php的符号表,遍历所有变量,去实现引用计数的计算并清理内存,将消耗大量的cpu资源,不建议频繁使用。

另外,除去这些方法,php内存到达一定临界值时,会自动调用内存清理(我猜的),每次调用都会消耗大量的资源,可通过gc_disable 函数,去关闭php的自动gc。

Tags: php垃圾回收机制

分享到: