Diagnosi delle perdite di memoria - Dimensione della memoria consentita di # byte esaurita


98

Ho riscontrato il temuto messaggio di errore, forse attraverso uno sforzo scrupoloso, PHP ha esaurito la memoria:

Dimensione della memoria consentita di #### byte esauriti (si è tentato di allocare #### byte) in file.php alla riga 123

Aumentare il limite

Se sai cosa stai facendo e vuoi aumentare il limite vedi memory_limit :

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

Attenti! Potresti solo risolvere il sintomo e non il problema!

Diagnosi della perdita:

Il messaggio di errore indica una linea all'interno di un ciclo che credo stia perdendo o accumulando inutilmente memoria. Ho stampato memory_get_usage()dichiarazioni alla fine di ogni iterazione e posso vedere il numero crescere lentamente fino a raggiungere il limite:

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
}

Ai fini di questa domanda, supponiamo che il peggior codice di spaghetti immaginabile si nasconda in ambito globale da qualche parte in $usero Task.

Quali strumenti, trucchi PHP o voodoo di debug possono aiutarmi a trovare e risolvere il problema?


PS: di recente ho riscontrato un problema con questo tipo esatto di cose. Sfortunatamente, ho anche scoperto che php ha un problema di distruzione di oggetti figlio. Se annulli l'impostazione di un oggetto padre, i suoi oggetti figlio non vengono liberati. Dovendo assicurarmi di utilizzare un unset modificato che includa una chiamata ricorsiva a tutti gli oggetti figlio __destruct e così via. Dettagli qui: paul-m-jones.com/archives/262 :: Sto facendo qualcosa come: function super_unset ($ item) {if (is_object ($ item) && method_exists ($ item, "__destruct")) {$ elemento -> __ destruct (); } unset ($ item); }
Josh

Risposte:


48

PHP non ha un garbage collector. Utilizza il conteggio dei riferimenti per gestire la memoria. Pertanto, la fonte più comune di perdite di memoria sono i riferimenti ciclici e le variabili globali. Se usi un framework, avrai molto codice da esplorare per trovarlo, temo. Lo strumento più semplice consiste nell'effettuare chiamate selettive memory_get_usagee restringere il campo al punto in cui si perde il codice. Puoi anche usare xdebug per creare una traccia del codice. Eseguire il codice con tracce di esecuzione e show_mem_delta.


3
Ma attenzione ... i file di traccia generati saranno ENORME. La prima volta che ho eseguito una traccia xdebug su un'app Zend Framework ci è voluto molto tempo per eseguire e ha generato un file di dimensioni multi GB (non kb o MB ... GB). Basta essere consapevoli di questo.
rg88

1
Sì, è piuttosto pesante .. GB suona un po 'troppo però - a meno che tu non abbia una sceneggiatura grande. Forse prova a elaborare solo un paio di righe (dovrebbe essere sufficiente per identificare la perdita). Inoltre, non installare l'estensione xdebug sul server di produzione.
troelskn

31
Dalla 5.3 PHP ha effettivamente un garbage collector. D'altra parte, la funzione di profilazione della memoria è stata rimossa da xdebug :(
wdev

3
+1 ha trovato la perdita! Una classe che aveva riferimenti ciclici! Una volta che questi riferimenti sono stati unset (), gli oggetti sono stati raccolti come previsto! Grazie! :)
rinogo

@rinogo quindi come hai scoperto la perdita? Puoi condividere quali passi hai fatto?
JohnnyQ

11

Ecco un trucco che abbiamo usato per identificare quali script stanno utilizzando la maggior parte della memoria sul nostro server.

Salva il seguente frammento in un file, ad esempio /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');

Utilizzalo aggiungendo quanto segue a httpd.conf:

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

Quindi analizza il file di registro in /var/log/httpd/php_memory_log

Potrebbe essere necessario touch /var/log/httpd/php_memory_log && chmod 666 /var/log/httpd/php_memory_logprima che il tuo utente web possa scrivere nel file di registro.


8

Ho notato una volta in un vecchio script che PHP manterrebbe la variabile "as" come in scope anche dopo il mio ciclo foreach. Per esempio,

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

Non sono sicuro se le future versioni di PHP abbiano risolto questo problema o meno da quando l'ho visto. Se questo è il caso, puoi unset($user)dopo la doSomething()riga per cancellarlo dalla memoria. YMMV.


