日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > php >内容正文

php

7 php 内存泄漏_PHP 内存泄漏分析定位

發布時間:2024/7/19 php 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 7 php 内存泄漏_PHP 内存泄漏分析定位 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

引用前言

本文開始撰寫時我負責的項目需要用php開發一個通過 Socket 與服務端建立長連接后持續實時上報數據的常駐進程程序,在程序業務功能開發聯調完畢后實際運行發送大量數據后發現內存增長非常迅速,在很短的時間內達到了 php 默認可用內存上限 128M ,并報錯:

Fatal error: Allowed memory size of X bytes exhausted (tried to allocate Y bytes)

我第一反應是內存泄露了,但是不知道在哪。第二反應是無用的變量應該用完就 unset 掉,修改完畢后問題依舊。經過了幾番周折終于解決了問題。就決定好好把類似情況整理一下,遂有此文,與諸君共勉。

觀察 PHP 程序內存使用情況

php提提供了兩個方法來獲取當前程序的內存使用情況。

memorygetusage(),這個函數的作用是獲取目前PHP腳本所用的內存大小。

memorygetpeak_usage(),這個函數的作用返回當前腳本到目前位置所占用的內存峰值,這樣就可能獲取到目前的腳本的內存需求情況。int memory_get_usage ([ bool $real_usage = false ] )

int memory_get_peak_usage ([ bool $real_usage = false ] )

函數默認得到的是調用emalloc()占用的內存,如果設置參數為TRUE,則得到的是實際程序向系統申請的內存。因為 PHP 有自己的內存管理機制,所以有時候盡管內部已經釋放了內存但并沒有還給系統。

linux 系統文件 /proc/{$pid}/status 會記錄某個進程的運行狀態,里面的 VmRSS 字段記錄了該進程使用的常駐物理內存(Residence),這個就是該進程實際占用的物理內存了,用這個數據比較靠譜,在程序里面提取這個值也很容易

場景一:程序操作數據過大

情景還原:一次性讀取超過php可用內存上限的數據導致內存耗盡

ini_set('memory_limit', '128M');

$string = str_pad('1', 128 * 1024 * 1024);

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 134217729 bytes) in /Users/zouyi/php-oom/bigfile.php on line 3

這是告訴我們程序運行時試圖分配新內存時由于達到了PHP允許分配的內存上限而拋出致命錯誤,無法繼續執行了,在 java 開發中一般稱之為 OOM ( Out Of Memory ) 。

PHP 配置內存上限是在php.ini中設置memory_limit,PHP 5.2 以前這個默認值是8M,PHP 5.2 的默認值是16M,在這之后的版本默認值都是128M。

問題現象:特定數據處理時可復現,做任何 IO 操作都有可能遇到此類問題,比如:一次 mysql 查詢返回大量數據、一次把大文件讀取進程序等。

解決方法:

能用錢解決的問題都不是問題,如果程序要讀大文件的機會不是很多,且上限可預期,那么通過ini_set('memory_limit', '1G');來設置一個更大的值或者memory_limit=-1。內存管夠的話讓程序一直跑也可以。

如果程序需要考慮在小內存機器上也能正常使用,那就需要優化程序了。如下,代碼復雜了很多。<?php

//php7 以下版本通過 composer 引入 paragonie/random_compat ,為了方便來生成一個隨機名稱的臨時文件

require "vendor/autoload.php";

ini_set('memory_limit', '128M');

//生成臨時文件存放大字符串

$fileName = 'tmp'.bin2hex(random_bytes(5)).'.txt';

touch($fileName);

for ( $i = 0; $i < 128; $i++ ) {

$string = str_pad('1', 1 * 1024 * 1024);

file_put_contents($fileName, $string, FILE_APPEND);

}

$handle = fopen($fileName, "r");

for ( $i = 0; $i <= filesize($fileName) / 1 * 1024 * 1024; $i++ )

{

//do something

$string = fread($handle, 1 * 1024 * 1024);

}

fclose($handle);

unlink($fileName);

場景二:程序操作大數據時產生拷貝

