本篇文章主要探討內(nèi)存泄漏的原因和后果。通過(guò)這篇文章,希望你能收獲更多。下面是探討內(nèi)存泄漏的原因和后果的詳細(xì)內(nèi)容。
創(chuàng)新互聯(lián)建站是專業(yè)的鹽邊網(wǎng)站建設(shè)公司,鹽邊接單;提供網(wǎng)站建設(shè)、成都網(wǎng)站建設(shè),網(wǎng)頁(yè)設(shè)計(jì),網(wǎng)站設(shè)計(jì),建網(wǎng)站,PHP網(wǎng)站建設(shè)等專業(yè)做網(wǎng)站服務(wù);采用PHP框架,可快速的進(jìn)行鹽邊網(wǎng)站開(kāi)發(fā)網(wǎng)頁(yè)制作和功能擴(kuò)展;專業(yè)做搜索引擎喜愛(ài)的網(wǎng)站,專業(yè)的做網(wǎng)站團(tuán)隊(duì),希望更多企業(yè)前來(lái)合作!
內(nèi)部泄漏錯(cuò)誤代碼:
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)
觀察php程序內(nèi)存使用情況
php提提供了兩個(gè)方法來(lái)獲取當(dāng)前程序的內(nèi)存使用情況。
memorygetusage(),這個(gè)函數(shù)的作用是獲取目前PHP腳本所用的內(nèi)存大小。
memorygetpeak_usage(),這個(gè)函數(shù)的作用返回當(dāng)前腳本到目前位置所占用的內(nèi)存峰值,這樣就可能獲取到目前的腳本的內(nèi)存需求情況。
int memory_get_usage ([ bool $real_usage = false ] ) int memory_get_peak_usage ([ bool $real_usage = false ] )
函數(shù)默認(rèn)得到的是調(diào)用emalloc()占用的內(nèi)存,如果設(shè)置參數(shù)為TRUE,則得到的是實(shí)際程序向系統(tǒng)申請(qǐng)的內(nèi)存。因?yàn)?PHP 有自己的內(nèi)存管理機(jī)制,所以有時(shí)候盡管內(nèi)部已經(jīng)釋放了內(nèi)存但并沒(méi)有還給系統(tǒng)。
linux 系統(tǒng)文件 /proc/{$pid}/status 會(huì)記錄某個(gè)進(jìn)程的運(yùn)行狀態(tài),里面的 VmRSS 字段記錄了該進(jìn)程使用的常駐物理內(nèi)存(Residence),這個(gè)就是該進(jìn)程實(shí)際占用的物理內(nèi)存了,用這個(gè)數(shù)據(jù)比較靠譜,在程序里面提取這個(gè)值也很容易 。
場(chǎng)景一:程序操作數(shù)據(jù)過(guò)大
情景還原:一次性讀取超過(guò)php可用內(nèi)存上限的數(shù)據(jù)導(dǎo)致內(nèi)存耗盡
實(shí)例:
這是告訴我們程序運(yùn)行時(shí)試圖分配新內(nèi)存時(shí)由于達(dá)到了PHP允許分配的內(nèi)存上限而拋出致命錯(cuò)誤,無(wú)法繼續(xù)執(zhí)行了,在 java 開(kāi)發(fā)中一般稱之為 OOM ( Out Of Memory ) 。
PHP 配置內(nèi)存上限是在php.ini中設(shè)置memory_limit,PHP 5.2 以前這個(gè)默認(rèn)值是8M,PHP 5.2 的默認(rèn)值是16M,在這之后的版本默認(rèn)值都是128M。
問(wèn)題現(xiàn)象:特定數(shù)據(jù)處理時(shí)可復(fù)現(xiàn),做任何 IO 操作都有可能遇到此類問(wèn)題,比如:一次 MySQL 查詢返回大量數(shù)據(jù)、一次把大文件讀取進(jìn)程序等。解決方法:
1、能用錢解決的問(wèn)題都不是問(wèn)題,如果程序要讀大文件的機(jī)會(huì)不是很多,且上限可預(yù)期,那么通過(guò)ini_set('memory_limit', '1G');來(lái)設(shè)置一個(gè)更大的值或者memory_limit=-1。內(nèi)存管夠的話讓程序一直跑也可以。
2、如果程序需要考慮在小內(nèi)存機(jī)器上也能正常使用,那就需要優(yōu)化程序了。如下,代碼復(fù)雜了很多。
場(chǎng)景二、程序操作大數(shù)據(jù)時(shí)產(chǎn)生拷貝
情景還原:執(zhí)行過(guò)程中對(duì)大變量進(jìn)行了復(fù)制,導(dǎo)致內(nèi)存不夠用。
問(wèn)題現(xiàn)象:局部代碼執(zhí)行過(guò)程中占用內(nèi)存翻倍。
問(wèn)題分析:
php 是寫時(shí)復(fù)制(Copy On Write),也就是說(shuō),當(dāng)新變量被賦值時(shí)內(nèi)存不發(fā)生變化,直到新變量的內(nèi)容被操作時(shí)才會(huì)產(chǎn)生復(fù)制。解決方法:
及早釋放無(wú)用變量,或者以引用的形式操作原始數(shù)據(jù)。
場(chǎng)景三、配置不合理系統(tǒng)資源耗盡
情景還原:因配置不合理導(dǎo)致內(nèi)存不夠用,2G 內(nèi)存機(jī)器上設(shè)置最大可以啟動(dòng) 100 個(gè) php-fpm 子進(jìn)程,但實(shí)際啟動(dòng)了 50 個(gè) php-fpm 子進(jìn)程后無(wú)法再啟動(dòng)更多進(jìn)程 。
問(wèn)題現(xiàn)象:線上業(yè)務(wù)請(qǐng)求量小的時(shí)候不出現(xiàn)問(wèn)題,請(qǐng)求量一旦很大后部分請(qǐng)求就會(huì)執(zhí)行失敗 。
問(wèn)題分析:一般為了安全方面考慮, php 限制表單請(qǐng)求的最大可提交的數(shù)量及大小等參數(shù),post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level。 假設(shè)帶寬足夠,用戶頻繁的提交post_max_size = 8M數(shù)據(jù)到服務(wù)端,nginx 轉(zhuǎn)發(fā)給 php-fpm 處理,那么每個(gè) php-fpm 子進(jìn)程除了自身占用的內(nèi)存外,即使什么都不做也有可能多占用 8M 內(nèi)存。
解決方法:合理設(shè)置post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level等參數(shù)并調(diào)優(yōu) php-fpm 相關(guān)參數(shù)。
php.ini代碼:
$ php -i |grep memory memory_limit => 1024M => 1024M //php腳本執(zhí)行最大可使用內(nèi)存 $php -i |grep max max_execution_time => 0 => 0 //最大執(zhí)行時(shí)間,腳本默認(rèn)為0不限制,web請(qǐng)求默認(rèn)30s max_file_uploads => 20 => 20 //一個(gè)表單里最大上傳文件數(shù)量 max_input_nesting_level => 64 => 64 //一個(gè)表單里數(shù)據(jù)最大數(shù)組深度層數(shù) max_input_time => -1 => -1 //php從接收請(qǐng)求開(kāi)始處理數(shù)據(jù)后的超時(shí)時(shí)間 max_input_vars => 1000 => 1000 //一個(gè)表單(包括get、post、cookie的所有數(shù)據(jù))最多提交1000個(gè)字段 post_max_size => 8M => 8M //一次post請(qǐng)求最多提交8M數(shù)據(jù) upload_max_filesize => 2M => 2M //一個(gè)可上傳的文件最大不超過(guò)2M如果上傳設(shè)置不合理那么出現(xiàn)大量?jī)?nèi)存被占用的情況也不奇怪,比如有些內(nèi)網(wǎng)場(chǎng)景下需要 post 超大字符串post_max_size=200M,那么當(dāng)從表單提交了 200M 數(shù)據(jù)到服務(wù)端, php 就會(huì)分配 200M 內(nèi)存給這條數(shù)據(jù),直到請(qǐng)求處理完畢釋放內(nèi)存。
Php-fpm.conf代碼:
pm = dynamic //僅dynamic模式下以下參數(shù)生效 pm.max_children = 10 //最大子進(jìn)程數(shù) pm.start_servers = 3 //啟動(dòng)時(shí)啟動(dòng)子進(jìn)程數(shù) pm.min_spare_servers = 2 //最小空閑進(jìn)程數(shù),不夠了啟動(dòng)更多進(jìn)程 pm.max_spare_servers = 5 //最大空閑進(jìn)程數(shù),超過(guò)了結(jié)束一些進(jìn)程 pm.max_requests = 500 //最大請(qǐng)求數(shù),注意這個(gè)參數(shù)是一個(gè)php-fpm如果處理了500個(gè)請(qǐng)求后會(huì)自己重啟一下, 可以避免一些三方擴(kuò)展的內(nèi)存泄露問(wèn)題一個(gè) php-fpm 進(jìn)程按 30MB 內(nèi)存算,50 個(gè) php-fpm 進(jìn)程就需要 1500MB 內(nèi)存,這里需要簡(jiǎn)單估算一下在負(fù)載最重的情況下所有 php-fpm 進(jìn)程都啟動(dòng)后是否會(huì)把系統(tǒng)內(nèi)存耗盡。
Ulimit代碼:
$ulimit -a -t: cpu time (seconds) unlimited -f: file size (blocks) unlimited -d: data seg size (kbytes) unlimited -s: stack size (kbytes) 8192 -c: core file size (blocks) 0 -v: address space (kbytes) unlimited -l: locked-in-memory size (kbytes) unlimited -u: processes 1024 -n: file descriptors 1024這是我本地mac os的配置,文件描述符的設(shè)置是比較小的,一般生產(chǎn)環(huán)境配置要大得多。
場(chǎng)景四、無(wú)用的數(shù)據(jù)未及時(shí)釋放
情景還原:這種問(wèn)題從程序邏輯上不是問(wèn)題,但是無(wú)用的數(shù)據(jù)大量占用內(nèi)存導(dǎo)致資源不夠用,應(yīng)該有針對(duì)性的做代碼優(yōu)化。
Laravel開(kāi)發(fā)中用于監(jiān)聽(tīng)數(shù)據(jù)庫(kù)操作時(shí)有如下代碼:
代碼:
DB::listen(function ($query) { // $query->sql // $query->bindings // $query->time });啟用數(shù)據(jù)庫(kù)監(jiān)聽(tīng)后,每當(dāng)有 SQL 執(zhí)行時(shí)會(huì) new 一個(gè) QueryExecuted 對(duì)象并傳入匿名函數(shù)以便后續(xù)操作,對(duì)于執(zhí)行完畢就結(jié)束進(jìn)程釋放資源的php程序來(lái)說(shuō)沒(méi)有什么問(wèn)題,而如果是一個(gè)常駐進(jìn)程的程序,程序每執(zhí)行一條 SQL 內(nèi)存中就會(huì)增加一個(gè) QueryExecuted 對(duì)象,程序不結(jié)束內(nèi)存就會(huì)始終增長(zhǎng)。
問(wèn)題現(xiàn)象:程序運(yùn)行期間內(nèi)存逐漸增長(zhǎng),程序結(jié)束后內(nèi)存正常釋放。
問(wèn)題分析:此類問(wèn)題不易察覺(jué),定位困難,尤其是有些框架封裝好的方法,要明確其適用場(chǎng)景。
解決方法:本例中要通過(guò)DB::listen方法獲取所有執(zhí)行的 SQL 語(yǔ)句記錄并寫入日志,但此方法存在內(nèi)存泄露問(wèn)題,在開(kāi)發(fā)環(huán)境下無(wú)所謂,在生產(chǎn)環(huán)境下則應(yīng)停用,改用其他途徑獲取執(zhí)行的 SQL 語(yǔ)句并寫日志。
深入了解
1、名詞解釋
內(nèi)存泄漏(Memory Leak):是程序在管理內(nèi)存分配過(guò)程中未能正確的釋放不再使用的內(nèi)存導(dǎo)致資源被大量占用的一種問(wèn)題。在面向?qū)ο缶幊虝r(shí),造成內(nèi)存泄露的原因常常是對(duì)象在內(nèi)存中存儲(chǔ)但是運(yùn)行中的代碼卻無(wú)法訪問(wèn)他。由于產(chǎn)生類似問(wèn)題的情況很多,所以只能從源碼上入手分析定位并解決。
垃圾回收(Garbage Collection,簡(jiǎn)稱GC):是一種自動(dòng)內(nèi)存管理的形式,GC程序檢查并處理程序中那些已經(jīng)分配出去但卻不再被對(duì)象使用的內(nèi)存。最早的GC是1959年前后John McCarthy發(fā)明的,用來(lái)簡(jiǎn)化在Lisp中手動(dòng)控制內(nèi)存管理。 PHP的內(nèi)核中已自帶內(nèi)存管理的功能,一般應(yīng)用場(chǎng)景下,不易出現(xiàn)內(nèi)存泄露。
追蹤法(Tracing):從某個(gè)根對(duì)象開(kāi)始追蹤,檢查哪些對(duì)象可訪問(wèn),那么其他的(不可訪問(wèn))就是垃圾。
引用計(jì)數(shù)法(reference count):每個(gè)對(duì)象都一個(gè)數(shù)字用來(lái)標(biāo)示被引用的次數(shù)。引用次數(shù)為0的可以回收。當(dāng)對(duì)一個(gè)對(duì)象的引用創(chuàng)建時(shí)他的引用計(jì)數(shù)就會(huì)增加,引用銷毀時(shí)計(jì)數(shù)減少。引用計(jì)數(shù)法可以保證對(duì)象一旦不被引用時(shí)第一時(shí)間銷毀。但是引用計(jì)數(shù)有一些缺陷:1.循環(huán)引用,2.引用計(jì)數(shù)需要申請(qǐng)更多內(nèi)存,3.對(duì)速度有影響,4.需要保證原子性,5.不是實(shí)時(shí)的。
2、php內(nèi)存管理
在 PHP 5.3 以后引入了同步周期回收算法(Concurrent Cycle Collection)來(lái)處理內(nèi)存泄露問(wèn)題,代價(jià)是對(duì)性能有一定影響,不過(guò)一般 web 腳本應(yīng)用程序影響很小。PHP的垃圾回收機(jī)制是默認(rèn)打開(kāi)的,php.ini 可以設(shè)置zend.enable_gc=0來(lái)關(guān)閉。也能通過(guò)分別調(diào)用gcenable() 和 gcdisable()函數(shù)來(lái)打開(kāi)和關(guān)閉垃圾回收機(jī)制。
雖然垃圾回收讓php開(kāi)發(fā)者在內(nèi)存管理上無(wú)需擔(dān)心了,但也有極端的反例:php界著名的包管理工具composer曾因加入一行g(shù)c_disable();性能得到極大提升。3、php-fpm內(nèi)存泄漏問(wèn)題
在一臺(tái)常見(jiàn)的 nginx + php-fpm 的服務(wù)器上:
nginx 服務(wù)器 fork 出 n 個(gè)子進(jìn)程(worker), php-fpm 管理器 fork 出 n 個(gè)子進(jìn)程。當(dāng)有用戶請(qǐng)求, nginx 的一個(gè) worker 接收請(qǐng)求,并將請(qǐng)求拋到 socket 中。
php-fpm 空閑的子進(jìn)程監(jiān)聽(tīng)到 socket 中有請(qǐng)求,接收并處理請(qǐng)求。
一個(gè) php-fpm 的生命周期大致是這樣的:
模塊初始化(MINIT)-> 請(qǐng)求初始化(RINIT)-> 請(qǐng)求處理 -> 請(qǐng)求結(jié)束(RSHUTDOWN) -> 請(qǐng)求初始化(RINIT)-> 請(qǐng)求處理 -> 請(qǐng)求結(jié)束(RSHUTDOWN)……. 請(qǐng)求初始化(RINIT)-> 請(qǐng)求處理 -> 請(qǐng)求結(jié)束(RSHUTDOWN)-> 模塊關(guān)閉(MSHUTDOWN)。
在請(qǐng)求初始化(RINIT)-> 請(qǐng)求處理 -> 請(qǐng)求結(jié)束(RSHUTDOWN)這個(gè)“請(qǐng)求處理”過(guò)程是: php 讀取相應(yīng)的 php 文件,對(duì)其進(jìn)行詞法分析,生成 opcode , zend 虛擬機(jī)執(zhí)行 opcode 。
php 在每次請(qǐng)求結(jié)束后自動(dòng)釋放內(nèi)存,有效避免了常見(jiàn)場(chǎng)景下內(nèi)存泄露的問(wèn)題,然而實(shí)際環(huán)境中因某些擴(kuò)展的內(nèi)存管理沒(méi)有做好或者 php 代碼中出現(xiàn)循環(huán)引用導(dǎo)致未能正常釋放不用的資源。
在 php-fpm 配置文件中,將pm.max_requests這個(gè)參數(shù)設(shè)置小一點(diǎn)。這個(gè)參數(shù)的含義是:一個(gè) php-fpm 子進(jìn)程最多處理pm.max_requests個(gè)用戶請(qǐng)求后,就會(huì)被銷毀。當(dāng)一個(gè) php-fpm 進(jìn)程被銷毀后,它所占用的所有內(nèi)存都會(huì)被回收。4、常駐進(jìn)程內(nèi)存泄漏問(wèn)題
Valgrind 包括如下一些工具:
Memcheck。這是 valgrind 應(yīng)用最廣泛的工具,一個(gè)重量級(jí)的內(nèi)存檢查器,能夠發(fā)現(xiàn)開(kāi)發(fā)中絕大多數(shù)內(nèi)存錯(cuò)誤使用情況,比如:使用未初始化的內(nèi)存,使用已經(jīng)釋放了的內(nèi)存,內(nèi)存訪問(wèn)越界等。Callgrind。它主要用來(lái)檢查程序中函數(shù)調(diào)用過(guò)程中出現(xiàn)的問(wèn)題。
Cachegrind。它主要用來(lái)檢查程序中緩存使用出現(xiàn)的問(wèn)題。
Helgrind。它主要用來(lái)檢查多線程程序中出現(xiàn)的競(jìng)爭(zhēng)問(wèn)題。
Massif。它主要用來(lái)檢查程序中堆棧使用中出現(xiàn)的問(wèn)題。
Extension??梢岳胏ore提供的功能,自己編寫特定的內(nèi)存調(diào)試工具。
Memcheck 對(duì)調(diào)試 C/C++ 程序的內(nèi)存泄露很有幫助,它的機(jī)制是在系統(tǒng) alloc/free 等函數(shù)調(diào)用上加計(jì)數(shù)。 php 程序的內(nèi)存泄露,是由于一些循環(huán)引用,或者 gc 的邏輯錯(cuò)誤, valgrind 無(wú)法探測(cè),因此需要在檢測(cè)時(shí)需要關(guān)閉 php 自帶的內(nèi)存管理。
代碼:
$ export USE_ZEND_ALLOC=0 # 設(shè)置環(huán)境變量關(guān)閉內(nèi)存管理 valgrind --tool=memcheck --num-callers=30 --log-file=php.log /Users/zouyi/Downloads/php-5.6.31/sapi/cli/php leak.php引用:
definitely lost
: 肯定內(nèi)存泄露indirectly lost
: 非直接內(nèi)存泄露possibly lost
: 可能發(fā)生內(nèi)存泄露still reachable
: 仍然可訪問(wèn)的內(nèi)存suppressed
: 外部造成的內(nèi)存泄露Callgrind 配合 php 擴(kuò)展 xdebug 輸出的 profile 分析日志文件可以分析程序運(yùn)行期間各個(gè)函數(shù)調(diào)用時(shí)占用的內(nèi)存、 CPU 占用情況。
總結(jié):遇到了內(nèi)存泄露時(shí)先觀察是程序本身內(nèi)存不足還是外部資源導(dǎo)致,然后搞清楚程序運(yùn)行中用到了哪些資源:寫入磁盤日志、連接數(shù)據(jù)庫(kù) SQL 查詢、發(fā)送 Curl 請(qǐng)求、 Socket 通信等, I/O 操作必然會(huì)用到內(nèi)存,如果這些地方都沒(méi)有發(fā)生明顯的內(nèi)存泄露,檢查哪里處理大量數(shù)據(jù)沒(méi)有及時(shí)釋放資源,如果是 php 5.3 以下版本還需考慮循環(huán)引用的問(wèn)題。多了解一些 Linux 下的分析輔助工具,解決問(wèn)題時(shí)可以事半功倍。
最后宣傳一下穿云團(tuán)隊(duì)今年最新開(kāi)源的應(yīng)用透明鏈路追蹤工具 Molten:https://github.com/chuan-yun/Molten。安裝好php擴(kuò)展后就能幫你實(shí)時(shí)收集程序的 curl,pdo,mysqli,redis,MongoDB,memcached 等請(qǐng)求的數(shù)據(jù),可以很方便的與 zipkin 集成。關(guān)于內(nèi)存泄漏的原因和后果就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的參考價(jià)值,可以學(xué)以致用。如果喜歡本篇文章,不妨把它分享出去讓更多的人看到。
當(dāng)前名稱:深入分析內(nèi)存泄漏的原因和后果
轉(zhuǎn)載注明:http://weahome.cn/article/iphdhi.html