PHP V5.2:開始
2006
年 11 月發(fā)布了 PHP V5.2,,它包括許多新增功能和錯(cuò)誤修正,。它廢止了 5.1 版并被推薦給所有 PHP V5
用戶進(jìn)行升級(jí),。我最喜歡的實(shí)驗(yàn)室環(huán)境 —— Windows®,、Apache,、MySQL、PHP (WAMP) —— 已經(jīng)被引入了 V5.2
的新軟件包中(請(qǐng)參閱 參考資料),。您將在那里找到在 Windows® XP 或 2003 計(jì)算機(jī)上安裝 PHP V5.2,、MySQL 和 Apache 的應(yīng)用程序。您可以十分輕松地進(jìn)行安裝,,它有很多不錯(cuò)的小的管理優(yōu)點(diǎn),,并且我十分誠懇地推薦使用它。
雖然對(duì)于 Windows 用戶來說,,這是最簡(jiǎn)單的軟件包,,但是在 Linux 上配置 PHP 時(shí)您需要添加以下代碼:--memory-limit-enabled (適用于您服務(wù)器的任何其他選項(xiàng)除外)。不過,,在 Windows 下,,提供了一個(gè)解決此問題的函數(shù)。
PHP V5.2 中有很多改進(jìn)之處,,并且一個(gè)至關(guān)重要的領(lǐng)域是內(nèi)存管理,。從 README.ZEND_MM 中準(zhǔn)確地引述就是:“新內(nèi)存管理器(PHP5.2 以及更高版本)的目標(biāo)是減少內(nèi)存分配開銷并加速內(nèi)存管理。”
下面是 V5.2 發(fā)行說明中的一些關(guān)鍵內(nèi)容:
- 刪除了不必要的
--disable-zend-memory-manager 配置選項(xiàng)
- 添加了
--enable-malloc-mm 配置選項(xiàng),,調(diào)試構(gòu)建時(shí)此配置選項(xiàng)將被默認(rèn)啟用以允許使用內(nèi)部和外部?jī)?nèi)存調(diào)試程序
- 允許使用
ZEND_MM_MEM_TYPE 和 ZEND_MM_SEG_SIZE 環(huán)境變量調(diào)整內(nèi)存管理器
為了理解這些新增功能的含義,,我們需要深入研究?jī)?nèi)存管理中的藝術(shù),并考慮為什么分配開銷和運(yùn)行速度是大問題,。
為什么進(jìn)行內(nèi)存管理,?
計(jì)
算中開發(fā)最快速的一項(xiàng)技術(shù)是內(nèi)存和數(shù)據(jù)存儲(chǔ),它們是受不斷增加速度和存儲(chǔ)大小這樣持續(xù)的需求而驅(qū)動(dòng)的,。早期的計(jì)算機(jī)使用卡作為內(nèi)存,,然后轉(zhuǎn)向了芯片技術(shù),。
您能想象在只有 1 KB RAM
內(nèi)存的計(jì)算機(jī)工作的情景嗎?很多早期的計(jì)算機(jī)程序員就曾使用過,。這些先驅(qū)者很快就意識(shí)到,,要在技術(shù)限制下工作,他們將必須細(xì)心地用瑣碎的命令避免系統(tǒng)過
載,。
身為 PHP 開發(fā)人員,,與使用 C++
或其他更嚴(yán)格的語言編碼的同事相比,我們所在的環(huán)境更方便進(jìn)行編碼,。在我們的世界里,,我們自己不必?fù)?dān)心如何處理系統(tǒng)內(nèi)存,因?yàn)?PHP
將為我們處理這個(gè)問題,。但是,,在其他編程領(lǐng)域里,負(fù)責(zé)任的編碼人員將使用各種函數(shù)確保執(zhí)行的命令不會(huì)覆蓋其他一些程序數(shù)據(jù) ——
因而,,破壞了程序的運(yùn)行,。
內(nèi)存管理通常是由來自編碼人員的請(qǐng)求處理的,以分配和釋放內(nèi)存塊,。分配塊 可以保存任何類型的數(shù)據(jù),,并且此過程將為該數(shù)據(jù)隔開一定量的內(nèi)存,,并當(dāng)操作需要訪問數(shù)據(jù)時(shí)為應(yīng)用程序提供訪問方法,。人們期望程序在完成任何操作后釋放分配的內(nèi)存,并允許系統(tǒng)和其他程序員使用該內(nèi)存,。如果程序沒有把內(nèi)存釋放回系統(tǒng),,則稱為內(nèi)存泄露。
泄露是任何運(yùn)行程序都存在的普遍問題,,并且某種程度內(nèi)通常是可以接受的,,尤其是當(dāng)我們知道運(yùn)行程序?qū)⒘⒓唇K止并釋放默認(rèn)分配給程序的所有內(nèi)存。
由
于隨機(jī)運(yùn)行和終止程序,,像幾乎所有客戶機(jī)應(yīng)用程序一樣,,這是個(gè)問題。期望服務(wù)器應(yīng)用程序不確定地運(yùn)行而不終止或重新啟動(dòng),,這使得內(nèi)存管理對(duì)于服務(wù)器守護(hù)程
序編程絕對(duì)的至關(guān)重要,。在長(zhǎng)時(shí)間運(yùn)行的程序中,即使一個(gè)小的泄露最后都將發(fā)展為系統(tǒng)衰弱問題,,因?yàn)閮?nèi)存塊已被使用并且永遠(yuǎn)不被釋放,。
長(zhǎng)期考慮
正如使用任何語言編寫一樣,用 PHP 編寫的永久性服務(wù)器守護(hù)程序有很多可能的用途,。但是當(dāng)我們出于這些目的開始使用 PHP 時(shí),,我們也必須考慮內(nèi)存使用情況,。
解
析大量數(shù)據(jù)或可能隱藏?zé)o限次循環(huán)的腳本都趨于消耗大量?jī)?nèi)存。很明顯,,一旦內(nèi)存被耗盡,,服務(wù)器的性能就降低,因此在執(zhí)行腳本時(shí)我們還必須注意內(nèi)存的使用情
況,。雖然我們可以通過啟用系統(tǒng)監(jiān)視器來簡(jiǎn)單觀察內(nèi)存的使用量,,但是它不會(huì)告訴我們比整個(gè)系統(tǒng)內(nèi)存狀態(tài)更有用的任何內(nèi)容。有時(shí)我們不止需要幫助進(jìn)行故障檢修
或優(yōu)化的內(nèi)容,,而有時(shí)我們只是需要更多詳細(xì)信息,。
獲得腳本執(zhí)行內(nèi)容的透明性的一種方法是使用內(nèi)部或外部調(diào)試器。內(nèi)部調(diào)試器 是呈現(xiàn)為執(zhí)行腳本的相同的進(jìn)程,。從操作系統(tǒng)的角度考慮的獨(dú)立進(jìn)程是外部調(diào)試器,。使用調(diào)試器進(jìn)行內(nèi)存分析類似于任何一種情況,但是使用了不同的方法訪問內(nèi)存,。內(nèi)部調(diào)試器對(duì)運(yùn)行進(jìn)程所在的內(nèi)存空間具有直接訪問權(quán),,而外部調(diào)試器將通過套接字訪問內(nèi)存。
有許多方法和可用的調(diào)試服務(wù)器(外部)和庫(內(nèi)部)可用于輔助開發(fā),。為了準(zhǔn)備好對(duì) PHP 安裝進(jìn)行調(diào)試,,可以使用新提供的 --enable-malloc-mm ,它在 DEBUG 構(gòu)建中默認(rèn)被啟用,。這使環(huán)境變量 USE_ZEND_ALLOC 可用于允許在運(yùn)行時(shí)選擇 malloc 或 emalloc 內(nèi)存分配,。使用 malloc-type 內(nèi)存分配將允許外部調(diào)試器觀察內(nèi)存使用情況,而 emalloc 分配將使用 Zend 內(nèi)存管理器抽象,,要求進(jìn)行內(nèi)部調(diào)試,。
PHP 中的內(nèi)存管理函數(shù)
除了使內(nèi)存管理器更靈活更透明之外,PHP V5.2 還為 memory_get_usage() 和 memory_get_peak_usage() 提供了一個(gè)新參數(shù),,這兩個(gè)函數(shù)允許查看內(nèi)存使用量,。說明中提及的新布爾值是 real_size 。通過調(diào)用函數(shù) memory_get_usage($real); (其中 $real = true ),,結(jié)果將為調(diào)用時(shí)系統(tǒng)中實(shí)際分配的內(nèi)存大小,,包括內(nèi)存管理器開銷。如果不使用標(biāo)記組,,則返回的數(shù)據(jù)將只包括在運(yùn)行腳本內(nèi)使用的內(nèi)存,,減去內(nèi)存管理器開銷。
memory_get_usage() 和 memory_get_peak_usage() 的不同之處在于后者將返回到目前為止調(diào)用它的運(yùn)行進(jìn)程的最高內(nèi)存量,,而前者只返回執(zhí)行時(shí)的使用量,。
對(duì)于 memory_get_usage() , 提供了清單 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
?>
|
在這個(gè)簡(jiǎn)單示例中,,我們首先回轉(zhuǎn)了直接調(diào)用 memory_get_usage() 的結(jié)果,,代碼注釋中顯示可能在作者的系統(tǒng)中有 36640 字節(jié)的常見結(jié)果,。然后我們使用 4,242 個(gè) “Hello” 副本來裝載 $a 并再次運(yùn)行函數(shù),。圖 1 中可以看到此簡(jiǎn)單應(yīng)用的輸出。
圖 1. memory_get_usage() 的示例輸出
沒有 memory_get_peak_usage() 的示例,,因?yàn)閮烧呤窒嗨?,語法是相同的,。但是,對(duì)于清單 1 中的示例代碼,,將只有一個(gè)結(jié)果,,即當(dāng)時(shí)的最高內(nèi)存使用量。讓我們看一看清單 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() 已經(jīng)替換為 memory_get_peak_usage() 。在我們用 4242 個(gè) “Hello” 副本填充 $a 之前,,輸出都不會(huì)有多大更改,。內(nèi)存跳升至 57960,表示到目前為止的峰值,。當(dāng)檢查內(nèi)存使用量峰值時(shí),,得到了目前為止的最高值,因此所有進(jìn)一步調(diào)用都將得到 57960,,直至我們處理的操作比處理 $a 使用的內(nèi)存更多(參見圖 2),。
圖 2. memory_get_peak_usage() 的示例輸出
限制內(nèi)存使用
確
保托管應(yīng)用程序的服務(wù)器不過載的一種方法是限制 PHP 執(zhí)行的任何腳本使用的內(nèi)存量。這根本不是我們應(yīng)當(dāng)執(zhí)行的操作,,但由于 PHP
是一種松散類型的語言,,并且是在運(yùn)行時(shí)解析的,,因此我們有時(shí)會(huì)獲得在釋放到生產(chǎn)應(yīng)用程序中后編寫得很差的腳本,。這些腳本可能執(zhí)行循環(huán),也可能打開一張長(zhǎng)的
文件列表,,忘記在打開新文件之前先關(guān)閉當(dāng)前文件,。無論在哪一種情況下,編寫很差的腳本可能在您知道之前以消耗大量?jī)?nèi)存告終,。
在 PHP.INI 中,,您可以使用配制參數(shù) memory_limit 來指定任何腳本能夠在系統(tǒng)中運(yùn)行的最大內(nèi)存使用量。這不是對(duì)于 V5.2 的特定更改,,但是內(nèi)存管理器及其使用的任何討論都值得至少快速查看一次這個(gè)特性,。它還精心地引導(dǎo)我使用內(nèi)存管理器的最后幾個(gè)新功能:環(huán)境變量。
調(diào)整內(nèi)存管理器
最后,在不能做完美主義者但是又完全符合自己目的的情況下怎樣編程,?新環(huán)境變量 ZEND_MM_MEM_TYPE 和 ZEND_MM_SEG_SIZE 正好可以滿足您的需求,。
當(dāng)內(nèi)存管理器分配大型內(nèi)存塊時(shí),它是安裝 ZEND_MM_SEG_SIZE
變量中列出的預(yù)定大小執(zhí)行操作的,。這些內(nèi)存塊的默認(rèn)分區(qū)大小為每塊 256
KB,,但是您可以調(diào)整這些分區(qū)大小以滿足特殊需求。例如,,如果您注意到最常用的一個(gè)腳本中的操作導(dǎo)致大量的內(nèi)存浪費(fèi),,則可以將此大小調(diào)整為更接近匹配腳本
需求的值,減少分配的內(nèi)存量但剩下的內(nèi)存量仍然為零,。在正確的條件下,,此類謹(jǐn)慎的配制調(diào)整可能造成巨大差別。
在 Windows 中檢索內(nèi)存使用情況
如果具有預(yù)構(gòu)建的 PHP Windows 二進(jìn)制代碼,,而沒有在構(gòu)建時(shí)使用 --enable-memory-limit 選項(xiàng),,則需要先瀏覽此部分然后再繼續(xù)。對(duì)于 Linux®,,配置 PHP 構(gòu)建時(shí)用 --enable-memory-limit 選項(xiàng)構(gòu)建 PHP,。
要使用 Windows 二進(jìn)制代碼檢索內(nèi)存使用情況,請(qǐng)創(chuàng)建以下函數(shù),。
清單 3. 在 Windows 下獲得內(nèi)存使用情況
<?php
function memory_get_usage(){ $output = array(); exec(‘tasklist /FI "PID eq ‘.getmypid().‘" /FO LIST‘, $output ); return preg_replace( ‘/[^0-9]/‘, ‘‘, $output[5] ) * 1024; }
?>
|
將結(jié)果保存到名為 function.php 的文件?,F(xiàn)在您只能將此文件包含在需要使用它的腳本中。
動(dòng)手實(shí)踐
讓
我們來看一看使用這些設(shè)置的實(shí)際示例給我們帶來的好處,??赡苡泻芏啻文枷胫罏槭裁丛谀_本的末尾沒有正確分配內(nèi)存。原因是因?yàn)橐恍┖瘮?shù)本身導(dǎo)致了內(nèi)存泄
露,,尤其是在僅使用內(nèi)置 PHP 函數(shù)的情況下,。在這里,您將了解如何發(fā)現(xiàn)此類問題,。并且為了開始進(jìn)行內(nèi)存泄露查找的征戰(zhàn),,您將創(chuàng)建一個(gè)測(cè)試
MySQL 數(shù)據(jù)庫,如清單 4 所示,。
清單 4. 創(chuàng)建測(cè)試數(shù)據(jù)庫
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");
|
這將創(chuàng)建一個(gè)帶有 ID 字段和數(shù)據(jù)字段的簡(jiǎn)單表,。
在下一張清單中,想象我們堅(jiān)韌不拔的程序員正在執(zhí)行一些 MySQL 函數(shù),,特別是使用 mysql_query() 將結(jié)果應(yīng)用到變量,。當(dāng)他這樣做時(shí),他將注意到即使調(diào)用 mysql_free_result() ,,一些內(nèi)存也不會(huì)被釋放,,導(dǎo)致內(nèi)存使用量隨著 Apache 進(jìn)程不斷增長(zhǎng)(參見清單 5)。
清單 5. 內(nèi)存泄露檢測(cè)示例
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 是在任何位置都可能使用的簡(jiǎn)單 MySQL 數(shù)據(jù)庫操作。在運(yùn)行腳本時(shí),,我們注意到一些與內(nèi)存使用量相關(guān)的奇怪行為并需要將其檢查出來,。為了使用內(nèi)存管理函數(shù)以使我們可以檢驗(yàn)發(fā)生錯(cuò)誤的位置,我們將使用以下代碼,。
清單 6. 定標(biāo)查找錯(cuò)誤的示例
<?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();
?>
|
注:按照定義的時(shí)間間隔檢查當(dāng)前內(nèi)存使用量,。在下面的輸出中,通過顯示我們的腳本一直在為函數(shù)分配內(nèi)存,,并且在應(yīng)當(dāng)釋放的時(shí)候沒有釋放內(nèi)存,,從而提供對(duì)內(nèi)存泄露的實(shí)際測(cè)試,您可以看到每次調(diào)用時(shí)內(nèi)存使用量如何增長(zhǎng),。
清單 7. 測(cè)試腳本輸出
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
|
我們所做的操作是發(fā)現(xiàn)了執(zhí)行腳本時(shí)出現(xiàn)的一些可疑操作,,然后調(diào)整腳本使其給我們提供一些可理解的反饋。我們?cè)俅芜\(yùn)行了腳本,,在每次迭代期間使用 memory_get_usage() 查看內(nèi)存使用量的變化,。根據(jù)分配的內(nèi)存值的增長(zhǎng)情況,暗示了我們用腳本在某個(gè)位置建立了一個(gè)漏洞,。由于 mysql_free_result() 函數(shù)不釋放內(nèi)存,,因此我們可以認(rèn)為 mysql_query() 并未正確分配內(nèi)存。
結(jié)束語
PHP V5.2 版包括一些優(yōu)秀的新工具,,可以幫助您更好地洞察腳本的系統(tǒng)內(nèi)存分配情況,,以及重新全面控制內(nèi)存管理的精確調(diào)整。當(dāng)?shù)玫接行褂脮r(shí),,新內(nèi)存管理工具將支持您的調(diào)試工作,,從而重新獲得一些系統(tǒng)資源。
參考資料
學(xué)習(xí)
- 您可以參閱本文在 developerWorks 全球站點(diǎn)上的 英文原文 ,。
- 閱讀 PHP V5.2 發(fā)行說明,。
- “How to Manage Memory in PHP” 是一篇關(guān)于 PHP 內(nèi)存管理編程實(shí)踐的優(yōu)秀文章。
-
Zend Developer Zone 中有很多針對(duì)內(nèi)存管理器函數(shù)的文檔,。
- 訪問 PHP.net 獲得 PHP 文檔,。
- 文章 “A step-by-step how-to guide to install, configure, and test a Linux, Apache, Informix, and PHP server” 包含一個(gè)關(guān)于為 Linux 編譯 PHP 解析程序的部分。
- 查閱實(shí)際示例產(chǎn)生的 錯(cuò)誤報(bào)告,。
- 在 “PHP V5 遷移指南” 中了解如何將在 PHP V4 中開發(fā)的代碼遷移到 V5,。
- 要獲得學(xué)習(xí)用 PHP 進(jìn)行編程的教程,,請(qǐng)查閱 developerWorks 的 “學(xué)習(xí) PHP” 系列,。
-
Planet PHP 是 PHP 開發(fā)者社區(qū)新聞資源。
-
PHP.net 是 PHP 開發(fā)者的資源,。
- 查閱 “PHP 推薦讀物列表”,。
- 瀏覽 developerWorks 上的所有 PHP 文章 和 PHP 教程。
- 查閱 IBM developerWorks 的 PHP 項(xiàng)目資源中心 擴(kuò)展 PHP 技巧。
- 收聽針對(duì)軟件開發(fā)人員的有趣訪談和討論,,一定要訪問 developerWorks podcast,。
- 隨時(shí)關(guān)注 developerWorks 的 技術(shù)事件和網(wǎng)絡(luò)廣播。
- 查閱最近將在全球舉辦的面向 IBM 開放源碼開發(fā)人員的研討會(huì),、交易展覽,、網(wǎng)絡(luò)廣播和其他 活動(dòng)。
- 訪問 developerWorks 開源軟件技術(shù)專區(qū),,獲得豐富的 how-to 信息,、工具和項(xiàng)目更新,幫助您用開放源碼技術(shù)進(jìn)行開發(fā),,并與 IBM 產(chǎn)品結(jié)合使用,。
- 訪問 Safari 在線書店 瀏覽開放源碼技術(shù)的各種參考資料。
|