PHP V5.2 中的新增功能,第 1 部分: 使用新的内存管理器
像跟踪和监视 uber-nerd 一样跟踪和监视 PHP 内存
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_TYPE
和ZEND_MM_SEG_SIZE
环境变量调整内存管理器
为了理解这些新增功能的含义,我们需要深入研究内存管理中的艺术,并考虑为什么分配开销和运行速度是大问题。
计算中开发最快速的一项技术是内存和数据存储,它们是受不断增加速度和存储大小这样持续的需求而驱动的。早期的计算机使用 卡作为内存,然后转向了芯片技术。您能想象在只有 1 KB RAM 内存的计算机工作的情景吗?很多早期的计算机程序员就曾使用过。这些先驱者很快就意识到,要在技术限制下工作,他们将必须细心地用琐碎的命令避免系统过 载。
身为 PHP 开发人员,与使用 C++ 或其他更严格的语言编码的同事相比,我们所在的环境更方便进行编码。在我们的世界里,我们自己不必担心如何处理系统内存,因为 PHP 将为我们处理这个问题。但是,在其他编程领域里,负责任的编码人员将使用各种函数确保执行的命令不会覆盖其他一些程序数据 —— 因而,破坏了程序的运行。
内存管理通常是由来自编码人员的请求处理的,以分配和释放内存块。分配块 可以保存任何类型的数据,并且此过程将为该数据隔开一定量的内存,并当操作需要访问数据时为应用程序提供访问方法。人们期望程序在完成任何操作后释放分配的内存,并允许系统和其他程序员使用该内存。如果程序没有把内存释放回系统,则称为内存泄露 。
泄露是任何运行程序都存在的普遍问题,并且某种程度内通常是可以接受的,尤其是当我们知道运行程序将立即终止并释放默认分配给程序的所有内存。
由于随机运行和终止程序,像几乎所有客户机应用程序一样,这是个问题。期望服务器应用程序不确定地运行而不终止或重新启 动,这使得内存管理对于服务器守护程序编程绝对的至关重要。在长时间运行的程序中,即使一个小的泄露最后都将发展为系统衰弱问题,因为内存块已被使用并且 永远不被释放。
正如使用任何语言编写一样,用 PHP 编写的永久性服务器守护程序有很多可能的用途。但是当我们出于这些目的开始使用 PHP 时,我们也必须考虑内存使用情况。
解析大量数据或可能隐藏无限次循环的脚本都趋于消耗大量内存。很明显,一旦内存被耗尽,服务器的性能就降低,因此在执行脚 本时我们还必须注意内存的使用情况。虽然我们可以通过启用系统监视器来简单观察内存的使用量,但是它不会告诉我们比整个系统内存状态更有用的任何内容。有 时我们不止需要帮助进行故障检修或优化的内容,而有时我们只是需要更多详细信息。
获得脚本执行内容的透明性的一种方法是使用内部或外部调试器。内部调试器 是呈现为执行脚本的相同的进程。从操作系统的角度考虑的独立进程是外部调试器 。使用调试器进行内存分析类似于任何一种情况,但是使用了不同的方法访问内存。内部调试器对运行进程所在的内存空间具有直接访问权,而外部调试器将通过套接字访问内存。
有许多方法和可用的调试服务器(外部)和库(内部)可用于辅助开发。为了准备好对 PHP 安装进行调试,可以使用新提供的 --enable-malloc-mm
,它在 DEBUG
构建中默认被启用。这使环境变量 USE_ZEND_ALLOC
可用于允许在运行时选择 malloc 或 emalloc 内存分配。使用 malloc-type 内存分配将允许外部调试器观察内存使用情况,而 emalloc 分配将使用 Zend 内存管理器抽象,要求进行内部调试。
除了使内存管理器更灵活更透明之外,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 中的代码片段。
<?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 中可以看到此简单应用的输出。
没有 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()
的示例输出
确保托管应用程序的服务器不过载的一种方法是限制 PHP 执行的任何脚本使用的内存量。这根本不是我们应当执行的操作,但由于 PHP 是一种松散类型的语言,并且是在运行时解析的,因此我们有时会获得在释放到生产应用程序中后编写得很差的脚本。这些脚本可能执行循环,也可能打开一张长的 文件列表,忘记在打开新文件之前先关闭当前文件。无论在哪一种情况下,编写很差的脚本可能在您知道之前以消耗大量内存告终。
在 PHP.INI 中,您可以使用配制参数 memory_limit
来指定任何脚本能够在系统中运行的最大内存使用量。这不是对于 V5.2 的特定更改,但是内存管理器及其使用的任何讨论都值得至少快速查看一次这个特性。它还精心地引导我使用内存管理器的最后几个新功能:环境变量。
最后,在不能做完美主义者但是又完全符合自己目的的情况下怎样编程?新环境变量 ZEND_MM_MEM_TYPE
和 ZEND_MM_SEG_SIZE
正好可以满足您的需求。
当内存管理器分配大型内存块时,它是安装 ZEND_MM_SEG_SIZE
变量中列出的预定大小执行操作的。这些内存块的默认分区大小为每块 256 KB,但是您可以调整这些分区大小以满足特殊需求。例如,如果您注意到最常用的一个脚本中的操作导致大量的内存浪费,则可以将此大小调整为更接近匹配脚本 需求的值,减少分配的内存量但剩下的内存量仍然为零。在正确的条件下,此类谨慎的配制调整可能造成巨大差别。
如果具有预构建的 PHP Windows 二进制代码,而没有在构建时使用 --enable-memory-limit
选项,则需要先浏览此部分然后再继续。对于 Linux®,配置 PHP 构建时用 --enable-memory-limit
选项构建 PHP。
要使用 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 所示。
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)。
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 数据库操作。在运行脚本时,我们注意到一些与内存使用量相关的奇怪行为并需要将其检查出来。为了使用内存管理函数以使我们可以检验发生错误的位置,我们将使用以下代码。
<?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(); ?> |
注:按照定义的时间间隔检查当前内存使用量。在下面的输出中,通过显示我们的脚本一直在为函数分配内存,并且在应当释放的时候没有释放内存,从而提供对内存泄露的实际测试,您可以看到每次调用时内存使用量如何增长。
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 版包括一些优秀的新工具,可以帮助您更好地洞察脚本的系统内存分配情况,以及重新全面控制内存管理的精确调整。当得到有效使用时,新内存管理工具将支持您的调试工作,从而重新获得一些系统资源。