前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >PHP编程语言垃圾回收是什么?

PHP编程语言垃圾回收是什么?

作者头像
Tinywan
发布2024-03-11 13:43:12
1860
发布2024-03-11 13:43:12
举报
文章被收录于专栏:开源技术小栈

概念

PHP的垃圾回收机制是自动的,它通过内置的垃圾回收器(Garbage Collector)来实现。当一个PHP对象不再被引用时,它就成为垃圾。垃圾回收器会定期扫描内存中的所有对象,将没有引用的对象标记为垃圾,并释放它们占用的内存空间,以便其他对象可以使用这些空间。

PHP的垃圾回收机制使用了 引用计数(reference counting) 的算法来跟踪对象的引用情况。每个对象都有一个引用计数器,它记录着对象当前被引用的次数。当一个对象被赋给一个变量时,它的引用计数器会增加1;当一个变量不再引用该对象时,它的引用计数器会减少1。当引用计数器降为0时,这个对象就成为垃圾,垃圾回收器就会释放它所占用的内存。

PHP的垃圾回收机制是自动的,程序员无需手动管理内存。但是,如果程序中存在循环引用的情况,垃圾回收器就无法释放这些对象。为了避免这种情况的发生,PHP提供了一种手动解除引用的方法,即将对象赋值为null,这样就可以让对象的引用计数器降为0,从而被垃圾回收器释放。

引用计数基础

PHP 变量存储在称为zval的容器中。zval 容器除了变量的类型和值之外,还包含两个额外的信息位。第一个是is_ref,是布尔值,表示变量是否是“引用集合”的一部分。通过这个位,PHP 引擎知道如何区分普通变量和引用。由于 PHP 允许用户自定义引用,通过 & 运算符创建引用,zval 容器还有内部引用计数机制来优化内存使用。第二个是refcount,表示有多少个变量名(也称为符号)指向这个 zval 容器。所有符号都存储在一个符号表中,每个作用域都有一个符号表。主脚本(即通过浏览器请求的脚本)有一个作用域,每个函数或方法也有一个作用域。

当使用常量值创建新变量时,也会创建 zval 容器,例如

示例 #1 创建新 zval 容器

代码语言:javascript
复制
<?php
$a = "new string";

在这种情况下,新的符号名称 a 会在当前作用域中创建,并且会创建新的变量容器,其类型为 string,值为 new string。由于没有创建用户定义的引用,is_ref位默认设置为 falserefcount设置为 1,因为只有一个符号使用了这个变量容器。请注意,具有refcount为 1 的引用(即is_ref为 true)会视为非引用(即is_reffalse)。如果安装了 » Xdebug,可以通过调用 xdebug_debug_zval() 来显示此信息。

示例 #2 显示 zval 信息

代码语言:javascript
复制
<?php
$a = "new string";
xdebug_debug_zval('a');

以上示例会输出:

代码语言:javascript
复制
a: (refcount=1, is_ref=0)='new string'

将这个变量赋值给另一变量名将增加 refcount 的计数。

示例 #3 增加 zval 的 ``refcount

代码语言:javascript
复制
<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );

以上示例会输出:

代码语言:javascript
复制
a: (refcount=2, is_ref=0)='new string'

这里的 refcount 是 2,因为同一个变量容器链接到 ab。PHP 很聪明,当没有必要的时候,不会复制实际的变量容器。当refcount0 时,就会销毁变量容器。当链接到变量容器的任何符号离开作用域(例如函数结束时)或取消符号赋值(例如通过调用 unset())时,refcount会减少 1。以下是示例:

示例 #4 减少 zval refcount

代码语言:javascript
复制
<?php
$a = "new string";
$c = $b = $a;
xdebug_debug_zval( 'a' );
$b = 42;
xdebug_debug_zval( 'a' );
unset( $c );
xdebug_debug_zval( 'a' );

以上示例会输出:

代码语言:javascript
复制
a: (refcount=3, is_ref=0)='new string'
a: (refcount=2, is_ref=0)='new string'
a: (refcount=1, is_ref=0)='new string'

如果现在调用 unset($a);,变量容器,包含类型和值,会从内存中移除。

复合类型

