PHP V5.2 中的新增功能,第 1 部分: 使用新的内存管理器

像跟踪和监视 uber-nerd 一样跟踪和监视 PHP 内存

了解如何使用 PHP V5.2 中引入的新内存管理器并开始精通于跟踪和监视内存使用情况。这将使您可以在 PHP V5.2 中更加有效地使用更多的内存。

Tracy Peterson (tracy@tracypeterson.com), 自由撰稿人, Consultant

自 1997 年以来,Tracy Peterson 担任过 IT 项目经理和 Web 开发人员,并且目前担任 Microsoft 的 MSN Search 的运算程序主管。他目前的工作地点位于旧金山。



2007 年 4 月 10 日

PHP V5.2:开始

2006 年 11 月发布了 PHP V5.2,它包括许多新增功能和错误修正。它废止了 5.1 版并被推荐给所有 PHP V5 用户进行升级。我最喜欢的实验室环境 —— Windows®、Apache、MySQL、PHP (WAMP) —— 已经被引入了 V5.2 的新软件包中(请参阅 参考资料)。您将在那里找到在 Windows® XP 或 2003 计算机上安装 PHP V5.2、MySQL 和 Apache 的应用程序。您可以十分轻松地进行安装,它有很多不错的小的管理优点,并且我十分诚恳地推荐使用它。

虽然对于 Windows 用户来说,这是最简单的软件包,但是在 Linux 上配置 PHP 时您需要添加以下代码:--memory-limit-enabled(适用于您服务器的任何其他选项除外)。不过,在 Windows 下,提供了一个解决此问题的函数。

PHP V5.2 中有很多改进之处,并且一个至关重要的领域是内存管理。从 README.ZEND_MM 中准确地引述就是:“新内存管理器(PHP5.2 以及更高版本)的目标是减少内存分配开销并加速内存管理。”

下面是 V5.2 发行说明中的一些关键内容:

  • 删除了不必要的 --disable-zend-memory-manager 配置选项
  • 添加了 --enable-malloc-mm 配置选项,调试构建时此配置选项将被默认启用以允许使用内部和外部内存调试程序
  • 允许使用 ZEND_MM_MEM_TYPEZEND_MM_SEG_SIZE 环境变量调整内存管理器

为了理解这些新增功能的含义,我们需要深入研究内存管理中的艺术,并考虑为什么分配开销和运行速度是大问题。


为什么进行内存管理?

计算中开发最快速的一项技术是内存和数据存储,它们是受不断增加速度和存储大小这样持续的需求而驱动的。早期的计算机使用卡作为内存,然后转向了芯片技术。您能想象在只有 1 KB RAM 内存的计算机工作的情景吗?很多早期的计算机程序员就曾使用过。这些先驱者很快就意识到,要在技术限制下工作,他们将必须细心地用琐碎的命令避免系统过载。

身为 PHP 开发人员,与使用 C++ 或其他更严格的语言编码的同事相比,我们所在的环境更方便进行编码。在我们的世界里,我们自己不必担心如何处理系统内存,因为 PHP 将为我们处理这个问题。但是,在其他编程领域里,负责任的编码人员将使用各种函数确保执行的命令不会覆盖其他一些程序数据 —— 因而,破坏了程序的运行。

内存管理通常是由来自编码人员的请求处理的,以分配和释放内存块。分配块 可以保存任何类型的数据,并且此过程将为该数据隔开一定量的内存,并当操作需要访问数据时为应用程序提供访问方法。人们期望程序在完成任何操作后释放分配的内存,并允许系统和其他程序员使用该内存。如果程序没有把内存释放回系统,则称为内存泄露

泄露是任何运行程序都存在的普遍问题,并且某种程度内通常是可以接受的,尤其是当我们知道运行程序将立即终止并释放默认分配给程序的所有内存。

由于随机运行和终止程序,像几乎所有客户机应用程序一样,这是个问题。期望服务器应用程序不确定地运行而不终止或重新启动,这使得内存管理对于服务器守护程序编程绝对的至关重要。在长时间运行的程序中,即使一个小的泄露最后都将发展为系统衰弱问题,因为内存块已被使用并且永远不被释放。


