本篇文章是对PHP中的内存管理
PHP动态分配和释放内存进行了详细的分析介绍
需要的朋友参考下
摘要 内存管理对于长期运行的程序例如服务器守护程序是相当重要的影响因此理解PHP是如何分配与释放内存的对于创建这类程序极为重要本文将重点探讨PHP的内存管理问题
一 内存
在PHP中填充一个字符串变量相当简单这只需要一个语句"<?php $str = hello world ; ?>"即可并且该字符串能够被自由地修改拷贝和移动而在C语言中尽管你能够编写例如"char *str = "hello world ";"这样的一个简单的静态字符串但是却不能修改该字符串因为它生存于程序空间内为了创建一个可操纵的字符串你必须分配一个内存块并且通过一 个函数(例如strdup())来复制其内容
复制代码 代码如下:
{
char *str;
str = strdup("hello world");
if (!str) {
fprintf(stderr
"Unable to allocate memory!");
}
}
由于后面我们将分析的各种原因传统型内存管理函数(例如malloc()free()strdup()realloc()calloc()等等)几乎都不能直接为PHP源代码所使用
二 释放内存
在几乎所有的平台上内存管理都是通过一种请求和释放模式实现的首先一个应用程序请求它下面的层(通常指"操作系统")"我想使用一些内存空间"如果存在可用的空间操作系统就会把它提供给该程序并且打上一个标记以便不会再把这部分内存分配给其它程序
当 应用程序使用完这部分内存它应该被返回到OS这样以来它就能够被继续分配给其它程序如果该程序不返回这部分内存那么OS无法知道是否这块内存不 再使用并进而再分配给另一个进程如果一个内存块没有释放并且所有者应用程序丢失了它那么我们就说此应用程序"存在漏洞"因为这部分内存无法再为 其它程序可用
在一个典型的客户端应用程序中较小的不太经常的内存洩漏有时能够为OS所"容忍"因为在这个进程稍后结束时该洩漏内存会被隐式返回到OS这并没有什么因为OS知道它把该内存分配给了哪个程序并且它能够确信当该程序终止时不再需要该内存
而对于长时间运行的服务器守护程序包括象Apache这样的web服务器和扩展php模块来说进程往往被设计为相当长时间一直运行因为OS不能清理内存使用所以任何程序的洩漏无论是多么小都将导致重复操作并最终耗尽所有的系统资源
现 在我们不妨考虑用户空间内的stristr()函数为了使用大小写不敏感的搜索来查找一个字符串它实际上创建了两个串的各自的一个小型副本然后执 行一个更传统型的大小写敏感的搜索来查找相对的偏移量然而在定位该字符串的偏移量之后它不再使用这些小写版本的字符串如果它不释放这些副本那 么每一个使用stristr()的脚本在每次调用它时都将洩漏一些内存最后web服务器进程将拥有所有的系统内存但却不能够使用它
你可以理直气壮地说理想的解决方案就是编写良好干净的一致的代码这当然不错但是在一个象PHP解释器这样的环境中这种观点仅对了一半
三 错误处理
为了实现"跳出"对用户空间脚本及其依赖的扩展函数的一个活动请求需要使用一种方法来 完全"跳出"一个活动请求这是在Zend引擎内实现的在一个请求的开始设置一个"跳出"地址然后在任何die()或exit()调用或在遇到任何关 键错误(E_ERROR)时执行一个longjmp()以跳转到该"跳出"地址
尽管这个"跳出"进程能够简化程序执行的流程但是在绝大多数情况下这会意味着将会跳过资源清除代码部分(例如free()调用)并最终导致出现内存漏洞现在让我们来考虑下面这个简化版本的处理函数调用的引擎代码
复制代码 代码如下:
void call_function(const char *fname
int fname_len TSRMLS_DC){
zend_function *fe;
char *lcase_fname;
/* PHP函数名是大小写不敏感的
*为了简化在函数表中对它们的定位
*所有函数名都隐含地翻译为小写的
*/
lcase_fname = estrndup(fname
fname_len);
zend_str_tolower(lcase_fname
fname_len);
if (zend_hash_find(EG(function_table)
lcase_fname
fname_len +
(void **)&fe) == FAILURE) {
zend_execute(fe
>op_array TSRMLS_CC);
} else {
php_error_docref(NULL TSRMLS_CC
E_ERROR
"Call to undefined function: %s()"
fname);
}
efree(lcase_fname);
}
当 执行到php_error_docref()这一行时内部错误处理器就会明白该错误级别是critical并相应地调用longjmp()来中断当前 程序流程并离开call_function()函数甚至根本不会执行到efree(lcase_fname)这一行你可能想把efree()代码行移 动到zend_error()代码行的上面但是调用这个call_function()例程的代码行会怎么样呢?fname本身很可能就是一个分配的 字符串并且在它被错误消息处理使用完之前你根本不能释放它
注意这个php_error_docref()函数是trigger_error()函数的一个内部等价实现它的第一个参数是一个将被添加到docref的可选的文档引用第三个参数可以是任何我们熟悉的E_*家族常量用于指示错误的严重程度第四个参数(最后一个)遵循printf()风格的格式化和变量参数列表式样
四 Zend内存管理器
在 上面的"跳出"请求期间解决内存洩漏的方案之一是使用Zend内存管理(ZendMM)层引擎的这一部分非常类似于操作系统的内存管理行为分配内存 给调用程序区别在于它处于进程空间中非常低的位置而且是"请求感知"的这样以来当一个请求结束时它能够执行与OS在一个进程终止时相同的行为 也就是说它会隐式地释放所有的为该请求所占用的内存图展示了ZendMM与OS以及PHP进程之间的关系
图Zend内存管理器代替系统调用来实现针对每一种请求的内存分配
除 了提供隐式内存清除功能之外ZendMM还能够根据phpini中memory_limit的设置控制每一种内存请求的用法如果一个脚本试图请求比 系统中可用内存更多的内存或大于它每次应该请求的最大量那么ZendMM将自动地发出一个E_ERROR消息并且启动相应的"跳出"进程这种方法 的一个额外优点在于大多数内存分配调用的返回值并不需要检查因为如果失败的话将会导致立即跳转到引擎的退出部分
把PHP内部代码和 OS的实际的内存管理层"钩"在一起的原理并不复杂所有内部分配的内存都要使用一组特定的可选函数实现例如PHP代码不是使用malloc() 来分配一个字节内存块而是使用了emalloc()除了实现实际的内存分配任务外ZendMM还会使用相应的绑定请求类型来标志该内存块这 样以来当一个请求"跳出"时ZendMM可以隐式地释放它
经常情况下内存一般都需要被分配比单个请求持续时间更长的一段时间这 种类型的分配(因其在一次请求结束之后仍然存在而被称为"永久性分配")可以使用传统型内存分配器来实现因为这些分配并不会添加ZendMM使用的那 些额外的相应于每种请求的信息然而有时直到运行时刻才会确定是否一个特定的分配需要永久性分配因此ZendMM导出了一组帮助宏其行为类似于其它 的内存分配函数但是使用最后一个额外参数来指示是否为永久性分配
如果你确实想实现一个永久性分配那么这个参数应该被设置为在这 种情况下请求是通过传统型malloc()分配器家族进行传递的然而如果运行时刻逻辑认为这个块不需要永久性分配那么这个参数可以被设置为零 并且调用将会被调整到针对每种请求的内存分配器函数
例如pemalloc(buffer_len)将映射到malloc(buffer_len)而pemalloc(buffer_len)将被使用下列语句映射到emalloc(buffer_len)
#define in Zend/zend_alloch:
#define pemalloc(size persistent) ((persistent)?malloc(size): emalloc(size))
所有这些在ZendMM中提供的分配器函数都能够从下表中找到其更传统的对应实现
表格展示了ZendMM支持下的每一个分配器函数以及它们的e/pe对应实现
表格传统型相对于PHP特定的分配器
分配器函数 e/pe对应实现 void *malloc(size_t count); void *emalloc(size_t count);void *pemalloc(size_t count
char persistent); void *calloc(size_t count); void *ecalloc(size_t count);void *pecalloc(size_t count
char persistent); void *realloc(void *ptr
size_t count); void *erealloc(void *ptr
size_t count);
void *perealloc(void *ptr
size_t count
char persistent); void *strdup(void *ptr); void *estrdup(void *ptr);void *pestrdup(void *ptr
char persistent); void free(void *ptr); void efree(void *ptr);
void pefree(void *ptr
char persistent);
你可能会注意到即使是pefree()函数也要求使用永久性标志这是因为在调用pefree()时它实际上并不知道是否ptr是一种永久性分 配针对一个非永久性分配调用free()能够导致双倍的空间释放而针对一种永久性分配调用efree()有可能会导致一个段错误因为内存管理器会试 图查找并不存在的管理信息因此你的代码需要记住它分配的数据结构是否是永久性的
除了分配器函数核心部分外还存在其它一些非常方便的ZendMM特定的函数例如
void *estrndup(void *ptrint len);
该函数能够分配len+个字节的内存并且从ptr处复制len个字节到最新分配的块这个estrndup()函数的行为可以大致描述如下
复制代码 代码如下:
void *estrndup(void *ptr
int len)
{
char *dst = emalloc(len +
);
memcpy(dst
ptr
len);
dst[len] =
;
return dst;
}
在 此被隐式放置在缓沖区最后的NULL字节可以确保任何使用estrndup()实现字符串复制操作的函数都不需要担心会把结果缓沖区传递给一个例如 printf()这样的希望以为NULL为结束符的函数当使用estrndup()来复制非字符串数据时最后一个字节实质上都浪费了但其中的利明显 大于弊
void *safe_emalloc(size_t size size_t count size_t addtl);
void *safe_pemalloc(size_t size size_t countsize_t addtlchar persistent);
这 些函数分配的内存空间最终大小是((size*count)+addtl)你可以会问"为什么还要提供额外函数呢?为什么不使用一个 emalloc/pemalloc呢?"原因很简单为了安全尽管有时候可能性相当小但是正是这一"可能性相当小"的结果导致宿主平台的内存溢出 这可能会导致分配负数个数的字节空间或更有甚者会导致分配一个小于调用程序要求大小的字节空间而safe_emalloc()能够避免这种类型的陷 井通过检查整数溢出并且在发生这样的溢出时显式地预以结束
注意并不是所有的内存分配例程都有一个相应的p*对等实现例如不存在pestrndup()并且在PHP 版本前也不存在safe_pemalloc()
五 引用计数
慎重的内存分配与释放对于PHP(它是一种多请求进程)的长期性能有极其重大的影响但是这还仅是问题的一半为了使一个每秒处理上千次点击的服务器高效地运行每一次请求都需要使用尽可能少的内存并且要尽可能减少不必要的数据复制操作请考虑下列PHP代码片断
复制代码 代码如下:
<?php
$a =
Hello World
;
$b = $a;
unset($a);
?>
在第一次调用之后只有一个变量被创建并且一个字节的内存块指派给它以便存储字符串"Hello World"还包括一个结尾处的NULL字符现在让我们来观察后面的两行$b被置为与变量$a相同的值然后变量$a被释放
如 果PHP因每次变量赋值都要复制变量内容的话那么对于上例中要复制的字符串还需要复制额外的个字节并且在数据复制期间还要进行另外的处理器加 载这一行为乍看起来有点荒谬因为当第三行代码出现时原始变量被释放从而使得整个数据复制显得完全不必要其实我们不妨再远一层考虑让我们设想 当一个MB大小的文件的内容被装载到两个变量中时会发生什么这将会占用MB的空间此时已经足够了引擎会把那么多的时间和内存浪费在这 样一种无用的努力上吗?
你应该知道PHP的设计者早已深谙此理
记住在引擎中变量名和它们的值实际上是两个不同的概念值本身是一个无名的zval*存储体(在本例中是一个字符串值)它被通过zend_hash_add()赋给变量$a如果两个变量名都指向同一个值会发生什么呢?
复制代码 代码如下:
{
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval
"Hello World"
);
zend_hash_add(EG(active_symbol_table)
"a"
sizeof("a")
&helloval
sizeof(zval*)
NULL);
zend_hash_add(EG(active_symbol_table)
"b"
sizeof("b")
&helloval
sizeof(zval*)
NULL);
}
此 时你可以实际地观察$a或$b并且会看到它们都包含字符串"Hello World"遗憾的是接下来你继续执行第三行代码"unset($a);"此时unset()并不知道$a变量指向的数据还被另一个变量所使 用因此它只是盲目地释放掉该内存任何随后的对变量$b的存取都将被分析为已经释放的内存空间并因此导致引擎崩溃
这个问题可以借助于 zval(它有好几种形式)的第四个成员refcount加以解决当一个变量被首次创建并赋值时它的refcount被初始化为因为它被假定仅由 最初创建它时相应的变量所使用当你的代码片断开始把helloval赋给$b时它需要把refcount的值增加为这样以来现在该值被两个变量 所引用
复制代码 代码如下:
{
zval *helloval;
MAKE_STD_ZVAL(helloval);
ZVAL_STRING(helloval
"Hello World"
);
zend_hash_add(EG(active_symbol_table)
"a"
sizeof("a")
&helloval
sizeof(zval*)
NULL);
ZVAL_ADDREF(helloval);
zend_hash_add(EG(active_symbol_table)
"b"
sizeof("b")
&helloval
sizeof(zval*)
NULL);
}
现在当unset()删除原变量的$a相应的副本时它就能够从refcount参数中看到还有另外其他人对该数据感兴趣因此它应该只是减少refcount的计数值然后不再管它
六 写复制(Copy on Write)
通过refcounting来节约内存的确是不错的主意但是当你仅想改变其中一个变量的值时情况会如何呢?为此请考虑下面的代码片断
复制代码 代码如下:
<?php
$a =
;
$b = $a;
$b +=
;
?>
通过上面的逻辑流程你当然知道$a的值仍然等于而$b的值最后将是并且此时你还知道Zend在尽力节省内存通过使$a和$b都引用相同的zval(见第二行代码)那么当执行到第三行并且必须改变$b变量的值时会发生什么情况呢?
回答是Zend要查看refcount的值并且确保在它的值大于时对之进行分离在Zend引擎中分离是破坏一个引用对的过程正好与你刚才看到的过程相反
复制代码 代码如下:
zval *get_var_and_separate(char *varname
int varname_len TSRMLS_DC)
{
zval **varval
*varcopy;
if (zend_hash_find(EG(active_symbol_table)
varname
varname_len +
(void**)&varval) == FAILURE) {
/* 变量根本并不存在
失败而导致退出*/
return NULL;
}
if ((*varval)
>refcount <
) {
/* varname是唯一的实际引用
*不需要进行分离
*/
return *varval;
}
/* 否则
再复制一份zval*的值*/
MAKE_STD_ZVAL(varcopy);
varcopy = *varval;
/* 复制任何在zval*内的已分配的结构*/
zval_copy_ctor(varcopy);
/*删除旧版本的varname
*这将减少该过程中varval的refcount的值
*/
zend_hash_del(EG(active_symbol_table)
varname
varname_len +
);
/*初始化新创建的值的引用计数
并把它依附到
* varname变量
*/
varcopy
>refcount =
;
varcopy
>is_ref =
;
zend_hash_add(EG(active_symbol_table)
varname
varname_len +
&varcopy
sizeof(zval*)
NULL);
/*返回新的zval* */
return varcopy;
}
现在既然引擎有一个仅为变量$b所拥有的zval*(引擎能知道这一点)所以它能够把这个值转换成一个long型值并根据脚本的请求给它增加
七 写改变(changeonwrite)
引用计数概念的引入还导致了一个新的数据操作可能性其形式从用户空间脚本管理器看来与"引用"有一定关系请考虑下列的用户空间代码片断
复制代码 代码如下:
<?php
$a =
;
$b = &$a;
$b +=
;
?>
在 上面的PHP代码中你能看出$a的值现在为尽管它一开始为并且从未(直接)发生变化之所以会发生这种情况是因为当引擎开始把$b的值增加时 它注意到$b是一个对$a的引用并且认为"我可以改变该值而不必分离它因为我想使所有的引用变量都能看到这一改变"
但是引擎是如何 知道的呢?很简单它只要查看一下zval结构的第四个和最后一个元素(is_ref)即可这是一个简单的开/关位它定义了该值是否实际上是一个用户 空间风格引用集的一部分在前面的代码片断中当执行第一行时为$a创建的值得到一个refcount为还有一个is_ref值为因为它仅为一 个变量($a)所拥有并且没有其它变量对它产生写引用改变在第二行这个值的refcount元素被增加为除了这次is_ref元素被置为之外 (因为脚本中包含了一个"&"符号以指示是完全引用)
最后在第三行引擎再一次取出与变量$b相关的值并且检查是否有必要进行分离这一次该值没有被分离因为前面没有包括一个检查下面是get_var_and_separate()函数中与refcount检查有关的部分代码
复制代码 代码如下:
if ((*varval)
>is_ref || (*varval)
>refcount <
) {
/* varname是唯一的实际引用
* 或者它是对其它变量的一个完全引用
*任何一种方式
都没有进行分离
*/
return *varval;
}
这一次尽管refcount为却没有实现分离因为这个值是一个完全引用引擎能够自由地修改它而不必关心其它变量值的变化
八 分离问题
尽管已经存在上面讨论到的复制和引用技术但是还存在一些不能通过is_ref和refcount操作来解决的问题请考虑下面这个PHP代码块
复制代码 代码如下:
<?php
$a =
;
$b = $a;
$c = &$a;
?>
在 此你有一个需要与三个不同的变量相关联的值其中两个变量是使用了"changeonwrite"完全引用方式而第三个变量处于一种可分离 的"copyonwrite"(写复制)上下文中如果仅使用is_ref和refcount来描述这种关系有哪些值能够工作呢?
回答是没有一个能工作在这种情况下这个值必须被复制到两个分离的zval*中尽管两者都包含完全相同的数据(见图)
图引用时强制分离
同样下列代码块将引起相同的沖突并且强迫该值分离出一个副本(见图)
图复制时强制分离
复制代码 代码如下:
<?php
$a =
;
$b = &$a;
$c = $a;
?>
注意在这里的两种情况下$b都与原始的zval对象相关联因为在分离发生时引擎无法知道介于到该操作当中的第三个变量的名字 九 总结
PHP是一种托管语言从普通用户角度来看这种仔细地控制资源和内存的方式意味着更为容易地进行原型开发并导致出现更少的沖突然而当我们深入"内里"之后一切的承诺似乎都不复存在最终还要依赖于真正有责任心的开发者来维持整个运行时刻环境的一致性