使用PHP释放内存,unset()和$var = null哪个更好?

266

我意识到第二个方法避免了函数调用的开销(update实际上是一种语言结构),但是知道哪一个更好还是很有趣的。在我的大部分编码中,我一直使用unset(),但最近我看了一些从网络上找到的受人尊敬的类,它们使用$var = null代替。

是否有首选的方法,并且有什么理由?

13个回答

256

在2009年的未设置手册页面中提到:

unset()正如其名,它用于取消变量的设置。它并不强制立即释放内存。PHP的垃圾回收器会在合适的时候进行释放,有意地在那些CPU周期不再需要的时候,或者在脚本将要耗尽内存之前的最后时刻,以先发生的为准。

如果你使用$whatever = null;,那么你正在重写变量的数据。这可能会更快地释放/缩小内存,但它可能会从真正需要它们的代码中夺取CPU周期,导致总体执行时间更长。

(自2013年起,unset手册页面不再包含该部分)

请注意,在php5.3之前,如果你有一个循环引用的两个对象,比如在父子关系中,调用unset()函数释放父对象的内存并不会释放子对象中用于引用父对象的内存。(当父对象被垃圾回收时,内存也不会被释放。)(bug 33595
问题 "unset和= null的区别" 详细说明了一些差异:

unset($a)还会从符号表中移除$a;例如:

$a = str_repeat('hello world ', 100);
unset($a);
var_dump($a);

Outputs:

Notice: Undefined variable: a in xxx
NULL

但是当使用 $a = null 时:
$a = str_repeat('hello world ', 100);
$a = null;
var_dump($a);

Outputs:

NULL

似乎 $a = nullunset() 更快一些:更新符号表条目似乎比删除它更快。
当你尝试使用一个不存在的(unset)变量时,会触发一个错误,并且该变量表达式的值将为null。(因为,PHP应该做什么呢?每个表达式都需要有一个值。)
然而,一个被赋予null的变量仍然是一个完全正常的变量。

20
请注意,如果$whatever指向一个对象,$whatever = null会覆盖指针,而不是对象本身,因此它的作用与unset()基本相同。 - Gras Double
2
@VonC:您所指的php.net上未设置的引号已经不存在了。 - Jürgen Thelen
1
@VonC:当然。我只是不确定“不需要CPU周期”和“在内存耗尽之前”触发垃圾回收。请参见https://dev59.com/PnnZa4cB1Zd3GeqPsJtl。也许您可以解释一下? - Jürgen Thelen
@VonC:误解了,抱歉。我只是想告诉你,这个引用已经不存在了。我在查找另一个问题(何时触发垃圾回收的 C 代码在哪里?)时意识到了这一点。对我来说,重要的不是取消设置或分配 null 更好,而是证明这样的 C 代码确实存在。不应该混淆这一点,抱歉。 - Jürgen Thelen
1
@Omar 我已经编辑了答案:2009年的unset手册(我已经链接到2009年的版本)包括一个当前版本中不再存在的部分。 - VonC
显示剩余8条评论

54

unset 并不是一个函数,而是一种语言结构。它并不像 returninclude 一样是一个函数调用。

除了性能问题外,使用 unset 可以使您的代码意图更加清晰。


1
@VonC:是的,我也想到了,但为什么可以使用小写的 true、false 和 null 呢? - alex
3
@alex,你可以使用unset来做到那个效果。例如:"$test = 4; (unset) $test;",虽然很奇怪但是确实如此,并且它会返回unset之前的$test的值。不过,PHP手册确认它是一种语言结构。 - thomasrutter
5
@alex:PSR-2规范要求所有关键字必须使用小写。 - Tgr
2
@alex - PHP关键字不区分大小写;例如,您也可以将unset拼写为UnSeT。社区已经确定使用全小写作为一种风格,但其他大小写仍然有效。 - Mark Reed
1
@Tgr,PHP就是PHP。PSR就是PSR。 - Pacerier
显示剩余2条评论

40
通过对变量执行unset()操作,实质上是将该变量标记为“垃圾回收”(PHP实际上没有垃圾回收机制,但为了举例而言),因此内存不会立即释放。变量不再存储数据,但堆栈仍保持较大的大小。使用null方法会立即丢弃数据并缩小堆栈内存。
这是根据个人经验和他人的经验得出的结论。请参阅unset()函数的注释这里
我个人在循环的迭代之间使用unset(),这样就不必等待堆栈大小的变化。数据已经消失,但占用的空间仍然存在。在下一次迭代中,内存已经被PHP占用,因此初始化下一个变量更快。

17
如果将某个变量设置为NULL,那么有益处的原因可能是,用于保存NULL值所需的内存少于先前保存其他值所需的内存。例如,对于一个很长的字符串,如果该字符串不是常量且它的引用计数降至零,则应释放该内存。使用unset更加干净——它不再保留引用。虽然需要等待垃圾回收,但安全地将其视为不占用内存,因为低内存条件会触发垃圾回收。 - thomasrutter
1
我们不能同时使用“等于null”和“unset”吗? - Nabeel Khan
3
我建议在循环中使用unset(),并在退出循环时将其置空。否则,在循环内部同时执行两个操作会影响性能。如果你没有使用循环,那么只需要将变量置空,因为它已经在后台执行了unset()逻辑。 - William Holroyd

29
<?php
$start = microtime(true);
for ($i = 0; $i < 10000000; $i++) {
    $a = 'a';
    $a = NULL;
}
$elapsed = microtime(true) - $start;

echo "took $elapsed seconds\r\n";



$start = microtime(true);
for ($i = 0; $i < 10000000; $i++) {
    $a = 'a';
    unset($a);
}
$elapsed = microtime(true) - $start;

echo "took $elapsed seconds\r\n";
?>

根据这个看起来"= null"更快。

PHP 5.4 结果:

  • 花费了0.88389301300049秒
  • 花费了2.1757180690765秒

PHP 5.3 结果:

  • 花费了1.7235369682312秒
  • 花费了2.9490959644318秒

PHP 5.2 结果:

  • 花费了3.0069220066071秒
  • 花费了4.7002630233765秒

PHP 5.1 结果:

  • 花费了2.6272349357605秒
  • 花费了5.0403649806976秒

在 PHP 5.0 和 4.4 中情况开始变得不同。

5.0:

  • 花费了10.038941144943秒
  • 花费了7.0874409675598秒

4.4:

  • 花费了7.5352551937103秒
  • 花费了6.6245851516724秒

请注意,在PHP 4.4中microtime(true)无法使用,因此我必须使用php.net/microtime / Example #1中提供的microtime_float示例。


7
我认为你的测试有缺陷。第一个循环只是简单的赋值,而第二个循环则破坏并重新创建了相同的符号。如果使用数组重新进行测试,“unset”将更快。我有一个测试,在“unset”情况下稍后检查存在性。在该测试中,把它设置为“null”的速度略快。测试链接:http://pastebin.com/fUe57C51 - Knyri
5
在开始计时之前,始终调用gc_collect_cycles以获得更准确的结果。 - Pacerier
@knyri,你能否请提供链接? - Nabeel Khan
@NabeelKhan,我不再拥有那个测试的结果了;但是在我的先前评论中有测试代码的链接。 - Knyri
使用PHP 8,我得到以下结果:花费了0.13596701622009秒,花费了0.12726712226868秒。因此,使用unset略微更快。 - hunter

21

对于通过引用复制的变量,它的工作方式不同:

$a = 5;
$b = &$a;
unset($b); // just say $b should not point to any variable
print $a; // 5

$a = 5;
$b = &$a;
$b = null; // rewrites value of $b (and $a)
print $a; // nothing, because $a = null

7
我已经编写 PHP 几年了,从未见过 "&" 用于引用原始变量。谢谢 +1 :) - Chris
1
$a=78; $b=$a; unset($a); var_dump($b);//78; var_dump($a);//未定义变量:a - zloctb

