29

PHP 的垃圾回收机制

 4 years ago
source link: https://mp.weixin.qq.com/s/eSU43fBhCUFkZCHd4GpouA
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

UrU36bq.jpg!web

在平时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

$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)

$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'
脚本结束

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

引用时引用计数变化

如下代码:

$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将每个运行域作为一次生命周期,每次执行完一个域,将回收域内所有相关变量:

<?php
/**
  * Created by PhpStorm.
  * User: Tioncico
  * Date: 2020/1/6 0006
  * Time: 14:22
  */
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" ;
     // TODO: Implement __destruct() method.
   }
}
function a1(){
   echo "a1函数域开始\n" ;
   $a new A(1);
   echo "a1函数域结束\n" ;
   //函数结束,将回收所有在函数a1的变量$a
}
a1();
$a new A(2);
echo "php文件的全局结束\n" ;
//全局结束后,会回收全局的变量$a

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

再看看这个例子:

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" ;
     // TODO: Implement __destruct() method.
   }
}
$arr = [];
$i = 0;
while (1) {
   $arr [] =  new A( 'arr_' $i );
   $obj new A( 'obj_' $i );
   $i ++;
   echo "数组大小:" count ( $arr ). '\n' ;
   sleep(1);
//$arr 会随着循环,慢慢的变大,直到内存溢出
}
echo "php文件的全局结束\n" ;
//全局结束后,会回收全局的变量$a

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

内存泄漏

请看代码:

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

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

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 );
//a将会先回收
echo ( 'exit(1);' . PHP_EOL);
//b需要脚本结束才会回收

输出:

$a->__construct();
$b->__construct();
$a->__destruct();
exit (1);
$b->__destruct();

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

=null回收

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之后,该变量就没了:

$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)进行回收:

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  函数,进行手动回收

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

其他

以上就是全部内容了,如果发现文章有错,希望指出,也可以加我好友互相讨论

----------伟大的分割线-----------

PHP饭米粒(phpfamily) 由一群靠谱的人建立,愿为PHPer带来一些值得细细品味的精神食粮!

饭米粒只发原创或授权发表的文章,不转载网上的文章

所发的文章,均可找到原作者进行沟通。

也希望各位多多打赏(算作稿费给文章作者),更希望大家多多投 稿

投稿请联系:

[email protected]

本文由 仙士可 授权 饭米粒 发布,转载请注明本来源信息和以下的二维码(长按可识别二维码关注)

qM7RBjM.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK