Mendiagnosis Kebocoran Memori - Ukuran memori # byte yang diizinkan habis

98

Saya telah menemukan pesan kesalahan yang ditakuti, mungkin melalui upaya yang melelahkan, PHP kehabisan memori:

Ukuran memori yang diizinkan dari #### byte habis (mencoba mengalokasikan #### byte) di file.php pada baris 123

Meningkatkan batas

Jika Anda tahu apa yang Anda lakukan dan ingin meningkatkan batasnya, lihat memory_limit :

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

Awas! Anda mungkin hanya menyelesaikan gejalanya dan bukan masalahnya!

Mendiagnosis kebocoran:

Pesan kesalahan menunjuk ke sebuah baris dalam sebuah loop yang saya yakini telah bocor, atau terakumulasi secara tidak perlu, memori. Saya telah mencetak memory_get_usage()pernyataan di akhir setiap iterasi dan dapat melihat jumlahnya perlahan-lahan bertambah hingga mencapai batas:

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
}

Untuk keperluan pertanyaan ini, mari kita asumsikan kode spaghetti terburuk yang bisa dibayangkan bersembunyi dalam lingkup global di suatu tempat di $useratau Task.

Alat, trik PHP, atau debugging voodoo apa yang dapat membantu saya menemukan dan memperbaiki masalah?

Mike B
sumber
PS - Baru-baru ini saya mengalami masalah dengan jenis yang persis seperti ini. Sayangnya, saya juga menemukan bahwa php memiliki masalah penghancuran objek anak. Jika Anda membatalkan setel objek induk, objek turunannya tidak akan dibebaskan. Harus memastikan saya menggunakan unset yang dimodifikasi yang menyertakan panggilan rekursif ke semua objek anak __destruct dan seterusnya. Detailnya di sini: paul-m-jones.com/archives/262 :: Saya melakukan sesuatu seperti: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ item -> __ destruct (); } tidak disetel ($ item); }
Josh

Jawaban:

48

PHP tidak memiliki pengumpul sampah. Ini menggunakan penghitungan referensi untuk mengelola memori. Jadi, sumber kebocoran memori yang paling umum adalah referensi siklik dan variabel global. Jika Anda menggunakan kerangka kerja, Anda akan memiliki banyak kode untuk dijelajahi untuk menemukannya, saya khawatir. Instrumen paling sederhana adalah melakukan panggilan ke tempat yang selektif memory_get_usagedan mempersempitnya ke tempat kode bocor. Anda juga dapat menggunakan xdebug untuk membuat jejak kode. Jalankan kode dengan jejak eksekusi dan show_mem_delta.

troelskn
sumber
3
Tapi hati-hati ... file jejak yang dihasilkan akan menjadi BESAR. Pertama kali saya menjalankan jejak xdebug pada aplikasi Zend Framework, butuh waktu lama untuk menjalankan dan menghasilkan file berukuran multi GB (bukan kb atau MB ... GB). Waspadai ini.
rg88
1
Ya, itu cukup berat .. GB terdengar agak berlebihan - kecuali Anda memiliki skrip yang besar. Mungkin mencoba untuk memproses beberapa baris (Seharusnya cukup untuk mengidentifikasi kebocoran). Selain itu, jangan menginstal ekstensi xdebug di server produksi.
troelskn
31
Sejak 5.3 PHP sebenarnya memiliki pengumpul sampah. Di sisi lain, fungsi profil memori telah dihapus dari
xdebug
3
+1 menemukan kebocorannya! Kelas yang memiliki referensi siklik! Setelah referensi ini tidak disetel (), objek dikumpulkan seperti yang diharapkan! Terima kasih! :)
rinogo
@ rinogo jadi bagaimana Anda mengetahui tentang kebocoran tersebut? Dapatkah Anda membagikan langkah apa yang Anda ambil?
JohnnyQ
11

Berikut adalah trik yang kami gunakan untuk mengidentifikasi skrip mana yang menggunakan paling banyak memori di server kami.

Simpan potongan berikut dalam file di, misalnya, /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');

Gunakan dengan menambahkan berikut ini ke httpd.conf:

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

Kemudian analisis file log di /var/log/httpd/php_memory_log

Anda mungkin perlu melakukannya touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logsebelum pengguna web Anda dapat menulis ke file log.

Quinn Comendant
sumber
8

Saya melihat suatu kali dalam skrip lama bahwa PHP akan mempertahankan variabel "as" seperti dalam cakupan bahkan setelah perulangan foreach saya. Sebagai contoh,

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

Saya tidak yakin apakah versi PHP yang akan datang memperbaiki ini atau tidak karena saya sudah melihatnya. Jika demikian, Anda dapat menghapus baris unset($user)setelahnya doSomething()dari memori. YMMV.