长期考虑

正如使用任何语言编写一样,用 PHP 编写的永久性服务器守护程序有很多可能的用途。但是当我们出于这些目的开始使用 PHP 时,我们也必须考虑内存使用情况。

解析大量数据或可能隐藏无限次循环的脚本都趋于消耗大量内存。很明显,一旦内存被耗尽,服务器的性能就降低,因此在执行脚本时我们还必须注意内存的使用情况。虽然我们可以通过启用系统监视器来简单观察内存的使用量,但是它不会告诉我们比整个系统内存状态更有用的任何内容。有时我们不止需要帮助进行故障检修或优化的内容,而有时我们只是需要更多详细信息。

获得脚本执行内容的透明性的一种方法是使用内部或外部调试器。内部调试器 是呈现为执行脚本的相同的进程。从操作系统的角度考虑的独立进程是外部调试器。使用调试器进行内存分析类似于任何一种情况,但是使用了不同的方法访问内存。内部调试器对运行进程所在的内存空间具有直接访问权,而外部调试器将通过套接字访问内存。

有许多方法和可用的调试服务器(外部)和库(内部)可用于辅助开发。为了准备好对 PHP 安装进行调试,可以使用新提供的 --enable-malloc-mm,它在 DEBUG 构建中默认被启用。这使环境变量 USE_ZEND_ALLOC 可用于允许在运行时选择 mallocemalloc 内存分配。使用 malloc-type 内存分配将允许外部调试器观察内存使用情况,而 emalloc 分配将使用 Zend 内存管理器抽象,要求进行内部调试。


PHP 中的内存管理函数

除了使内存管理器更灵活更透明之外,PHP V5.2 还为 memory_get_usage()memory_get_peak_usage() 提供了一个新参数,这两个函数允许查看内存使用量。说明中提及的新布尔值是 real_size。通过调用函数 memory_get_usage($real);(其中 $real = true),结果将为调用时系统中实际分配的内存大小,包括内存管理器开销。如果不使用标记组,则返回的数据将只包括在运行脚本内使用的内存,减去内存管理器开销。

memory_get_usage()memory_get_peak_usage() 的不同之处在于后者将返回到目前为止调用它的运行进程的最高内存量,而前者只返回执行时的使用量。

对于 memory_get_usage(),php.net 提供了清单 1 中的代码片段。

清单 1. memory_get_usage() 示例
<?php

// This is only an example, the numbers below will 
// differ depending on your system

echo memory_get_usage() . "\n"; // 36640
$a = str_repeat("Hello", 4242);
echo memory_get_usage() . "\n"; // 57960
unset($a);
echo memory_get_usage() . "\n"; // 36744

?>

在这个简单示例中,我们首先回转了直接调用 memory_get_usage() 的结果,代码注释中显示可能在作者的系统中有 36640 字节的常见结果。然后我们使用 4,242 个 “Hello” 副本来装载 $a 并再次运行函数。图 1 中可以看到此简单应用的输出。

图 1. memory_get_usage() 的示例输出
memory_get_usage() 的示例输出

没有 memory_get_peak_usage() 的示例,因为两者十分相似,语法是相同的。但是,对于清单 1 中的示例代码,将只有一个结果,即当时的最高内存使用量。让我们看一看清单 2。

清单 2. memory_get_peak_usage() 示例
<?php

// This is only an example, the numbers below will 
// differ depending on your system

echo memory_get_peak_usage() . "\n"; // 36640
$a = str_repeat("Hello", 4242);
echo memory_get_peak_usage() . "\n"; // 57960
unset($a);
echo memory_get_peak_usage() . "\n"; // 36744

?>

清单 2 中的代码跟图 1 一样,但是 memory_get_usage() 已经替换为 memory_get_peak_usage()。在我们用 4242 个 “Hello” 副本填充 $a 之前,输出都不会有多大更改。内存跳升至 57960,表示到目前为止的峰值。当检查内存使用量峰值时,得到了目前为止的最高值,因此所有进一步调用都将得到 57960,直至我们处理的操作比处理 $a 使用的内存更多(参见图 2)。

