Les références circulaires en PHP sont une cause fréquente de fuites de mémoire. Les références circulaires se produisent lorsque des objets se réfèrent les uns aux autres, directement ou indirectement. Heureusement, PHP dispose d'un garbage collector capable de détecter et de nettoyer les références circulaires. Cependant, cela consomme des cycles CPU et peut ralentir l'application.
Le garbage collector est déclenché lorsqu'il y a 10 000 objets de boucle ou tableaux possibles en mémoire et que l'un d'eux sort de la portée.
Si vous disposez d'un petit nombre d'objets qui utilisent beaucoup de mémoire, le garbage collection ne sera jamais déclenché. Vous pouvez atteindre la limite de mémoire même si la mémoire est utilisée par des objets orphelins que le garbage collector est censé collecter.
C'est pourquoi vous devez identifier les situations qui créent des références circulaires et les éviter.
Idéalement, pour les applications Web, vous souhaitez désactiver le garbage collector et laisser PHP libérer toute la mémoire après l'envoi de la réponse. Mais cela est dangereux pour les scripts à exécution longue tels que les démons ou les processus de travail, car les fuites de mémoire peuvent s'accumuler au fil du temps et ralentir l'application en raison d'appels fréquents au garbage collector.
Dans cet article, nous explorerons comment les fermetures et les générateurs enregistrent les références circulaires et comment les empêcher.
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { public function __construct(public A $a) {} }</code>
Dans cet exemple, A et B se réfèrent l'un à l'autre. Lorsque vous créez une instance de A, cela crée une instance de B qui fait référence à A. Cela crée une référence circulaire.
Pour détecter les références circulaires, nous pouvons déclencher manuellement le garbage collector en utilisant gc_collect_cycles()
et lire le nombre de références collectées en utilisant gc_status()
.
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status());</code>
Cela affichera :
<code>Array ( ... [collected] => 2 ... )</code>
Cet exemple montre que le garbage collector a détecté et supprimé 2 objets avec des références circulaires.
Vous pouvez également utiliser la fonction xdebug_debug_zval()
pour afficher le nombre de références à un objet.
Lorsque vous rencontrez des références circulaires, une solution simple consiste à utiliser des références faibles. Une référence faible est un objet qui contient une référence qui n'empêche pas le ramasse-miettes de collecter l'objet auquel il fait référence. En PHP, vous pouvez créer des références faibles en utilisant la classe WeakReference
.
Cela nécessite quelques modifications du code. La classe B stocke désormais les objets WeakReference
au lieu des objets A. Vous devez accéder à l'objet A en utilisant la méthode WeakReference
de l'objet get()
.
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { /** @var WeakReference<a> $a */ public WeakReference $a; public function __construct(A $a) { $this->a = WeakReference::create($a); } }</code>
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 0</code>
Dans le résultat, vous verrez que le nombre de citations collectées est désormais de 0.
Astuce 1 : Utilisez des références faibles uniquement lorsque cela est nécessaire pour éviter les références circulaires.
Le concept de fermeture en PHP est de créer une fonction qui peut accéder aux variables dans la portée parent. Cela peut conduire à des références circulaires si vous n'y faites pas attention.
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { public function __construct(public A $a) {} }</code>
Dans cet exemple, la fermeture $a->b
fait référence à une variable $a
dans la portée parent. Les références circulaires sont faciles à repérer car les références sont sans ambiguïté.
Cependant, le même problème peut survenir de manière plus subtile si vous utilisez la syntaxe abrégée des fermetures. Avec les fonctions fléchées, la variable $a
n'est pas explicitement référencée dans la fermeture, mais elle est toujours capturée par référence.
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status());</code>
Dans cet exemple, le nombre de références collectées est de 2, indiquant une référence circulaire.
Toute fermeture non statique créée dans une méthode de classe aura une référence à l'instance d'objet ($this
) même si $this
n'est pas accessible.
<code>Array ( ... [collected] => 2 ... )</code>
C'est parce que $this
les références sont toujours capturées par référence dans les fermetures. Il est accessible en utilisant Reflection::getClosureThis()
.
<code class="language-php">class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { /** @var WeakReference<a> $a */ public WeakReference $a; public function __construct(A $a) { $this->a = WeakReference::create($a); } }</code>
Si la fermeture est créée à partir de la portée globale ou d'une méthode statique, la référence $this
est nulle.
Astuce 2 : Si vous n'avez pas besoin de
$this
, utilisez toujoursstatic function () {}
oustatic fn () =>
pour créer une fermeture.
Parlons de la raison de cet article. J'ai récemment découvert quelque chose : Les générateurs conservent les références tant qu'elles ne sont pas épuisées.
Dans cet exemple, la classe stocke le générateur dans une propriété, mais le générateur a une $this
référence à l'instance d'objet.
Un générateur se comporte comme une fermeture et contient une référence à l'instance d'objet.
<code class="language-php">// 创建的对象但未分配给变量 new A(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 0</code>
L'instance de classe est collectée par le garbage collector car elle a une référence au générateur, qui a une référence à l'instance d'objet.
Une fois le générateur épuisé, la référence est libérée et l'instance d'objet est supprimée de la mémoire.
<code class="language-php">function createCircularReference() { $a = new stdClass(); $a->b = function () use ($a) { return $a; }; return $a; }</code>
Astuce 3 : Épuisez toujours le générateur par itération.
Astuce 4 : Utilisez des méthodes statiques ou des fermetures pour créer des générateurs afin d'éviter de conserver des références à des instances d'objet.
Les références circulaires sont une cause fréquente de fuites de mémoire en PHP. Même si le garbage collector peut détecter et nettoyer les références circulaires, il consomme des cycles CPU et peut ralentir l'application. Vous devez détecter les situations qui créent de telles références circulaires et ajuster votre code pour les éviter. L'utilisation de références faibles peut empêcher les cycles de référence, mais quelques conseils simples peuvent vous aider à les éviter :
$this
n'est pas requis, utilisez static function () {}
ou static fn () =>
pour créer une fermeture. Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!