patcoll.dll
sumber
13
PHP tidak mencakup loop / kondisional seperti C / Java / etc. Apa pun yang dideklarasikan di dalam loop / kondisional masih dalam cakupan bahkan setelah keluar dari loop / kondisional (menurut desain [?]). Metode / fungsi, di sisi lain, memiliki cakupan seperti yang Anda harapkan - semuanya dirilis setelah eksekusi fungsi berakhir.
Frank Farmer
Saya berasumsi bahwa itu memang sengaja. Salah satu keuntungannya adalah bahwa setelah pengulangan, Anda dapat mengerjakan item terakhir yang Anda temukan, misalnya yang memenuhi kriteria tertentu.
joachim
Anda dapat unset()melakukannya, tetapi perlu diingat bahwa untuk objek, yang Anda lakukan hanyalah mengubah ke mana variabel Anda mengarah - Anda belum benar-benar menghapusnya dari memori. PHP akan secara otomatis membebaskan memori setelah berada di luar ruang lingkup, jadi solusi yang lebih baik (dalam hal jawaban ini, bukan pertanyaan OP) adalah menggunakan fungsi pendek sehingga mereka tidak bergantung pada variabel itu dari loop juga panjang.
Rich Court
@patcoll Ini tidak ada hubungannya dengan kebocoran memori. Ini hanyalah penunjuk array yang berubah. Lihat di sini: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html di versi 3a.
Harm Smits
7

Ada beberapa kemungkinan kebocoran memori di php:

  • php itu sendiri
  • ekstensi php
  • perpustakaan php yang Anda gunakan
  • kode php Anda

Cukup sulit untuk menemukan dan memperbaiki 3 yang pertama tanpa rekayasa balik yang mendalam atau pengetahuan kode sumber php. Untuk yang terakhir Anda dapat menggunakan pencarian biner untuk kode kebocoran memori dengan memory_get_usage

kingoleg.dll
sumber
91
Jawaban Anda kira-kira sama umumnya
TravisO
2
Sungguh memalukan bahwa bahkan php 7.2 mereka tidak dapat memperbaiki kebocoran memori inti php. Anda tidak dapat menjalankan proses yang berjalan lama di dalamnya.
Aftab Naveed
6

Saya baru-baru ini mengalami masalah ini pada aplikasi, dalam keadaan yang saya anggap serupa. Skrip yang berjalan di cli PHP yang melakukan loop pada banyak iterasi. Skrip saya bergantung pada beberapa pustaka yang mendasarinya. Saya menduga perpustakaan tertentu adalah penyebabnya dan saya menghabiskan beberapa jam dengan sia-sia mencoba menambahkan metode penghancuran yang sesuai ke kelasnya tetapi tidak berhasil. Dihadapkan dengan proses konversi yang panjang ke perpustakaan yang berbeda (yang ternyata memiliki masalah yang sama), saya datang dengan solusi kasar untuk masalah tersebut dalam kasus saya.

Dalam situasi saya, di cli linux, saya melakukan perulangan pada banyak catatan pengguna dan untuk masing-masing dari mereka membuat contoh baru dari beberapa kelas yang saya buat. Saya memutuskan untuk mencoba membuat instance baru dari kelas menggunakan metode exec PHP sehingga proses tersebut akan berjalan di "thread baru". Berikut adalah contoh yang sangat mendasar dari apa yang saya maksud:

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
}

Tentunya pendekatan ini memiliki keterbatasan, dan kita perlu menyadari bahayanya, karena akan mudah untuk membuat pekerjaan kelinci, namun dalam beberapa kasus yang jarang terjadi, ini mungkin membantu mengatasi titik yang sulit, sampai perbaikan yang lebih baik dapat ditemukan. , seperti dalam kasus saya.

Nate Flink
sumber
6

Saya menemukan masalah yang sama, dan solusi saya adalah mengganti foreach dengan yang biasa. Saya tidak yakin tentang spesifikasinya, tetapi sepertinya foreach membuat salinan (atau entah bagaimana referensi baru) ke objek. Menggunakan loop biasa, Anda mengakses item secara langsung.

Gunnar Lium
sumber
5

Saya sarankan Anda memeriksa manual php atau menambahkan gc_enable()fungsi untuk mengumpulkan sampah ... Itu adalah kebocoran memori tidak mempengaruhi bagaimana kode Anda berjalan.

PS: php memiliki pengumpul sampah gc_enable()yang tidak membutuhkan argumen.

Kosgei
sumber
3

Saya baru-baru ini memperhatikan bahwa fungsi lambda PHP 5.3 meninggalkan memori tambahan yang digunakan ketika mereka dihapus.

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

Saya tidak yakin mengapa, tetapi tampaknya membutuhkan 250 byte ekstra setiap lambda bahkan setelah fungsi tersebut dihapus.