对于 array 和 object 这样的复合类型,情况会稍微复杂一些。与 scalar 值不同,array 和 object 的属性存储在自己的符号表中。这意味着以下示例将创建三个 zval 容器:

示例 #5 创建 array zval

代码语言:javascript
复制
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
xdebug_debug_zval( 'a' );

以上示例的输出类似于:

代码语言:javascript
复制
a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=1, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42
)

图示:

这三个 zval 变量容器是ameaningnumber。增加和减少refcounts的规则也适用于此。下面,再向数组添加一个元素,并将其值设置为已存在元素的内容:

示例 #6 添加已存在的元素到数组

代码语言:javascript
复制
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
xdebug_debug_zval( 'a' );

以上示例的输出类似于:

代码语言:javascript
复制
a: (refcount=1, is_ref=0)=array (
   'meaning' => (refcount=2, is_ref=0)='life',
   'number' => (refcount=1, is_ref=0)=42,
   'life' => (refcount=2, is_ref=0)='life'
)

图示:

从上面的 Xdebug 输出中,可以看到新旧的数组元素现在都指向refcount2zval 容器。尽管 Xdebug 的输出显示了两个值为 'life' 的 zval 容器,但它们实际上是同一个。xdebug_debug_zval() 函数没有显示这一点,但可以通过显示内存指针来看到它。

从数组中删除元素就像从作用域中删除符号一样。删除后,数组元素指向的容器的refcount会减少。同样,当refcount0 时,变量容器就会从内存中删除。再举个例子来说明这一点:

示例 #7 从数组中删除元素

代码语言:javascript
复制
<?php
$a = array( 'meaning' => 'life', 'number' => 42 );
$a['life'] = $a['meaning'];
unset( $a['meaning'], $a['number'] );
xdebug_debug_zval( 'a' );

以上示例的输出类似于:

代码语言:javascript
复制
a: (refcount=1, is_ref=0)=array (
   'life' => (refcount=1, is_ref=0)='life'
)

现在,如果将数组本身作为数组的一个元素添加进去,情况就会变得有趣起来。在下一个例子中这样做,并且偷偷加入引用运算符,否则 PHP 会创建副本:

示例 #8 将数组本身作为其自身的一个元素添加进去

代码语言:javascript
复制
<?php
$a = array( 'one' );
$a[] =& $a;
xdebug_debug_zval( 'a' );

以上示例的输出类似于:

代码语言:javascript
复制
a: (refcount=2, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=2, is_ref=1)=...
)

图示:

可以看到数组变量(a)以及第二个元素(1)现在都指向refcount为 2 的变量容器。上面显示的...表示存在递归,这在这种情况下意味着...指向原数组。

就像之前一样,清除变量会删除符号,并且指向的变量容器的引用计数会减少 1。因此,如果在运行上述代码后清除变量 a,那么 a 和元素1所指向的变量容器的引用计数会减少 1,从2变为1。可以表示为:

示例 #9 清除 $a

代码语言:javascript
复制
(refcount=1, is_ref=1)=array (
   0 => (refcount=1, is_ref=0)='one',
   1 => (refcount=1, is_ref=1)=...
)

图示:

清理问题

虽然在任何作用域中都没有指向这个结构的符号,却无法清理它,因为数组元素“1”仍然指向同一个数组。由于没有外部符号指向它,用户无法清理该结构;因此会出现内存泄漏。幸运的是,PHP 会在请求结束时清理这个数据结构,但在此之前,它会占用宝贵的内存空间。如果你正在实现解析算法或其他需要子级元素指向"父级"元素的情况,会经常发生。当然,object 也可能出现相同的情况,因为 object 始终隐式引用。

如果这种情况只发生一两次,可能不是问题,但如果出现数千次,甚至数百万次的内存损失,显然就成了问题。这在长时间运行的脚本中尤为棘手,比如守护进程,其中请求基本上永远不会结束,或者在大量的单元测试集中。后者在运行 eZ Components 库的模板组件的单元测试时出现了问题。在某些情况下,它需要超过 2GB 的内存,而测试服务器并没有那么多内存可用。

回收循环

传统上,像 PHP 之前使用的引用计数内存机制无法解决循环引用内存泄漏的问题;然而,从 5.3.0 版本开始,PHP 实施了» 引用计数系统中的同步循环回收论文中的同步算法来解决这个问题。

