问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

有人研究过php的内存回收机制吗?

发布网友 发布时间:2022-04-06 10:14

我来回答

4个回答

懂视网 时间:2022-04-06 14:35

在平时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 "脚本结束
";

将输出:

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 "脚本结束
";

输出:

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 "脚本结束
";

输出:

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 "脚本结束
";

将输出:

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文件的全局开始
";
 
class A{
 protected $a;
 function __construct($a)
 {
 $this->a = $a;
 echo "类A{$this->a}生命周期开始
";
 }
 function test(){
 echo "类test方法域开始
";
 echo "类test方法域结束
";
 }
//通过类析构函数的特性,当类初始化或回收时,会调用相应的方法
 function __destruct()
 {
 echo "类A{$this->a}生命周期结束
";
 // TODO: Implement __destruct() method.
 }
}
 
function a1(){
 echo "a1函数域开始
";
 $a = new A(1);
 echo "a1函数域结束
";
 //函数结束,将回收所有在函数a1的变量$a
}
a1();
 
$a = new A(2);
 
echo "php文件的全局结束
";
//全局结束后,会回收全局的变量$a

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

再看看这个例子:

echo "php文件的全局开始
";
 
class A
{
 protected $a;
 
 function __construct($a)
 {
 $this->a = $a;
 echo "类{$this->a}生命周期开始
";
 }
 
 function test()
 {
 echo "类test方法域开始
";
 echo "类test方法域结束
";
 }
 
//通过类析构函数的特性,当类初始化或回收时,会调用相应的方法
 function __destruct()
 {
 echo "类{$this->a}生命周期结束
";
 // TODO: Implement __destruct() method.
 }
}
 
$arr = [];
$i = 0;
while (1) {
 $arr[] = new A('arr_' . $i);
 $obj = new A('obj_' . $i);
 $i++;
 echo "数组大小:". count($arr).'
';
 sleep(1);
//$arr 会随着循环,慢慢的变大,直到内存溢出
 
}
 
echo "php文件的全局结束
";
//全局结束后,会回收全局的变量$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教程》

热心网友 时间:2022-04-06 11:43

你是大牛,都研究源码了。
俺只知道php是会自动释放内存的,当php执行完成后,自动释放改次请求的所有内存。
php.ini里面的设置是单次请求最大允许的内存分配,如果本次请求内存超过配置,那么对不起,php会因为内存无法分配导致程序崩溃而结束请求。

热心网友 时间:2022-04-06 13:01

表示不敢研究...
php垃圾回收机制GC,每个版本都在更新...你研究他干嘛!直接研究内存原理就行了....

php.ini的内存主要是防止,php占用超过系统内存,造成死机...
配置文件里的内存只是单次解释占用的内存....

cup和内存的消耗的关系有一个临界点....就是最优值,
只想效率就是假设内存无限大,io带宽无限大,cpu缓存无限大的情况,cpu占用无限小....

热心网友 时间:2022-04-06 14:36

你牛 哎 我php还没学好
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
神九返回后轨道舱分离去哪里了,是在轨道继续运行,还是不管了。天宫一... 空间站通常由对接舱、气闸舱、轨道舱、生活舱、服务舱、专用设备... 为什么不让神舟七号的轨道舱留在天上让神州八号去追? 洗衣机生产厂家 狗鱼好吃吗? 洪霞的爷爷知识很丰富,你能用哪些成语来形容他 用什么成语可以形容包罗各种各样的想法 什么万千的成语有哪些 重庆哪些考研大学 重庆师范大学考研好不好考 微信公众平台怎么快速增加粉丝 怎样将磁盘c的东西移动到其他磁盘? 怎样将C盘的东西移到其他的地方? 怎么把电脑C盘上的东西移到其他盘上面,不知道怎么操作! 为什么自己越讨厌的事情,别人越要做给我看? 我班有一个女同学直接把她自己的衣服脱光了给我看我该怎么办? 算了算了,你自己的事你自己看吧,我这个人很难搞,而且我可能不会喜欢你, 一个女生把自己的不美地方给你看,例如腿上挺多红肿样的东西,是不是证明她不喜欢我呢? 我是个男的,有没有男的给我看自己手淫啊 你可以自己录个视频给我看吗? 这样看不懂 php有垃圾回收机制吗 你i7-8700主机8000?你还真会配,给你看一下我自己弄的,看清楚了 女盆友说:你自己安排你的 别给我纳入计划。是什么意思,请看描述? 一个男人拍自己的隐私给我看是什么意思,我走没要看 女生来回答,想让女友去户外把自己玩得脏脏的都是泥巴给我看,但我只看不玩,换作是你们的话,你们会答应 领导说“你自己看着办”,该如何回应领导? 女朋友总是发自己*,然后说是给我看的,我说那可以直接发给我啊,她是想让别的人关注她嘛,求解 我有时喜欢看女友自己用那个棒弄自己,是不是心理有问题? 看你以前有个帖子说你的电脑 一打开文件夹就死机, 后来是你自己弄好的,方便告诉我怎么弄好的吗 为什么我在身边,男朋友还是喜欢自己看视频自己弄 北京远郊公交车实现扫码支付了吗? 北京市北京通养老助残卡手机拍照原件可以扫二维码坐公交车吗? 公积金在异地买房可以吗 谁用过国产电脑系统,好用吗 如何把电脑显示桌面的文字改为宋体?是长城电脑 清华同方电脑怎么添加字体 安装office2010中出错,显示错误1907 ,无法注册字体。请确认有足够的权限安装字体 公积金异地可以买房吗 电脑文字很清晰,但是软件打开的文字却不清晰怎么回事 波龙怎么清洗 qq里发的动图文件不会动 我手机QQ表情怎么不动了?昨天还是动的。 波士顿龙虾肉质鲜美,这种龙虾蒸的过程中需要放辅料吗? 一环套一环剪纸怎么剪 波士顿龙虾怎么做 我家的QQ为什么我搞了动态图片为什么不动呢? 关于QQ的动态图…我保存了别人的动态图,然后我自己再发出去,为什么别人看到我的图就是不动的了? 剪纸图片谁有啊,给我几张 求这套十二生肖剪纸的高清图片,刻橡皮章用。谢谢啦! 请问在哪能收藏到真正的手工剪纸珍品?