情景還原:執行過程中對大變量進行了復制,導致內存不夠用。

ini_set("memory_limit",'1M');

$string = str_pad('1', 1* 750 *1024);

$string2 = $string;

$string2 .= '1';

Fatal error: Allowed memory size of 1048576 bytes exhausted (tried to allocate 768001 bytes) in /Users/zouyi/php-oom/unset.php on line 8

Call Stack:

0.0004 235440 1. {main}() /Users/zouyi/php-oom/unset.php:0

zend_mm_heap corrupted

問題現象:局部代碼執行過程中占用內存翻倍。

問題分析:

php 是寫時復制(Copy On Write),也就是說,當新變量被賦值時內存不發生變化,直到新變量的內容被操作時才會產生復制。

解決方法:

及早釋放無用變量,或者以引用的形式操作原始數據。

ini_set("memory_limit",'1M');

$string = str_pad('1', 1* 750 *1024);

$string2 = $string;

unset($string);

$string2 .= '1';

ini_set("memory_limit",'1M');

$string = str_pad('1', 1* 750 *1024);

$string2 = &$string;

$string2 .= '1';

unset($string2, $string);

場景三:配置不合理系統資源耗盡

情景還原:因配置不合理導致內存不夠用,2G 內存機器上設置最大可以啟動 100 個 php-fpm 子進程,但實際啟動了 50 個 php-fpm 子進程后無法再啟動更多進程

問題現象:線上業務請求量小的時候不出現問題,請求量一旦很大后部分請求就會執行失敗

問題分析:

一般為了安全方面考慮, php 限制表單請求的最大可提交的數量及大小等參數,post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level。 假設帶寬足夠,用戶頻繁的提交post_max_size = 8M數據到服務端,nginx 轉發給 php-fpm 處理,那么每個 php-fpm 子進程除了自身占用的內存外,即使什么都不做也有可能多占用 8M 內存。

解決方法:

合理設置post_max_size、max_file_uploads、upload_max_filesize、max_input_vars、max_input_nesting_level等參數并調優 php-fpm 相關參數。

$ php -i |grep memory

memory_limit => 1024M => 1024M //php腳本執行最大可使用內存

$php -i |grep max

max_execution_time => 0 => 0 //最大執行時間,腳本默認為0不限制,web請求默認30s

max_file_uploads => 20 => 20 //一個表單里最大上傳文件數量

max_input_nesting_level => 64 => 64 //一個表單里數據最大數組深度層數

max_input_time => -1 => -1 //php從接收請求開始處理數據后的超時時間

max_input_vars => 1000 => 1000 //一個表單(包括get、post、cookie的所有數據)最多提交1000個字段

post_max_size => 8M => 8M //一次post請求最多提交8M數據

upload_max_filesize => 2M => 2M //一個可上傳的文件最大不超過2M

如果上傳設置不合理那么出現大量內存被占用的情況也不奇怪,比如有些內網場景下需要 post 超大字符串post_max_size=200M,那么當從表單提交了 200M 數據到服務端, php 就會分配 200M 內存給這條數據,直到請求處理完畢釋放內存。

pm = dynamic //僅dynamic模式下以下參數生效

pm.max_children = 10 //最大子進程數

pm.start_servers = 3 //啟動時啟動子進程數

pm.min_spare_servers = 2 //最小空閑進程數,不夠了啟動更多進程

pm.max_spare_servers = 5 //最大空閑進程數,超過了結束一些進程

pm.max_requests = 500 //最大請求數,注意這個參數是一個php-fpm如果處理了500個請求后會自己重啟一下,可以避免一些三方擴展的內存泄露問題

一個 php-fpm 進程按 30MB 內存算,50 個 php-fpm 進程就需要 1500MB 內存,這里需要簡單估算一下在負載最重的情況下所有 php-fpm 進程都啟動后是否會把系統內存耗盡。

$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的配置,文件描述符的設置是比較小的,一般生產環境配置要大得多。

場景四:無用的數據未及時釋放