对算法的完全说明有点超出这部分内容的范围,将只介绍其中基础部分。首先,需要确立一些基本规则。如果 refcount 增加,则该变量仍在使用中,因此不是垃圾。如果 refcount 减少到 0,则 zval 可以释放。这意味着只有当引用计数参数减少到非零值时,才能创建垃圾循环。其次,在垃圾循环中,可以通过检查是否可以将 refcount 减少 1,并检查哪些 zvalrefcount0 来确定哪些部分是垃圾。

为避免不得不检查所有引用计数可能减少的垃圾循环,这个算法把所有可能根(possible roots 都是zval变量容器),放在根缓冲区(root buffer)中(用紫色来标记,称为疑似垃圾),这样可以同时确保每个可能的垃圾根(possible garbage root)在缓冲区中只出现一次。仅仅在根缓冲区满了时,才对缓冲区内部所有不同的变量容器执行垃圾回收操作。看上图的步骤 A。

在步骤 B 中,模拟删除每个紫色变量。模拟删除时可能将不是紫色的普通变量引用数减"1",如果某个普通变量引用计数变成0了,就对这个普通变量再做一次模拟删除。每个变量只能被模拟删除一次,模拟删除后标记为灰(原文说确保不会对同一个变量容器减两次"1",不对的吧)。

在步骤 C 中,模拟恢复每个紫色变量。恢复是有条件的,当变量的引用计数大于0时才对其做模拟恢复。同样每个变量只能恢复一次,恢复后标记为黑,基本就是步骤 B 的逆运算。这样剩下的一堆没能恢复的就是该删除的蓝色节点了,在步骤 D 中遍历出来真的删除掉。

算法中都是模拟删除、模拟恢复、真的删除,都使用简单的遍历即可(最典型的深搜遍历)。复杂度为执行模拟操作的节点数正相关,不只是紫色的那些疑似垃圾变量。

对算法的工作原理有了基本的了解后,现在可以回顾一下如何与 PHP 集成。默认情况下,PHP 的垃圾回收器是打开的。然而,有个 php.ini 设置可以进行更改:zend.enable_gc

当打开垃圾回收器时,如上所述的循环查找算法将在根缓冲区满时执行。根缓冲区的大小是固定的,可以容纳 10,000 个可能的根(尽管可以通过更改 PHP 源代码中的 Zend/zend_gc.c 中的 GC_THRESHOLD_DEFAULT 常量并重新编译 PHP 来修改这个值)。当关闭垃圾回收器时,循环查找算法将永不运行。然而,无论是否使用此配置激活垃圾回收机制,可能根都将始终记录在根缓冲区中。

如果在垃圾回收机制关闭时,根缓冲区存满了可能的根,那么将不会记录进一步的可能根。算法永远不会分析那些没有记录的可能根。如果他们是循环引用的一部分,将永不会清除从而导致内存泄漏的产生。

即使在垃圾回收机制不可用时,可能根也被记录的原因是,相对于每次找到可能根后检查垃圾回收机制是否打开而言,记录可能根的操作更快。不过垃圾回收和分析机制本身要耗不少时间。

除了改变配置中的 zend.enable_gc 之外,还可以通过调用 gc_enable()gc_disable() 来启用/禁用垃圾回收机制。调用这些函数与通过配置打开或关闭机制的效果相同。即使可能的根缓冲区尚未满,还可以强制回收循环。为此,可以使用 gc_collect_cycles() 函数。该函数将返回算法回收的循环数量。

允许打开和关闭垃圾回收机制并且允许自主的初始化的原因,是由于你的应用程序的某部分可能是高时效性的。在这种情况下,你可能不想使用垃圾回收机制。当然,对你的应用程序的某部分关闭垃圾回收机制,是在冒着可能内存泄漏的风险,因为一些可能根也许存不进有限的根缓冲区。因此,就在你调用gc_disable()函数释放内存之前,先调用gc_collect_cycles()函数可能比较明智。因为这将清除已存放在根缓冲区中的所有可能根,然后在垃圾回收机制被关闭时,可留下空缓冲区以有更多空间存储可能根。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-03-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源技术小栈 微信公众号,前往查看

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

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

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