13
PHP non ha l'ambito di loop / condizionali come C / Java / ecc. Qualunque cosa dichiarata all'interno di un ciclo / condizionale è ancora nell'ambito anche dopo l'uscita dal ciclo / condizionale (in base alla progettazione [?]). Metodi / funzioni, d'altra parte, hanno l'ambito come ci si aspetterebbe: tutto viene rilasciato una volta terminata l'esecuzione della funzione.
Frank Farmer

Ho pensato che fosse in base alla progettazione. Un vantaggio di questo è che dopo un ciclo puoi lavorare con l'ultimo elemento trovato, ad esempio che soddisfa criteri particolari.
joachim

Potresti unset(), ma tieni presente che per gli oggetti, tutto ciò che stai facendo è cambiare il punto in cui punta la tua variabile - non l'hai effettivamente rimossa dalla memoria. PHP libererà automaticamente la memoria una volta che è comunque fuori portata, quindi la soluzione migliore (in termini di questa risposta, non della domanda dell'OP) è usare funzioni brevi in ​​modo che non si aggrappino anche a quella variabile dal ciclo lungo.
Rich Court

@patcoll Questo non ha nulla a che fare con le perdite di memoria. Questo è semplicemente il cambiamento del puntatore dell'array. Dai un'occhiata qui: prismnet.com/~mcmahon/Notes/arrays_and_pointers.html alla versione 3a.
Harm Smits

7

Ci sono diversi possibili punti di perdita di memoria in php:

  • php stesso
  • estensione php
  • libreria php che usi
  • il tuo codice php

È abbastanza difficile trovare e correggere i primi 3 senza una profonda conoscenza del codice sorgente di reverse engineering o php. Per l'ultimo è possibile utilizzare la ricerca binaria per codice di perdita di memoria con memory_get_usage


91
La tua risposta è quanto di più generale avrebbe potuto ottenere
TravisO

2
È un peccato che anche php 7.2 non siano in grado di riparare le perdite di memoria del core php. Non è possibile eseguire processi a lunga esecuzione in esso.
Aftab Naveed

6

Recentemente ho riscontrato questo problema su un'applicazione, in quelle che ritengo essere circostanze simili. Uno script che viene eseguito nel cli di PHP che esegue un ciclo su molte iterazioni. Il mio script dipende da diverse librerie sottostanti. Sospetto che la causa sia una particolare libreria e ho passato diverse ore invano cercando di aggiungere metodi di distruzione appropriati alle sue classi senza alcun risultato. Di fronte a un lungo processo di conversione in una libreria diversa (che potrebbe rivelarsi avere gli stessi problemi), nel mio caso ho escogitato una rozza soluzione per il problema.

Nella mia situazione, su una cli di Linux, stavo eseguendo un loop su un gruppo di record utente e per ciascuno di essi creavo una nuova istanza di diverse classi che avevo creato. Ho deciso di provare a creare le nuove istanze delle classi usando il metodo exec di PHP in modo che quei processi venissero eseguiti in un "nuovo thread". Ecco un esempio davvero semplice di ciò a cui mi riferisco:

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
}

Ovviamente questo approccio ha dei limiti, e bisogna essere consapevoli dei pericoli di questo, poiché sarebbe facile creare un lavoro da coniglio, tuttavia in alcuni rari casi potrebbe aiutare a superare un punto difficile, fino a quando non si potrebbe trovare una soluzione migliore , come nel mio caso.


6

Ho riscontrato lo stesso problema e la mia soluzione è stata sostituire foreach con un normale per. Non sono sicuro delle specifiche, ma sembra che foreach crei una copia (o in qualche modo un nuovo riferimento) all'oggetto. Usando un ciclo for regolare, accedi direttamente all'elemento.


5

Ti suggerirei di controllare il manuale di php o di aggiungere la gc_enable()funzione per raccogliere la spazzatura ... Questa è la perdita di memoria non influisce sul modo in cui viene eseguito il codice.

PS: php ha un garbage collector gc_enable()che non accetta argomenti.


3

Recentemente ho notato che le funzioni lambda di PHP 5.3 lasciano memoria extra utilizzata quando vengono rimosse.

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