19

数组元素的顺序是有影响的。

考虑以下示例:

$a = array('test' => 1);
$a['test'] = NULL;
echo "Key test ", array_key_exists('test', $a)? "exists": "does not exist";

这里,关键字“test”仍然存在。然而,在这个例子中

$a = array('test' => 1);
unset($a['test']);
echo "Key test ", array_key_exists('test', $a)? "exists": "does not exist";

该键不再存在。


15

关于对象,特别是在懒加载场景下,应该考虑垃圾回收器在空闲CPU周期中运行,因此假设当加载大量对象时会有麻烦,采取小的时间惩罚来解决内存释放问题。

使用time_nanosleep来启用GC以收集内存。将变量设置为null是可取的。

在生产服务器上经过测试,原本任务消耗50MB,然后被停止。使用nanosleep后,内存消耗保持在14MB。

应该说这取决于GC的行为,可能会随着PHP版本的更替而改变。但它在PHP 5.3上运行良好。

例如,这个示例(代码取自VirtueMart2谷歌商店)

for($n=0; $n<count($ids); $n++)
{
    //unset($product); //usefull for arrays
    $product = null
    if( $n % 50 == 0 )
    {
        // let GC do the memory job
        //echo "<mem>" . memory_get_usage() . "</mem>";//$ids[$n];
        time_nanosleep(0, 10000000);
    }

    $product = $productModel->getProductSingle((int)$ids[$n],true, true, true);
    ...

5

PHP 7已经解决了这种内存管理问题,并将其减少到最小使用。

<?php
  $start = microtime(true);
  for ($i = 0; $i < 10000000; $i++) {
    $a = 'a';
    $a = NULL;
  }
  $elapsed = microtime(true) - $start;

  echo "took $elapsed seconds\r\n";

  $start = microtime(true);
  for ($i = 0; $i < 10000000; $i++) {
     $a = 'a';
     unset($a);
  }
  $elapsed = microtime(true) - $start;

  echo "took $elapsed seconds\r\n";

?>

PHP 7.1 输出:

花费了0.16778993606567秒 花费了0.16630101203918秒


4

仅供参考,不包括所需时间:

<?php
echo "<hr>First:<br>";
$x = str_repeat('x', 80000);
echo memory_get_usage() . "<br>\n";      
echo memory_get_peak_usage() . "<br>\n"; 
echo "<hr>Unset:<br>";
unset($x);
$x = str_repeat('x', 80000);
echo memory_get_usage() . "<br>\n";      
echo memory_get_peak_usage() . "<br>\n"; 
echo "<hr>Null:<br>";
$x=null;
$x = str_repeat('x', 80000);
echo memory_get_usage() . "<br>\n";      
echo memory_get_peak_usage() . "<br>\n";

echo "<hr>function:<br>";
function test() {
    $x = str_repeat('x', 80000);
}
echo memory_get_usage() . "<br>\n";      
echo memory_get_peak_usage() . "<br>\n"; 

echo "<hr>Reasign:<br>";
$x = str_repeat('x', 80000);
echo memory_get_usage() . "<br>\n";      
echo memory_get_peak_usage() . "<br>\n"; 

它返回

First:
438296
438352
Unset:
438296
438352
Null:
438296
438352
function:
438296
438352
Reasign:
438296
520216 <-- double usage.

结论:无论是null还是unset都能如预期地释放内存(不仅在执行结束时)。此外,重新分配变量在某些时候会将值保存两次(520216与438352)。


4

示例代码来自评论

echo "PHP Version: " . phpversion() . PHP_EOL . PHP_EOL;

$start = microtime(true);
for ($i = 0; $i < 10000000; $i++) {
    $a = 'a';
    $a = NULL;
}
$elapsed = microtime(true) - $start;

echo "took $elapsed seconds" . PHP_EOL;



$start = microtime(true);
for ($i = 0; $i < 10000000; $i++) {
    $a = 'a';
    unset($a);
}
$elapsed = microtime(true) - $start;

echo "took $elapsed seconds" . PHP_EOL;

在使用php:7.4-fpm镜像和其他镜像运行的Docker容器中。

PHP Version: 7.4.8
took 0.22569918632507 seconds null
took 0.11705803871155 seconds unset
took 0.20791196823121 seconds null
took 0.11697316169739 seconds unset

PHP Version: 7.3.20
took 0.22086310386658 seconds null
took 0.11882591247559 seconds unset
took 0.21383500099182 seconds null
took 0.11916995048523 seconds unset

PHP Version: 7.2.32
took 0.24728178977966 seconds null
took 0.12719893455505 seconds unset
took 0.23839902877808 seconds null
took 0.12744522094727 seconds unset

PHP Version: 7.1.33
took 0.51380109786987 seconds null
took 0.50135898590088 seconds unset
took 0.50358104705811 seconds null
took 0.50115609169006 seconds unset

PHP Version: 7.0.33
took 0.50918698310852 seconds null
took 0.50490307807922 seconds unset
took 0.50227618217468 seconds null
took 0.50514912605286 seconds unset

PHP Version: 5.6.40
took 1.0063569545746 seconds null
took 1.6303179264069 seconds unset
took 1.0689589977264 seconds null
took 1.6382601261139 seconds unset

PHP Version: 5.4.45
took 1.0791940689087 seconds null
took 1.6308979988098 seconds unset
took 1.0029168128967 seconds null
took 1.6320278644562 seconds unset

不过,举个例子:

<?php
ini_set("memory_limit", "512M");

echo "PHP Version: " . phpversion() . PHP_EOL . PHP_EOL;

$start = microtime(true);
$arr = [];
for ($i = 0; $i < 1000000; $i++) {
    $arr[] = 'a';
}
$arr = null;
$elapsed = microtime(true) - $start;

echo "took $elapsed seconds" . PHP_EOL;



$start = microtime(true);
$arr = [];
for ($i = 0; $i < 1000000; $i++) {
    $arr[] = 'a';
}
unset($arr);
$elapsed = microtime(true) - $start;

echo "took $elapsed seconds" . PHP_EOL;

结果:

PHP Version: 7.4.8
took 0.053696155548096 seconds
took 0.053897857666016 seconds

PHP Version: 7.3.20
took 0.054572820663452 seconds
took 0.054342031478882 seconds

PHP Version: 7.2.32
took 0.05678391456604 seconds
took 0.057311058044434 seconds


PHP Version: 7.1.33
took 0.097366094589233 seconds
took 0.073100090026855 seconds

PHP Version: 7.0.33
took 0.076443910598755 seconds
took 0.077098846435547 seconds

PHP Version: 7.0.33
took 0.075634002685547 seconds
took 0.075317859649658 seconds

PHP Version: 5.6.40
took 0.29681086540222 seconds
took 0.28199100494385 seconds

PHP Version: 5.4.45
took 0.30513095855713 seconds
took 0.29265689849854 seconds


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接