前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >简单谈谈PHP的GC-垃圾回收机制

简单谈谈PHP的GC-垃圾回收机制

原创
作者头像
PHP开发工程师
修改2021-04-27 17:55:33
5140
修改2021-04-27 17:55:33
举报
文章被收录于专栏:thinkphp+vue

PHP目前的开发框架,除了主流常用的FPM框架,想必就是基于swoole拓展的常驻内存开发了。

我们在FPM的开发模式中,每一次脚本程序结束之后,所有变量都会被销毁,内存会被释放,所以我们不用太担心。

但是常驻内存开发模式就不一样了,如果不注意变量内存的使用,无法很好的管理内存问题,会造成内存泄露。

所以,我们一定要熟悉PHP的垃圾回收机制(Garbage Collector 简称GC) 

写时复制与引用计数

写时复制

在PHP7+版本中,有关于变量内存的操作特性,采用了写时复制,也就是说, 在必要的时候才会进行深拷贝(即发生写的时候才会进行深拷贝).

当变量值为interned string字符串型(变量名,函数名,静态字符串,类名等)时,变量值存储在静态区,内存回收被系统全局接管,引用计数将一直为1 。

当赋值变量的值为 整型,浮点型 时,php7底层将会直接把值存储(php7的结构体将会直接存储简单数据类型),refcount将为0,我们用代码来看一下:

代码语言:javascript
复制
$a = 'chris'.time();
$b = $a;  //此时$b指向$a的同一个内存地址
$c = $a;  //一样
xdebug_debug_zval('a');
//a:(refcount=3, is_ref=0)string 'chris1614780053' (length=15)
我们通过Xdebug来观察变量的信息,他们指的都是同一个内存地址,是引用。
代码语言:javascript
复制
$a = 'chris'.time();
$b = '青玄';
$c = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
//a:(refcount=2, is_ref=0)string 'chris1614780283' (length=15)
 //b:(interned, is_ref=0)string '青玄' (length=6) //存在了静态区
我们通过Xdebug来观察变量的信息,这时$b的值已经发生了变化,这时候,才会使用新的内存空间,$a的引用次数-1。

引用计数

每个php变量存在一个叫”zval”的变量容器中。一个zval变量容器,除了包含变量的类型和值,还包括两个字节的额外信息。第一个是”is_ref”,是个bool值,用来标识这个变量是否是属于引用集合(reference set)。通过这个字节,php引擎才能把普通变量和引用变量区分开来,由于php允许用户通过使用&来使用自定义引用,zval变量容器中还有一个内部引用计数机制,来优化内存使用。第二个额外字节是”refcount”,用以表示指向这个zval变量容器的变量(也称符号即symbol)个数。 官方文档的描述

简单来说,就是给变量引用的次数进行计算,当计数refcount不等于0时,说明这个变量已经被引用,不能直接被回收,否则可以直接回收,用代码来看看把

代码语言:javascript
复制
$a='chris'.time();
$b=$a;
$c=$a; 
$b='青玄';

unset($c);
xdebug_debug_zval('a');
//a:(refcount=1, is_ref=0)string 'chris1614780526'(length=5)
//我们可以看到,删除$c,并不能把$a删除,因为refcount=1

内存泄漏

代码语言:javascript
复制
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机制循环查找回收,或脚本终止回收(域结束无法回收).

PHP作用域的生命周期和变量回收

每个方法/函数都作为一个作用域,当运行完该作用域时,将会回收作用域内的所有变量,全局变量只有在脚本结束后才会回收。

我们可以通过以下方式来手动回收:

  • unset() : unset的回收原理其实就是引用计数-1,当引用计数-1之后为0时,将会直接回收该变量,否则不做操作(这就是上面内存泄漏的原因,引用计数-1并没有等于0)。
  • 赋值为null :=null和unset($a),作用其实都为一致,null将变量值赋值为null,原先的变量值引用计数-1,而unset是将变量名从php底层变量表中清理,并将变量值引用计数-1,唯一的区别在于,=null,变量名还存在,而unset之后,该变量就没了。
  • 变量覆盖回收:通过给变量赋值其他值(例如null)进行回收,但是从程序的内存占用来说,覆盖变量并不是意义上的内存回收,只是将变量的内存修改为了其他值.内存不会直接清空。
  • gc_collect_cycles :强制执行周期回收,在PHP执行中,一旦根缓冲区满了或者调用gc_collect_cycles() 函数时,就会执行垃圾回收

另外:为避免不得不检查所有引用计数可能减少的垃圾周期,PHP会有算法把疑似垃圾的变量,放在根缓冲区(root buffer)中,在根缓冲区满了的时候,也会对垃圾缓冲区进行一次回收。

具体见文档https://www.php.net/manual/zh/features.gc.collecting-cycles.php

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写时复制与引用计数
    • 写时复制
      • 引用计数
      • 内存泄漏
        • PHP作用域的生命周期和变量回收
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档