图 2. memory_get_peak_usage() 的示例输出
memory_get_peak_usage() 的示例输出

限制内存使用

确保托管应用程序的服务器不过载的一种方法是限制 PHP 执行的任何脚本使用的内存量。这根本不是我们应当执行的操作,但由于 PHP 是一种松散类型的语言,并且是在运行时解析的,因此我们有时会获得在释放到生产应用程序中后编写得很差的脚本。这些脚本可能执行循环,也可能打开一张长的文件列表,忘记在打开新文件之前先关闭当前文件。无论在哪一种情况下,编写很差的脚本可能在您知道之前以消耗大量内存告终。

在 PHP.INI 中,您可以使用配制参数 memory_limit 来指定任何脚本能够在系统中运行的最大内存使用量。这不是对于 V5.2 的特定更改,但是内存管理器及其使用的任何讨论都值得至少快速查看一次这个特性。它还精心地引导我使用内存管理器的最后几个新功能:环境变量。


调整内存管理器

最后,在不能做完美主义者但是又完全符合自己目的的情况下怎样编程?新环境变量 ZEND_MM_MEM_TYPEZEND_MM_SEG_SIZE 正好可以满足您的需求。

当内存管理器分配大型内存块时,它是安装 ZEND_MM_SEG_SIZE 变量中列出的预定大小执行操作的。这些内存块的默认分区大小为每块 256 KB,但是您可以调整这些分区大小以满足特殊需求。例如,如果您注意到最常用的一个脚本中的操作导致大量的内存浪费,则可以将此大小调整为更接近匹配脚本需求的值,减少分配的内存量但剩下的内存量仍然为零。在正确的条件下,此类谨慎的配制调整可能造成巨大差别。


在 Windows 中检索内存使用情况

如果具有预构建的 PHP Windows 二进制代码,而没有在构建时使用 --enable-memory-limit 选项,则需要先浏览此部分然后再继续。对于 Linux®,配置 PHP 构建时用 --enable-memory-limit 选项构建 PHP。

要使用 Windows 二进制代码检索内存使用情况,请创建以下函数。

清单 3. 在 Windows 下获得内存使用情况
<?php

function memory_get_usage(){
    $output = array();
    exec('tasklist /FI "PID eq '.getmypid().'" /FO LIST', $output );
    return preg_replace( '/[^0-9]/', '', $output[5] ) * 1024;
}

?>

将结果保存到名为 function.php 的文件。现在您只能将此文件包含在需要使用它的脚本中。


动手实践

让我们来看一看使用这些设置的实际示例给我们带来的好处。可能有很多次您都想知道为什么在脚本的末尾没有正确分配内存。原因是因为一些函数本身导致了内存泄露,尤其是在仅使用内置 PHP 函数的情况下。在这里,您将了解如何发现此类问题。并且为了开始进行内存泄露查找的征战,您将创建一个测试 MySQL 数据库,如清单 4 所示。

清单 4. 创建测试数据库
mysql> create database memory_test;

mysql> use memory_test;

mysql> create table leak_test
       ( id int not null primary key auto_increment,
         data varchar(255) not null default '');

mysql> insert into leak_test (data) values ("data1"),("data 2"),
       ("data 3"),("data 4"),("data 5"),("data6"),("data 7"),
       ("data 8"),("data 9"),("data 10");

这将创建一个带有 ID 字段和数据字段的简单表。

在下一张清单中,想象我们坚韧不拔的程序员正在执行一些 MySQL 函数,特别是使用 mysql_query() 将结果应用到变量。当他这样做时,他将注意到即使调用 mysql_free_result(),一些内存也不会被释放,导致内存使用量随着 Apache 进程不断增长(参见清单 5)。

