web-dev-qa-db-fra.com

Forcer la libération de mémoire dans PHP

Dans un programme PHP, je lis séquentiellement un tas de fichiers (avec file_get_contents), gzdecode eux, json_decode le résultat, analysez le contenu, jetez l'essentiel et stockez environ 1% dans un tableau.

Malheureusement, à chaque itération (je traverse un tableau contenant les noms de fichiers), il semble y avoir une perte de mémoire (selon memory_get_peak_usage, environ 2 à 10 Mo à chaque fois). J'ai revérifié et revérifié mon code; Je ne stocke pas de données inutiles dans la boucle (et les données nécessaires dépassent à peine environ 10 Mo dans l'ensemble), mais je réécris fréquemment (en fait, des chaînes dans un tableau). Apparemment, PHP ne libère pas la mémoire correctement, utilisant ainsi de plus en plus RAM jusqu'à ce qu'il atteigne la limite).

Existe-t-il un moyen de faire une collecte forcée des ordures? Ou, au moins, pour savoir où la mémoire est utilisée?

53
DBa

cela a à voir avec la fragmentation de la mémoire.

Considérez deux chaînes, concaténées à une chaîne. Chaque original doit rester jusqu'à ce que la sortie soit créée. La sortie est plus longue que l'une ou l'autre entrée.
Par conséquent, une nouvelle allocation doit être faite pour stocker le résultat d'une telle concaténation. Les chaînes d'origine sont libérées mais ce sont de petits blocs de mémoire.
En cas de 'str1' . 'str2' . 'str3' . 'str4' vous avez créé plusieurs intérimaires à chaque fois. - et aucun d'entre eux ne rentre dans l'espace qui a été libéré. Les chaînes ne sont probablement pas disposées dans la mémoire contiguë (c'est-à-dire que chaque chaîne l'est, mais les différentes chaînes ne sont pas disposées bout à bout) en raison d'autres utilisations de la mémoire. La libération de la chaîne crée donc un problème car l'espace ne peut pas être réutilisé efficacement. Vous grandissez donc avec chaque tmp que vous créez. Et vous ne réutilisez jamais rien.

En utilisant l'implode basé sur un tableau, vous ne créez qu'une seule sortie - exactement la longueur dont vous avez besoin. Exécution d'une seule allocation supplémentaire. Il est donc beaucoup plus efficace en mémoire et ne souffre pas de la fragmentation de la concaténation. Il en va de même pour python. Si vous devez concaténer des chaînes, plus d'une concaténation doit toujours être basée sur un tableau:

''.join(['str1','str2','str3'])

en python

implode('', array('str1', 'str2', 'str3'))

en PHP

les équivalents sprintf sont également très bien.

La mémoire rapportée par memory_get_peak_usage est fondamentalement toujours le "dernier" bit de mémoire dans la carte virtuelle qu'il a dû utiliser. Ainsi, depuis sa croissance constante, il fait état d'une croissance rapide. Comme chaque allocation tombe "à la fin" du bloc de mémoire actuellement utilisé.

38
James Lyons

Dans PHP> = 5.3.0, vous pouvez appeler gc_collect_cycles() pour forcer une passe GC.

Remarque: Vous devez activer zend.enable_gc Dans votre php.ini Ou appeler gc_enable() pour activer le collecteur de référence circulaire.

19
Mo.