情景還原:這種問題從程序邏輯上不是問題,但是無用的數據大量占用內存導致資源不夠用,應該有針對性的做代碼優化。

Laravel開發中用于監聽數據庫操作時有如下代碼:

DB::listen(function ($query) {

// $query->sql

// $query->bindings

// $query->time

});

啟用數據庫監聽后,每當有 SQL 執行時會 new 一個 QueryExecuted 對象并傳入匿名函數以便后續操作,對于執行完畢就結束進程釋放資源的php程序來說沒有什么問題,而如果是一個常駐進程的程序,程序每執行一條 SQL 內存中就會增加一個 QueryExecuted 對象,程序不結束內存就會始終增長。

問題現象:程序運行期間內存逐漸增長,程序結束后內存正常釋放。

問題分析:

此類問題不易察覺,定位困難,尤其是有些框架封裝好的方法,要明確其適用場景。

解決方法:

本例中要通過DB::listen方法獲取所有執行的 SQL 語句記錄并寫入日志,但此方法存在內存泄露問題,在開發環境下無所謂,在生產環境下則應停用,改用其他途徑獲取執行的 SQL 語句并寫日志。

深入了解

1. 名詞解釋

內存泄漏(Memory Leak):是程序在管理內存分配過程中未能正確的釋放不再使用的內存導致資源被大量占用的一種問題。在面向對象編程時,造成內存泄露的原因常常是對象在內存中存儲但是運行中的代碼卻無法訪問他。由于產生類似問題的情況很多,所以只能從源碼上入手分析定位并解決。

垃圾回收(Garbage Collection,簡稱GC):是一種自動內存管理的形式,GC程序檢查并處理程序中那些已經分配出去但卻不再被對象使用的內存。最早的GC是1959年前后John McCarthy發明的,用來簡化在Lisp中手動控制內存管理。 PHP的內核中已自帶內存管理的功能,一般應用場景下,不易出現內存泄露。

追蹤法(Tracing):從某個根對象開始追蹤,檢查哪些對象可訪問,那么其他的(不可訪問)就是垃圾。

引用計數法(reference count):每個對象都一個數字用來標示被引用的次數。引用次數為0的可以回收。當對一個對象的引用創建時他的引用計數就會增加,引用銷毀時計數減少。引用計數法可以保證對象一旦不被引用時第一時間銷毀。但是引用計數有一些缺陷:1.循環引用,2.引用計數需要申請更多內存,3.對速度有影響,4.需要保證原子性,5.不是實時的2. php 內存管理

引用

在 PHP 5.2 以前, PHP 使用引用計數(Reference count)來做資源管理, 當一個 zval 的引用計數為 0 的時候, 它就會被釋放. 雖然存在循環引用(Cycle reference), 但這樣的設計對于開發 Web 腳本來說, 沒什么問題, 因為 Web 腳本的特點和它追求的目標就是執行時間短, 不會長期運行. 對于循環引用造成的資源泄露, 會在請求結束時釋放掉. 也就是說, 請求結束時釋放資源, 是一種部補救措施( backup ).

然而, 隨著 PHP 被越來越多的人使用, 就有很多人在一些后臺腳本使用 PHP , 這些腳本的特點是長期運行, 如果存在循環引用, 導致引用計數無法及時釋放不用的資源, 則這個腳本最終會內存耗盡退出.

所以在 PHP 5.3 以后, 我們引入了 GC .

—— 摘自鳥哥博客文章《請手動釋放你的資源》

在 PHP 5.3 以后引入了同步周期回收算法(Concurrent Cycle Collection)來處理內存泄露問題,代價是對性能有一定影響,不過一般 web 腳本應用程序影響很小。PHP的垃圾回收機制是默認打開的,php.ini 可以設置zend.enable_gc=0來關閉。也能通過分別調用gcenable() 和 gcdisable()函數來打開和關閉垃圾回收機制。

雖然垃圾回收讓php開發者在內存管理上無需擔心了,但也有極端的反例:php界著名的包管理工具composer曾因加入一行gc_disable();性能得到極大提升。傳送門

引用計數基本知識

