Chẩn đoán rò rỉ bộ nhớ - Dung lượng bộ nhớ được phép là # byte đã cạn kiệt


98

Tôi đã gặp phải thông báo lỗi đáng sợ, có thể là do nỗ lực chăm chỉ, PHP đã hết bộ nhớ:

Kích thước bộ nhớ được phép là #### byte đã cạn kiệt (đã cố gắng cấp phát #### byte) trong file.php trên dòng 123

Tăng giới hạn

Nếu bạn biết mình đang làm gì và muốn tăng giới hạn, hãy xem memory_limit :

ini_set('memory_limit', '16M');
ini_set('memory_limit', -1); // no limit

Hãy coi chừng! Bạn có thể chỉ giải quyết được triệu chứng chứ không phải vấn đề!

Chẩn đoán rò rỉ:

Thông báo lỗi trỏ đến một dòng có một vòng lặp mà tôi tin rằng đang bị rò rỉ, hoặc tích lũy không cần thiết, bộ nhớ. Tôi đã in các memory_get_usage()câu lệnh ở cuối mỗi lần lặp và có thể thấy số lượng tăng dần cho đến khi đạt đến giới hạn:

foreach ($users as $user) {
    $task = new Task;
    $task->run($user);
    unset($task); // Free the variable in an attempt to recover memory
    print memory_get_usage(true); // increases over time
}

Với mục đích của câu hỏi này, hãy giả sử mã spaghetti tồi tệ nhất có thể tưởng tượng được đang ẩn trong phạm vi toàn cầu ở đâu đó trong $userhoặc Task.

Những công cụ, thủ thuật PHP hoặc voodoo gỡ lỗi nào có thể giúp tôi tìm và khắc phục sự cố?


Tái bút - Gần đây tôi đã gặp sự cố với loại điều chính xác này. Thật không may, tôi cũng thấy rằng php có một vấn đề phá hủy đối tượng con. Nếu bạn bỏ đặt một đối tượng cha, các đối tượng con của nó sẽ không được giải phóng. Phải đảm bảo rằng tôi sử dụng một unset đã sửa đổi bao gồm một lệnh gọi đệ quy cho tất cả các đối tượng con __destruct, v.v. Thông tin chi tiết tại đây: paul-m-jones.com/archives/262 :: Tôi đang thực hiện một việc như: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ item -> __ destruct (); } unset ($ item); }
Josh

Câu trả lời:


48

PHP không có bộ thu gom rác. Nó sử dụng việc đếm tham chiếu để quản lý bộ nhớ. Do đó, nguồn rò rỉ bộ nhớ phổ biến nhất là các tham chiếu tuần hoàn và các biến toàn cục. Nếu bạn sử dụng một khung công tác, bạn sẽ có rất nhiều mã phải tra qua để tìm nó, tôi e rằng. Công cụ đơn giản nhất là thực hiện có chọn lọc các cuộc gọi đến memory_get_usagevà thu hẹp nó đến nơi mã bị rò rỉ. Bạn cũng có thể sử dụng xdebug để tạo dấu vết của mã. Chạy mã với các dấu vết thực thishow_mem_delta.


3
Nhưng hãy coi chừng ... các tập tin theo dõi được tạo ra sẽ MẠNH MẼ. Lần đầu tiên tôi chạy dấu vết xdebug trên ứng dụng Zend Framework, phải mất một khoảng thời gian dài để chạy và tạo tệp có kích thước nhiều GB (không phải kb hoặc MB ... GB). Chỉ cần lưu ý điều này.
rg88

1
Vâng, nó khá nặng .. GB có vẻ hơi nhiều - trừ khi bạn có một tập lệnh lớn. Có thể cố gắng chỉ xử lý một vài hàng (Đủ để xác định rò rỉ). Ngoài ra, không cài đặt phần mở rộng xdebug trên máy chủ sản xuất.
troelskn

31
Vì 5.3 PHP thực sự có một bộ thu gom rác. Mặt khác, bộ nhớ chức năng profiling đã được gỡ bỏ fro Xdebug :(
wdev

3
+1 tìm thấy rò rỉ! Một lớp có tham chiếu tuần hoàn! Khi các tham chiếu này không được đặt (), các đối tượng đã được thu gom như mong đợi! Cảm ơn! :)
rinogo