J'ai trouvé la solution: c'était une concaténation de chaînes. Je générais l'entrée ligne par ligne en concaténant certaines variables (la sortie est un fichier CSV). Cependant, PHP ne semble pas libérer la mémoire utilisée pour l'ancienne copie de la chaîne, encombrant ainsi efficacement RAM avec des données inutilisées. Passer à un tableau basé sur un tableau (et l'imploser avec des virgules juste avant de le placer dans le fichier externe) a contourné ce comportement.

Pour une raison quelconque - pas évidente pour moi - PHP a signalé l'augmentation de l'utilisation de la mémoire pendant les appels json_decode, ce qui m'a induit en erreur en supposant que la fonction json_decode était le problème.

12
DBa

J'ai trouvé que le gestionnaire de mémoire interne de PHP est le plus susceptible d'être appelé à la fin d'une fonction. Sachant cela, j'ai refactorisé le code dans une boucle comme ceci:

while (condition) {
  // do
  // cool
  // stuff
}

à

while (condition) {
  do_cool_stuff();
}

function do_cool_stuff() {
  // do
  // cool
  // stuff
}

MODIFIER

J'ai exécuté ce test rapide et je n'ai pas vu d'augmentation de l'utilisation de la mémoire. Cela m'amène à croire que la fuite n'est pas dans json_decode()

for($x=0;$x<10000000;$x++)
{
  do_something_cool();
}

function do_something_cool() {
  $json = '{"a":1,"b":2,"c":3,"d":4,"e":5}';
  $result = json_decode($json);
  echo memory_get_peak_usage() . PHP_EOL;
}
9
Mike B

Appelez memory_get_peak_usage() après chaque instruction et assurez-vous unset() tout ce que vous pouvez. Si vous itérez avec foreach(), utilisez une variable référencée pour éviter de faire une copie de l'original ( foreach () ).

foreach( $x as &$y)

Si PHP fuit réellement de la mémoire, un ramassage de mémoire forcé ne fera aucune différence.

Il y a un bon article sur PHP fuites de mémoire et leur détection à IBM

6
Andy

Je viens d'avoir le même problème et j'ai trouvé une solution de contournement possible.

SITUATION: J'écrivais à partir d'une requête db dans des fichiers csv. J'ai toujours alloué une ligne $, puis je l'ai réaffectée à l'étape suivante. La suppression de $ row n'a pas aidé; mettre une chaîne de 5 Mo dans $ row en premier (pour éviter la fragmentation) n'a pas aidé; la création d'un tableau de $ row-s (chargement de nombreuses lignes dans celui-ci + désarmement de l'ensemble à chaque 5000ème étape) n'a pas aidé; vraiment essayé deux ou trois choses.

MAIS.

Lorsque j'ai créé une fonction distincte qui ouvre le fichier, transfère 100 000 lignes (juste assez pour ne pas manger toute la mémoire) et ferme le fichier, PUIS j'ai fait des appels ultérieurs à cette fonction (en ajoutant au fichier existant), j'ai trouvé que pour chaque sortie de fonction, PHP supprimait les ordures. C'était une chose à espace variable local.

CONCLUSION: Chaque fois que votre fonction se termine, elle libère toutes les variables locales.

C'est la règle, autant que je sache. Une seule note latérale cependant: lorsque j'ai essayé de faire en sorte que ma fonction "do_only_a_smaller_subset ()" récupère certaines variables par référence (à savoir l'objet de requête et le pointeur de fichier), le garbage collection ne s'est pas produit. Maintenant peut-être que je me méprends sur quelque chose et peut-être que l'objet de requête (mysqli) fuyait, eh bien, je ne sais pas. Cependant, comme il a été passé par ref, il n'a évidemment pas pu être nettoyé car il existait après le point de sortie de la petite fonction.

Donc, ça vaut le coup d'essayer! Cela m'a sauvé la journée de le découvrir.

6
dkellner

J'allais dire que je ne m'attendrais pas nécessairement à ce que gc_collect_cycles () résout le problème - car les fichiers ne sont probablement plus mappés en zvars. Mais avez-vous vérifié que gc_enable a été appelé avant de charger des fichiers?

J'ai remarqué que PHP semble engloutir la mémoire en faisant des inclusions - beaucoup plus que ce qui est requis pour la source et le fichier tokenisé - cela peut être un problème similaire. Je ne dis pas que c'est un bug cependant.

Je crois qu'une solution de contournement serait de ne pas utiliser file_get_contents mais plutôt fopen () .... fgets () ... fclose () plutôt que de mapper le fichier entier en mémoire en une seule fois. Mais vous devrez l'essayer pour confirmer.

HTH

C.

4
symcbean

Il y a eu récemment problème similaire avec System_Daemon . Aujourd'hui, j'ai isolé mon problème pour file_get_contents.

Pourriez-vous essayer d'utiliser fread à la place? Je pense que cela peut résoudre votre problème. Si c'est le cas, il est probablement temps de faire un rapport de bogue sur PHP.

3
kvz