回收周期(Collecting Cycles)

上面兩個鏈接是php官方手冊中的內存管理、GC相關知識講解,圖文并茂,這里不再贅述。

3. php-fpm 內存泄露問題

在一臺常見的 nginx + php-fpm 的服務器上:

nginx 服務器 fork 出 n 個子進程(worker), php-fpm 管理器 fork 出 n 個子進程。

當有用戶請求, nginx 的一個 worker 接收請求,并將請求拋到 socket 中。

php-fpm 空閑的子進程監聽到 socket 中有請求,接收并處理請求。一個 php-fpm 的生命周期大致是這樣的:

模塊初始化(MINIT)-> 請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN) -> 請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN)……. 請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN)-> 模塊關閉(MSHUTDOWN)。

在請求初始化(RINIT)-> 請求處理 -> 請求結束(RSHUTDOWN)這個“請求處理”過程是: php 讀取相應的 php 文件,對其進行詞法分析,生成 opcode , zend 虛擬機執行 opcode 。

php 在每次請求結束后自動釋放內存,有效避免了常見場景下內存泄露的問題,然而實際環境中因某些擴展的內存管理沒有做好或者 php 代碼中出現循環引用導致未能正常釋放不用的資源。

在 php-fpm 配置文件中,將pm.max_requests這個參數設置小一點。這個參數的含義是:一個 php-fpm 子進程最多處理pm.max_requests個用戶請求后,就會被銷毀。當一個 php-fpm 進程被銷毀后,它所占用的所有內存都會被回收。

4. 常駐進程內存泄露問題

Valgrind 包括如下一些工具:

Memcheck。這是 valgrind 應用最廣泛的工具,一個重量級的內存檢查器,能夠發現開發中絕大多數內存錯誤使用情況,比如:使用未初始化的內存,使用已經釋放了的內存,內存訪問越界等。

Callgrind。它主要用來檢查程序中函數調用過程中出現的問題。

Cachegrind。它主要用來檢查程序中緩存使用出現的問題。

Helgrind。它主要用來檢查多線程程序中出現的競爭問題。

Massif。它主要用來檢查程序中堆棧使用中出現的問題。

Extension。可以利用core提供的功能,自己編寫特定的內存調試工具。Memcheck 對調試 C/C++ 程序的內存泄露很有幫助,它的機制是在系統 alloc/free 等函數調用上加計數。 php 程序的內存泄露,是由于一些循環引用,或者 gc 的邏輯錯誤, valgrind 無法探測,因此需要在檢測時需要關閉 php 自帶的內存管理。

$ export USE_ZEND_ALLOC=0 # 設置環境變量關閉內存管理

$ valgrind --tool=memcheck --num-callers=30 --log-file=php.log /Users/zouyi/Downloads/php-5.6.31/sapi/cli/php leak.php

通過命令行執行 valgrind 分析可能有內存泄露的文件

引用

==12075== Memcheck, a memory error detector

==12075== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.

==12075== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info

==12075== Command: /Users/zouyi/Downloads/php-5.6.31/sapi/cli/php leak.php

==12075== Parent PID: 42043

==12075==

==12075== Syscall param msg->desc.port.name points to uninitialised byte(s)

==12075==??? at 0x10121F34A: mach_msg_trap (in /usr/lib/system/libsystem_kernel.dylib)

==12075==??? by 0x10121E796: mach_msg (in /usr/lib/system/libsystem_kernel.dylib)

==12075==??? by 0x101218485: task_set_special_port (in /usr/lib/system/libsystem_kernel.dylib)

==12075==??? by 0x1013B410E: _os_trace_create_debug_control_port (in /usr/lib/system/libsystem_trace.dylib)

==12075==??? by 0x1013B4458: _libtrace_init (in /usr/lib/system/libsystem_trace.dylib)

==12075==??? by 0x100DF09DF: libSystem_initializer (in /usr/lib/libSystem.B.dylib)

==12075==??? by 0x100C37A1A: ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) (in /usr/lib/dyld)

==12075==??? by 0x100C37C1D: ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) (in /usr/lib/dyld)