@rinogo vậy làm thế nào bạn phát hiện ra rò rỉ? Bạn có thể chia sẻ những bước bạn đã thực hiện?
JohnnyQ

11

Đây là một thủ thuật mà chúng tôi đã sử dụng để xác định tập lệnh nào đang sử dụng nhiều bộ nhớ nhất trên máy chủ của chúng tôi.

Lưu đoạn mã sau vào một tệp tại, ví dụ /usr/local/lib/php/strangecode_log_memory_usage.inc.php:

<?php
function strangecode_log_memory_usage()
{
    $site = '' == getenv('SERVER_NAME') ? getenv('SCRIPT_FILENAME') : getenv('SERVER_NAME');
    $url = $_SERVER['PHP_SELF'];
    $current = memory_get_usage();
    $peak = memory_get_peak_usage();
    error_log("$site current: $current peak: $peak $url\n", 3, '/var/log/httpd/php_memory_log');
}
register_shutdown_function('strangecode_log_memory_usage');

Sử dụng nó bằng cách thêm phần sau vào httpd.conf:

php_admin_value auto_prepend_file /usr/local/lib/php/strangecode_log_memory_usage.inc.php

Sau đó, phân tích tệp nhật ký tại /var/log/httpd/php_memory_log

Bạn có thể cần touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logtrước khi người dùng web của bạn có thể ghi vào tệp nhật ký.


8

Một lần tôi nhận thấy trong một tập lệnh cũ rằng PHP sẽ duy trì biến "as" như trong phạm vi ngay cả sau vòng lặp foreach của tôi. Ví dụ,

foreach($users as $user){
  $user->doSomething();
}
var_dump($user); // would output the data from the last $user 

Tôi không chắc liệu các phiên bản PHP trong tương lai có sửa lỗi này hay không vì tôi đã thấy nó. Nếu đúng như vậy, bạn có thể unset($user)sau doSomething()dòng để xóa nó khỏi bộ nhớ. YMMV.


13
PHP không phạm vi các vòng lặp / điều kiện như C / Java / etc. Bất kỳ thứ gì được khai báo bên trong vòng lặp / điều kiện vẫn nằm trong phạm vi ngay cả sau khi thoát khỏi vòng lặp / điều kiện (theo thiết kế [?]). Mặt khác, các phương thức / hàm được xác định phạm vi như bạn mong đợi - mọi thứ sẽ được giải phóng khi quá trình thực thi hàm kết thúc.
Frank Farmer

Tôi đã cho rằng đó là do thiết kế. Một lợi ích của nó là sau một vòng lặp, bạn có thể làm việc với mục cuối cùng bạn tìm thấy, ví dụ như thỏa mãn các tiêu chí cụ thể.
joachim

Bạn có thể làm được unset()điều đó, nhưng hãy nhớ rằng đối với các đối tượng, tất cả những gì bạn đang làm là thay đổi vị trí mà biến của bạn trỏ đến - bạn chưa thực sự xóa nó khỏi bộ nhớ. PHP sẽ tự động giải phóng bộ nhớ khi nó nằm ngoài phạm vi, vì vậy giải pháp tốt hơn (về câu trả lời này, không phải câu hỏi của OP) là sử dụng các hàm ngắn để chúng không bị treo vào biến đó từ vòng lặp. Dài.
Rich Court

@patcoll Điều này không liên quan gì đến việc rò rỉ bộ nhớ. Đây chỉ đơn giản là thay đổi con trỏ mảng. Hãy xem tại đây: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html tại phiên bản 3a.
Harm Smits

7

Có một số điểm bộ nhớ có thể bị rò rỉ trong php:

  • chính php
  • phần mở rộng php
  • thư viện php bạn sử dụng
  • mã php của bạn

Khá khó để tìm và sửa 3 lỗi đầu tiên nếu không có kiến ​​thức về kỹ thuật đảo ngược sâu hoặc mã nguồn php. Đối với cái cuối cùng, bạn có thể sử dụng tìm kiếm nhị phân để tìm mã rò rỉ bộ nhớ với memory_get_usage


91
Câu trả lời của bạn về mức độ chung chung mà nó có thể nhận được
TravisO

