# Speicherverwaltung in PHP: Referenzzählung und Garbage Collection

> PHP-Speicherverwaltung verständlich: Referenzzählung, Copy-on-Write, Zyklen und Garbage Collector, Messfunktionen und warum PHP2Shell genau hier ansetzt.

Quelle: https://phpsos.de/php/speicherverwaltung/  
Stand: 25.10.2026  
Anbieter: PHP SOS, PHP-Notfallhilfe und Server-Sicherheit. Soforthilfe per WhatsApp: https://wa.me/491736494914?text=Hallo%20PHP%20SOS%2C%20wir%20brauchen%20Hilfe%20mit%20unserem%20PHP-Server.%20Firma%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**Kurz gesagt:** PHP gibt Speicher vor allem über Referenzzählung frei: Jeder Wert weiß, wie viele Variablen auf ihn zeigen, und wird gelöscht, sobald dieser Zähler null erreicht. Für Kreisbezüge, die so nie freigegeben würden, gibt es einen Zyklen-Garbage-Collector. Am Ende jeder Anfrage räumt PHP zusätzlich alles auf, was die Anfrage belegt hat. Beide PHP2Shell-Lücken setzen an Speicherzuständen an: an einem Out-of-Memory-Zustand während der Anfrageverarbeitung und an internem Zustand, der nach dem Freigeben zu lange bestehen bleibt.

## Wie PHP Speicher verwaltet: der Überblick

In Sprachen wie C muss der Programmierer Speicher selbst anfordern und wieder freigeben. PHP nimmt das ab. Jede Variable, jedes Array und jedes Objekt belegt Speicher, den PHP über seinen eigenen Speichermanager verwaltet. Die Grenze dafür setzt `memory_limit`, laut Handbuch standardmäßig `128M`.

Für Entscheider ist ein Punkt zentral: Bei klassischem Betrieb mit PHP-FPM oder mod_php laufen langlebige Worker-Prozesse, die nacheinander viele Anfragen bearbeiten. Was eine Anfrage hinterlässt, muss vor der nächsten zuverlässig aufgeräumt sein. Genau an dieser Stelle liegt eine der PHP2Shell-Lücken.

## Referenzzählung: zählen, wer einen Wert benutzt

PHP speichert jeden Wert in einem Container, dem sogenannten zval. Neben Typ und Inhalt führt er laut Handbuch einen Referenzzähler (`refcount`) und eine Markierung, ob der Wert Teil einer PHP-Referenz mit `&` ist (`is_ref`).

```php
<?php
$a = str_repeat('x', 1000);  // ein Wert, refcount 1
$b = $a;                     // kein Kopieren, refcount 2
$b .= 'y';                   // erst jetzt wird kopiert (Copy-on-Write)
unset($a);                   // refcount des Originals sinkt
// Erreicht ein Zähler 0, gibt PHP den Speicher sofort frei.
```

Das Prinzip heißt Copy-on-Write: PHP kopiert einen Wert erst, wenn eine der Variablen ihn verändert. Das spart viel Speicher. Den Zähler einer Variablen kann man mit der Erweiterung Xdebug und der Funktion `xdebug_debug_zval()` sichtbar machen. Das Handbuch weist darauf hin, dass die genauen Zahlen je PHP-Version abweichen können.

## Zyklen: wenn Referenzzählung nicht reicht

Referenzzählung hat eine bekannte Schwäche. Zeigt eine Struktur auf sich selbst, sinkt ihr Zähler nie auf null.

```php
<?php
$a = ['eins'];
$a[] = &$a;   // das Array enthält eine Referenz auf sich selbst
unset($a);    // niemand kann mehr darauf zugreifen, aber refcount bleibt 1
```

Laut Handbuch kommen solche Kreisbezüge besonders bei Objekten vor, etwa in Eltern-Kind-Strukturen oder Parsern. Einzelne Lecks sind harmlos, weil PHP am Ende der Anfrage aufräumt. In lang laufenden Prozessen wie Warteschlangen-Workern oder großen Testläufen können sich Millionen davon aber zu einem echten Speicherproblem summieren.

## Der Zyklen-Garbage-Collector

Seit PHP 5.3.0 verwendet PHP einen Algorithmus nach Bacon und Rajan, um solche Zyklen zu finden. Die Grundidee: Ein Zyklus kann nur entstehen, wenn ein Zähler auf einen Wert größer als null sinkt. Nur diese Werte merkt sich PHP als mögliche Wurzeln.

1. Mögliche Wurzeln landen in einem Puffer. Das Handbuch nennt als Standardgröße 10.000 Einträge; den aktuell gültigen Schwellenwert zeigt `gc_status()` im Feld `threshold`.
2. Ist der Puffer voll, durchläuft PHP alle erreichbaren Werte und verringert probeweise deren Zähler.
3. In einem zweiten Durchgang gilt: Werte mit Zähler null sind Müll, bei allen anderen wird die Verringerung rückgängig gemacht.
4. Die Wurzeln werden aus dem Puffer entfernt und alle als Müll erkannten Werte freigegeben.

Gesteuert wird das über `zend.enable_gc` (standardmäßig an) oder zur Laufzeit mit `gc_enable()` und `gc_disable()`. `gc_collect_cycles()` erzwingt einen Lauf und gibt die Zahl der eingesammelten Zyklen zurück.

## Speicher messen und beobachten