==12075==??? by 0x100C334A9: ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) (in /usr/lib/dyld)

==12075==??? by 0x100C33440: ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) (in /usr/lib/dyld)

==12075==??? by 0x100C32523: ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) (in /usr/lib/dyld)

==12075==??? by 0x100C325B8: ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) (in /usr/lib/dyld)

==12075==??? by 0x100C24433: dyld::initializeMainExecutable() (in /usr/lib/dyld)

==12075==??? by 0x100C288C5: dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) (in /usr/lib/dyld)

==12075==??? by 0x100C23248: dyldbootstrap::start(macho_header const*, int, char const**, long, macho_header const*, unsigned long*) (in /usr/lib/dyld)

==12075==??? by 0x100C23035: _dyld_start (in /usr/lib/dyld)

==12075==??? by 0x1: ???

==12075==??? by 0x1054AC862: ???

==12075==??? by 0x1054AC891: ???

==12075==? Address 0x1054aa98c is on thread 1's stack

==12075==? in frame #2, created by task_set_special_port (???:)

==12075==

--12075-- UNKNOWN mach_msg unhandled MACH_SEND_TRAILER option

--12075-- UNKNOWN mach_msg unhandled MACH_SEND_TRAILER option (repeated 2 times)

--12075-- UNKNOWN mach_msg unhandled MACH_SEND_TRAILER option (repeated 4 times)

==12075==

==12075== HEAP SUMMARY:

==12075==???? in use at exit: 125,805 bytes in 185 blocks

==12075==?? total heap usage: 14,686 allocs, 14,501 frees, 3,261,322 bytes allocated

==12075==

==12075== LEAK SUMMARY:

==12075==??? definitely lost: 3 bytes in 1 blocks

==12075==??? indirectly lost: 0 bytes in 0 blocks

==12075==????? possibly lost: 72 bytes in 3 blocks

==12075==??? still reachable: 107,582 bytes in 23 blocks

==12075==???????? suppressed: 18,148 bytes in 158 blocks

==12075== Rerun with --leak-check=full to see details of leaked memory

==12075==

==12075== For counts of detected and suppressed errors, rerun with: -v

==12075== Use --track-origins=yes to see where uninitialised values come from

==12075== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 1 from 1)

引用

definitely lost: 肯定內存泄露

indirectly lost: 非直接內存泄露

possibly lost: 可能發生內存泄露

still reachable: 仍然可訪問的內存

suppressed: 外部造成的內存泄露

Callgrind 配合 php 擴展 xdebug 輸出的 profile 分析日志文件可以分析程序運行期間各個函數調用時占用的內存、 CPU 占用情況。

總結

遇到了內存泄露時先觀察是程序本身內存不足還是外部資源導致,然后搞清楚程序運行中用到了哪些資源:寫入磁盤日志、連接數據庫 SQL 查詢、發送 Curl 請求、 Socket 通信等, I/O 操作必然會用到內存,如果這些地方都沒有發生明顯的內存泄露,檢查哪里處理大量數據沒有及時釋放資源,如果是 php 5.3 以下版本還需考慮循環引用的問題。多了解一些 Linux 下的分析輔助工具,解決問題時可以事半功倍。

最后宣傳一下穿云團隊今年最新開源的應用透明鏈路追蹤工具 Molten:https://github.com/chuan-yun/Molten。安裝好php擴展后就能幫你實時收集程序的 curl,pdo,mysqli,redis,mongodb,memcached 等請求的數據,可以很方便的與 zipkin 集成。

參考資料

http://php.net/manual/zh/features.gc.php

http://www.php-internals.com/book/?p=chapt06/06-07-memory-leaks

http://www.programering.com/a/MDN5UjMwATk.html

https://stackoverflow.com/questions/20458136/using-valgrind-to-debug-a-php-cli-segmentation-fault

http://www.laruence.com/2013/08/14/2899.html

https://mengkang.net/873.html

總結

以上是生活随笔為你收集整理的7 php 内存泄漏_PHP 内存泄漏分析定位的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。