2
Thật tiếc là ngay cả php 7.2 họ cũng không thể sửa lỗi bộ nhớ php lõi. Bạn không thể chạy các quy trình chạy dài trong đó.
Aftab Naveed

6

Gần đây tôi đã gặp sự cố này trên một ứng dụng, theo những gì tôi thu thập được là những trường hợp tương tự. Một tập lệnh chạy trong cli của PHP lặp lại nhiều lần. Tập lệnh của tôi phụ thuộc vào một số thư viện cơ bản. Tôi nghi ngờ một thư viện cụ thể là nguyên nhân và tôi đã mất vài giờ vô ích để cố gắng thêm các phương thức hủy phù hợp vào các lớp của nó nhưng không có kết quả. Đối mặt với quá trình chuyển đổi kéo dài sang một thư viện khác (có thể gặp phải các vấn đề tương tự), tôi đã đưa ra một phương án thô sơ cho vấn đề trong trường hợp của mình.

Trong tình huống của tôi, trên một cli linux, tôi đã lặp qua một loạt các bản ghi người dùng và đối với mỗi một trong số chúng, tạo một phiên bản mới của một số lớp tôi đã tạo. Tôi quyết định thử tạo các phiên bản mới của các lớp bằng cách sử dụng phương thức thực thi của PHP để quá trình đó sẽ chạy trong một "luồng mới". Đây là một mẫu thực sự cơ bản về những gì tôi đang đề cập:

foreach ($ids as $id) {
   $lines=array();
   exec("php ./path/to/my/classes.php $id", $lines);
   foreach ($lines as $line) { echo $line."\n"; } //display some output
}

Rõ ràng là cách tiếp cận này có những hạn chế và người ta cần phải nhận thức được sự nguy hiểm của điều này, vì có thể dễ dàng tạo ra một công việc cho thỏ, tuy nhiên trong một số trường hợp hiếm hoi, nó có thể giúp vượt qua một điểm khó khăn, cho đến khi có thể tìm ra giải pháp tốt hơn , như trong trường hợp của tôi.


6

Tôi cũng gặp phải vấn đề tương tự và giải pháp của tôi là thay thế foreach bằng for thông thường. Tôi không chắc về các chi tiết cụ thể, nhưng có vẻ như foreach tạo một bản sao (hoặc bằng cách nào đó một tham chiếu mới) cho đối tượng. Sử dụng vòng lặp for thông thường, bạn truy cập trực tiếp vào mục.


5

Tôi khuyên bạn nên kiểm tra hướng dẫn sử dụng php hoặc thêm gc_enable() chức năng thu thập rác ... Đó là việc rò rỉ bộ nhớ không ảnh hưởng đến cách chạy mã của bạn.

PS: php có một trình thu gom rác gc_enable()không có đối số.


3

Gần đây tôi nhận thấy rằng các hàm lambda của PHP 5.3 để lại bộ nhớ thừa được sử dụng khi chúng bị xóa.

for ($i = 0; $i < 1000; $i++)
{
    //$log = new Log;
    $log = function() { return new Log; };
    //unset($log);
}

Tôi không chắc tại sao, nhưng có vẻ như mỗi lambda mất thêm 250 byte ngay cả sau khi hàm bị xóa.