Non sono sicuro del perché, ma sembra che occorra 250 byte in più per ogni lambda anche dopo che la funzione è stata rimossa.


2
Stavo per dire lo stesso. Questo problema è stato risolto il 5.3.10 ( # 60139 )
Kristopher Ives

@KristopherIves, grazie per l'aggiornamento! Hai ragione, questo non è più un problema quindi non dovrei aver paura di usarli come un matto ora.
Xeoncross

2

Se quello che dici su PHP che fa GC solo dopo una funzione è vero, potresti racchiudere il contenuto del ciclo all'interno di una funzione come soluzione alternativa / esperimento.


1
@DavidKullmann In realtà penso che la mia risposta sia sbagliata. Dopotutto, run()ciò che viene chiamato è anche una funzione, alla fine della quale dovrebbe verificarsi il GC.
Bart van Heukelom

2

Un grosso problema che ho avuto è stato usare create_function . Come nelle funzioni lambda, lascia in memoria il nome temporaneo generato.

Un'altra causa di perdite di memoria (in caso di Zend Framework) è Zend_Db_Profiler. Assicurati che sia disabilitato se esegui script in Zend Framework. Ad esempio, nel mio application.ini avevo il seguente:

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

L'esecuzione di circa 25.000 query + carichi di elaborazione precedenti, ha portato la memoria a un bel 128 MB (il mio limite massimo di memoria).

Impostando semplicemente:

resources.db.profiler.enabled    = false

bastava mantenerlo sotto i 20 Mb

E questo script era in esecuzione nella CLI, ma creava un'istanza di Zend_Application ed eseguiva Bootstrap, quindi utilizzava la configurazione di "sviluppo".

Ha davvero aiutato l'esecuzione dello script con la profilazione xDebug


2

Non l'ho visto menzionato esplicitamente, ma xdebug fa un ottimo lavoro nel profiling di tempo e memoria (a partire da 2.6 ). Puoi prendere le informazioni che genera e passarle a un'interfaccia grafica a tua scelta: webgrind (solo tempo), kcachegrind , qcachegrind o altri e genera alberi di chiamate e grafici molto utili per farti trovare le fonti dei tuoi vari guai .

Esempio (di qcachegrind): inserisci qui la descrizione dell'immagine


1

Sono un po 'in ritardo per questa conversazione, ma condividerò qualcosa di pertinente a Zend Framework.

Ho avuto un problema di perdita di memoria dopo aver installato php 5.3.8 (utilizzando phpfarm) per lavorare con un'app ZF sviluppata con php 5.2.9. Ho scoperto che la perdita di memoria veniva innescata nel file httpd.conf di Apache, nella mia definizione di host virtuale, dove dice SetEnv APPLICATION_ENV "development". Dopo aver commentato questa riga, le perdite di memoria si sono interrotte. Sto cercando di trovare una soluzione alternativa in linea nel mio script php (principalmente definendolo manualmente nel file index.php principale).


1
La domanda dice che è in esecuzione nella CLI. Ciò significa che Apache non è affatto coinvolto nel processo.
Maxime

1
@ Maxime Buon punto, non sono riuscito a capirlo, grazie. Vabbè, spero che qualche googler casuale trarrà vantaggio dalla nota che ho lasciato qui comunque, poiché questa pagina mi è venuta fuori mentre cercavo di risolvere il mio problema.
fronzee

Controlla la mia risposta su questa domanda, forse è stato anche il tuo caso.
Andy

La tua applicazione dovrebbe avere configurazioni differenti a seconda dell'ambiente. L' "development"ambiente di solito ha un sacco di registrazione e profilazione che altri ambienti potrebbero non avere. Commentando la riga, la tua applicazione utilizza invece l'ambiente predefinito, che di solito è "production"o "prod". La perdita di memoria esiste ancora; il codice che lo contiene semplicemente non viene chiamato in quell'ambiente.
Marco Roy

0

Non l'ho visto menzionato qui, ma una cosa che potrebbe essere utile è usare xdebug e xdebug_debug_zval ('variableName') per vedere il refcount.

Posso anche fornire un esempio di un'estensione php che si intromette: Z-Ray di Zend Server. Se la raccolta dei dati è abilitata, l'utilizzo della memoria aumenterà a ogni iterazione proprio come se la raccolta dei rifiuti fosse disattivata.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.