清单 5. 内存泄露检测示例
for ( $x=0; $x<300; $x++ ) {
    $db = mysql_connect("localhost", "root", "test");
    mysql_select_db("test");
    $sql  = "SELECT data FROM test"; 
    $result = mysql_query($sql); // The operation suspected of leaking
    mysql_free_result($result);
    mysql_close($db);
 }

清单 5 是在任何位置都可能使用的简单 MySQL 数据库操作。在运行脚本时,我们注意到一些与内存使用量相关的奇怪行为并需要将其检查出来。为了使用内存管理函数以使我们可以检验发生错误的位置,我们将使用以下代码。

清单 6. 定标查找错误的示例
<?php

if( !function_exists('memory_get_usage') ){ 
    include('function.php');
}

echo "At the start we're using (in bytes): ",
     memory_get_usage() , "\n<br>";
    
$db = mysql_connect("localhost", "user", "password");
mysql_select_db("memory_test");

echo "After connecting, we're using (in bytes): ",
     memory_get_usage(),"\n<br>"; 

for ( $x=0; $x<10; $x++ ) {
    $sql  =
        "SELECT data FROM leak_test WHERE id='".$x."'"; 
    $result = mysql_query($sql); // The operation
                                 // suspected of leaking.
    echo "After query #$x, we're using (in bytes): ",
         memory_get_usage(), "\n<br>";
    mysql_free_result($result);
    echo "After freeing result $x, we're using (in bytes): ",
         memory_get_usage(), "\n<br>";
}
  
mysql_close($db);
echo "After closing the connection, we're using (in bytes): ",
     memory_get_usage(), "\n<br>";
echo "Peak memory usage for the script (in bytes):".
     memory_get_peak_usage();

?>

注:按照定义的时间间隔检查当前内存使用量。在下面的输出中,通过显示我们的脚本一直在为函数分配内存,并且在应当释放的时候没有释放内存,从而提供对内存泄露的实际测试,您可以看到每次调用时内存使用量如何增长。

清单 7. 测试脚本输出
At the start we're using (in bytes): 63216
After connecting, we're using (in bytes): 64436
After query #0, we're using (in bytes): 64760
After freeing result 0, we're using (in bytes): 64828
After query #1, we're using (in bytes): 65004
After freeing result 1, we're using (in bytes): 65080
After query #2, we're using (in bytes): 65160
After freeing result 2, we're using (in bytes): 65204
After query #3, we're using (in bytes): 65284
After freeing result 3, we're using (in bytes): 65328
After query #4, we're using (in bytes): 65408
After freeing result 4, we're using (in bytes): 65452
After query #5, we're using (in bytes): 65532
After freeing result 5, we're using (in bytes): 65576
After query #6, we're using (in bytes): 65656
After freeing result 6, we're using (in bytes): 65700
After query #7, we're using (in bytes): 65780
After freeing result 7, we're using (in bytes): 65824
After query #8, we're using (in bytes): 65904
After freeing result 8, we're using (in bytes): 65948
After query #9, we're using (in bytes): 66028
After freeing result 9, we're using (in bytes): 66072
After closing the connection, we're using (in bytes): 65108
Peak memory usage for the script (in bytes): 88748

我们所做的操作是发现了执行脚本时出现的一些可疑操作,然后调整脚本使其给我们提供一些可理解的反馈。我们再次运行了脚本,在每次迭代期间使用 memory_get_usage() 查看内存使用量的变化。根据分配的内存值的增长情况,暗示了我们用脚本在某个位置建立了一个漏洞。由于 mysql_free_result() 函数不释放内存,因此我们可以认为 mysql_query() 并未正确分配内存。


结束语

PHP V5.2 版包括一些优秀的新工具,可以帮助您更好地洞察脚本的系统内存分配情况,以及重新全面控制内存管理的精确调整。当得到有效使用时,新内存管理工具将支持您的调试工作,从而重新获得一些系统资源。

参考资料

学习

获得产品和技术

  • 使用 IBM 试用软件 构建您的下一个开发项目,这些软件可以通过下载或从 DVD 中获得。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=208310
ArticleTitle=PHP V5.2 中的新增功能,第 1 部分: 使用新的内存管理器
publish-date=04102007