2
Tôi cũng sẽ nói như vậy. Điều này đã được khắc phục kể từ ngày 5.3.10 ( # 60139 )
Kristopher Ives

@KristopherIves, cảm ơn bạn đã cập nhật! Bạn nói đúng, đây không còn là vấn đề nữa vì vậy tôi không nên sợ hãi khi sử dụng chúng như điên rồ.
Xeoncross

2

Nếu những gì bạn nói về việc PHP chỉ thực hiện GC sau một hàm là đúng, bạn có thể bọc nội dung của vòng lặp bên trong một hàm như một cách giải quyết / thử nghiệm.


1
@DavidKullmann Thực ra tôi nghĩ câu trả lời của mình là sai. Rốt cuộc, run()cái được gọi cũng là một hàm, khi kết thúc GC sẽ xảy ra.
Bart van Heukelom

2

Một vấn đề lớn mà tôi gặp phải là khi sử dụng create_ functions . Giống như trong các hàm lambda, nó để lại tên tạm thời được tạo trong bộ nhớ.

Một nguyên nhân khác gây ra rò rỉ bộ nhớ (trong trường hợp Zend Framework) là Zend_Db_Profiler. Đảm bảo rằng điều đó bị vô hiệu hóa nếu bạn chạy tập lệnh trong Zend Framework. Ví dụ, tôi đã có trong application.ini của mình những điều sau:

resources.db.profiler.enabled    = true
resources.db.profiler.class      = Zend_Db_Profiler_Firebug

Chạy khoảng 25.000 truy vấn + vô số xử lý trước đó, đã đưa bộ nhớ lên 128Mb (Giới hạn bộ nhớ tối đa của tôi).

Bằng cách chỉ thiết lập:

resources.db.profiler.enabled    = false

nó đủ để giữ nó dưới 20 Mb

Và tập lệnh này đang chạy trong CLI, nhưng nó đang khởi tạo Zend_Application và chạy Bootstrap, vì vậy nó sử dụng cấu hình "development".

Nó thực sự giúp chạy tập lệnh với cấu hình xDebug


2

Tôi không thấy nó được đề cập rõ ràng, nhưng xdebug thực hiện rất tốt việc lập hồ sơ thời gian và bộ nhớ (kể từ 2.6 ). Bạn có thể lấy thông tin mà nó tạo ra và chuyển nó đến giao diện người dùng của gui mà bạn chọn: webgrind (chỉ thời gian), kcachegrind , qcachegrind hoặc những thứ khác và nó tạo ra các cây gọi và đồ thị rất hữu ích để cho phép bạn tìm ra nguồn gốc của các tai ương khác nhau của mình .

Ví dụ (của qcachegrind): nhập mô tả hình ảnh ở đây


1

Tôi hơi muộn trong cuộc trò chuyện này nhưng tôi sẽ chia sẻ điều gì đó liên quan đến Zend Framework.

Tôi đã gặp sự cố rò rỉ bộ nhớ sau khi cài đặt php 5.3.8 (sử dụng phpfarm) để làm việc với ứng dụng ZF được phát triển với php 5.2.9. Tôi phát hiện ra rằng lỗi rò rỉ bộ nhớ đang được kích hoạt trong tệp httpd.conf của Apache, trong định nghĩa máy chủ ảo của tôi, nơi nó cho biết SetEnv APPLICATION_ENV "development". Sau khi bình luận dòng này, việc rò rỉ bộ nhớ đã dừng lại. Tôi đang cố gắng đưa ra một giải pháp nội tuyến trong tập lệnh php của mình (chủ yếu bằng cách xác định nó theo cách thủ công trong tệp index.php chính).


1
Câu hỏi nói rằng anh ta đang chạy trong CLI. Điều đó có nghĩa là Apache hoàn toàn không tham gia vào quá trình này.
Maxime

1
@Maxime Điểm tốt, tôi không hiểu được, cảm ơn. Ồ, dù sao, hy vọng một số nhân viên Google ngẫu nhiên sẽ được hưởng lợi từ ghi chú tôi để lại ở đây, vì trang này đã xuất hiện cho tôi trong khi cố gắng giải quyết vấn đề của tôi.
footere

Kiểm tra câu trả lời của tôi cho câu hỏi này, có lẽ đó cũng là trường hợp của bạn.
Andy

Ứng dụng của bạn phải có các cấu hình khác nhau tùy thuộc vào môi trường. Các "development"môi trường thường có một loạt các cách đăng nhập và profiling rằng môi trường khác có thể không có. Nhận xét dòng ra chỉ làm cho ứng dụng của bạn sử dụng môi trường mặc định thay thế, thường là "production"hoặc "prod". Rò rỉ bộ nhớ vẫn tồn tại; mã chứa nó không được gọi trong môi trường đó.
Marco Roy

0

Tôi không thấy nó được đề cập ở đây nhưng một điều có thể hữu ích là sử dụng xdebug và xdebug_debug_zval ('variableName') để xem số tiền hoàn lại.

Tôi cũng có thể cung cấp một ví dụ về một phần mở rộng php đang cản trở: Z-Ray của Máy chủ Zend. Nếu tính năng thu thập dữ liệu được bật, việc sử dụng bộ nhớ sẽ tăng lên trên mỗi lần lặp lại giống như khi tắt tính năng thu thập rác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.