| Funktion | Zweck |
|---|---|
| `memory_get_usage()` | aktuell vom Skript belegter Speicher |
| `memory_get_usage(true)` | vom Speichermanager beim System reservierter Speicher; gegen diesen Wert wird `memory_limit` geprüft |
| `memory_get_peak_usage()` | Höchststand des belegten Speichers |
| `gc_status()` | Läufe, eingesammelte Zyklen, Schwellenwert, Wurzeln; seit PHP 8.3.0 auch Laufzeiten |
| `gc_collect_cycles()` | Zyklensammlung sofort auslösen |
| `WeakMap` | Zuordnung zu Objekten, ohne deren Freigabe zu verhindern, seit PHP 8.0 |

Laut Handbuch erfasst `memory_get_usage()` nur Speicher, den PHP über den eigenen Speichermanager anfordert. Was Erweiterungen direkt beim Betriebssystem holen, fehlt in diesen Zahlen. Für die Speichergrenze des gesamten Prozesses sind deshalb Betriebssystem oder Container zuständig.

## Speicherverwaltung und PHP2Shell

Die Beschreibungen beider Lücken führen direkt in dieses Thema. Bei CVE-2026-109618 kann ein Out-of-Memory-Zustand PHP unterbrechen, während Anfragedaten nur teilweise initialisiert sind; beim Aufräumen können ungültige Daten dann als gültig behandelt werden. Was bei einem Speicherüberlauf intern passiert, erklärt die Seite [memory_limit und Out-of-Memory](https://phpsos.de/php/out-of-memory/). Bei CVE-2026-109619 kann interner Zustand einer Anfrage nach dem Freigeben des Speichers bestehen bleiben und von einer späteren Anfrage im selben Worker genutzt werden, bevor er zurückgesetzt wird. Den Ablauf von Start bis Aufräumen beschreibt die Seite [Der Request-Lebenszyklus](https://phpsos.de/php/request-lebenszyklus/).

Als Übergangsmaßnahme empfiehlt php.net, `memory_limit=-1` so zu setzen, dass Anwendungen es nicht überschreiben können, etwa mit `php_admin_value` im FPM-Pool, und den Speicher stattdessen über Container oder cgroups zu begrenzen, nicht über `RLIMIT_AS` oder `RLIMIT_DATA`. Wie das mit systemd geht, zeigt der Beitrag [Speichergrenzen mit systemd und cgroups setzen](https://phpsos.de/blog/speichergrenzen-systemd-cgroups/).

```ini
; FPM-Pool: Übergangsmaßnahme laut php.net, danach alle Worker neu starten
php_admin_value[memory_limit] = -1
```

> **Wichtig:**
> Ohne Grenze über Container oder cgroup kann ein einzelner Prozess den Server auslasten. Und keine Einstellung ersetzt das Update auf PHP 8.5.12, 8.4.27, 8.3.36 oder 8.2.35 samt Neustart aller Worker, denn nur der Neustart entfernt alte Prozesse aus dem Speicher.

## Wie hilft PHP SOS konkret?

- Wir prüfen die wirksamen Speichereinstellungen je Pool und ob Anwendungen sie überschreiben können.
- Wir setzen die Übergangsmaßnahmen mit einer Speichergrenze auf Ebene von Container oder cgroup um und testen die Last.
- Wir spielen das Update ein, starten alle Worker neu und weisen nach, dass keine alten Prozesse mehr laufen, im Rahmen unseres [PHP-Patch-Service](https://phpsos.de/leistungen/php-patch-service/).
- Wir werten Speicherfehler und abgebrochene Worker in den Logs ab dem 20.10.2026 aus.


## Häufige Fragen

**Was ist Referenzzählung in PHP?**  
PHP zählt für jeden Wert, wie viele Variablen oder Elemente auf ihn verweisen. Sinkt der Zähler auf null, wird der Speicher sofort freigegeben. So wird der meiste Speicher ohne eigenen Aufräumlauf zurückgegeben.

**Wozu braucht PHP dann noch einen Garbage Collector?**  
Für Zyklen. Verweist ein Array auf sich selbst oder kennen sich zwei Objekte gegenseitig, sinkt der Zähler nie auf null, auch wenn niemand mehr darauf zugreift. Der Zyklen-Garbage-Collector findet und entfernt solche Strukturen.

**Gibt PHP den Speicher am Ende einer Anfrage frei?**  
Ja, Speicher, den ein Skript während einer Anfrage belegt, räumt PHP am Ende der Anfrage auf. Bei PHP2Shell betrifft CVE-2026-109619 aber genau den Fall, dass interner Zustand nach dem Freigeben bestehen bleiben und von einer späteren Anfrage im selben Worker genutzt werden kann.

**Warum empfiehlt php.net memory_limit=-1 als Übergangsmaßnahme?**  
Die Lücke CVE-2026-109618 hängt mit einem Out-of-Memory-Zustand zusammen. php.net empfiehlt, memory_limit=-1 so zu setzen, dass Anwendungen es nicht überschreiben können, und den Speicher stattdessen über Container oder cgroups zu begrenzen. Das ist kein vollständiger Schutz und ersetzt das Update nicht.

## Quellen

- [PHP-Handbuch: Grundlagen der Referenzzählung](https://www.php.net/manual/de/features.gc.refcounting-basics.php)
- [PHP-Handbuch: Einsammeln von Zyklen](https://www.php.net/manual/de/features.gc.collecting-cycles.php)
- [PHP-Handbuch: gc_status](https://www.php.net/manual/de/function.gc-status.php)
- [PHP-Handbuch: memory_get_usage](https://www.php.net/manual/en/function.memory-get-usage.php)
- [php.net: Sicherheitsmeldungen](https://www.php.net/)