Xeoncross
sumber
2
Saya akan mengatakan hal yang sama. Ini telah diperbaiki pada 5.3.10 ( # 60139 )
Kristopher Ives
@KopherIves, terima kasih atas pembaruannya! Anda benar, ini bukan lagi masalah jadi saya tidak perlu takut menggunakannya seperti orang gila sekarang.
Xeoncross
2

Jika apa yang Anda katakan tentang PHP hanya melakukan GC setelah suatu fungsi benar, Anda dapat menggabungkan konten loop di dalam fungsi sebagai solusi / eksperimen.

Bart van Heukelom
sumber
1
@DavidKullmann Sebenarnya saya rasa jawaban saya salah. Bagaimanapun, run()yang disebut juga merupakan fungsi, yang pada akhirnya GC harus terjadi.
Bart van Heukelom
2

Satu masalah besar yang saya hadapi adalah dengan menggunakan create_function . Seperti dalam fungsi lambda, ia meninggalkan nama sementara yang dihasilkan di memori.

Penyebab lain kebocoran memori (dalam kasus Zend Framework) adalah Zend_Db_Profiler. Pastikan itu dinonaktifkan jika Anda menjalankan skrip di bawah Zend Framework. Misalnya yang saya miliki di aplikasi saya .ini berikut ini:

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

Menjalankan sekitar 25.000 kueri + banyak pemrosesan sebelum itu, membawa memori ke 128Mb (Batas memori maks saya).

Dengan hanya mengatur:

resources.db.profiler.enabled    = false

itu cukup untuk menyimpannya di bawah 20 Mb

Dan skrip ini berjalan di CLI, tetapi ia membuat instance Zend_Application dan menjalankan Bootstrap, jadi ia menggunakan konfigurasi "pengembangan".

Ini sangat membantu menjalankan skrip dengan profil xDebug

Andy
sumber
2

Saya tidak melihatnya secara eksplisit disebutkan, tetapi xdebug melakukan pekerjaan yang baik dalam membuat profil waktu dan memori (mulai 2.6 ). Anda dapat mengambil informasi yang dihasilkannya dan menyebarkannya ke front end gui pilihan Anda: webgrind (hanya waktu), kcachegrind , qcachegrind atau lainnya dan itu menghasilkan pohon panggilan dan grafik yang sangat berguna untuk memungkinkan Anda menemukan sumber berbagai kesengsaraan Anda .

Contoh (dari qcachegrind): masukkan deskripsi gambar di sini

SeanDowney
sumber
1

Saya agak terlambat untuk percakapan ini tetapi saya akan membagikan sesuatu yang berkaitan dengan Zend Framework.

Saya mengalami masalah kebocoran memori setelah menginstal php 5.3.8 (menggunakan phpfarm) untuk bekerja dengan aplikasi ZF yang dikembangkan dengan php 5.2.9. Saya menemukan bahwa kebocoran memori dipicu di file httpd.conf Apache, dalam definisi host virtual saya, yang tertulis SetEnv APPLICATION_ENV "development". Setelah mengomentari kalimat ini, kebocoran memori berhenti. Saya mencoba mencari solusi inline di skrip php saya (terutama dengan mendefinisikannya secara manual di file index.php utama).

fronzee.dll
sumber
1
Pertanyaannya mengatakan dia menjalankan CLI. Itu berarti Apache sama sekali tidak terlibat dalam proses tersebut.
Maxime
1
@Maxime Poin yang bagus, saya gagal menangkapnya, terima kasih. Baiklah, semoga beberapa Googler acak akan mendapatkan manfaat dari catatan yang saya tinggalkan di sini, karena halaman ini muncul untuk saya saat mencoba menyelesaikan masalah saya.
fronzee
Periksa jawaban saya untuk pertanyaan ini, mungkin itu kasus Anda juga.
Andy
Aplikasi Anda harus memiliki konfigurasi yang berbeda bergantung pada lingkungannya. The "development"lingkungan biasanya memiliki banyak penebangan & profiling bahwa lingkungan lainnya mungkin tidak memiliki. Mengomentari baris keluar hanya membuat aplikasi Anda menggunakan lingkungan default, yang biasanya "production"atau "prod". Kebocoran memori masih ada; kode yang memuatnya tidak dipanggil di lingkungan itu.
Marco Roy
0

Saya tidak melihatnya disebutkan di sini tetapi satu hal yang mungkin membantu adalah menggunakan xdebug dan xdebug_debug_zval ('variableName') untuk melihat refcount.

Saya juga dapat memberikan contoh ekstensi php yang menghalangi: Z-Ray Zend Server. Jika pengumpulan data diaktifkan, penggunaan memori akan membengkak pada setiap iterasi seperti jika pengumpulan sampah dimatikan.

HappyDude
sumber