PHP Garbage Collection entscheidet oft darüber, ob ein Hosting-Stack unter Last flüssig läuft oder in Latenzspitzen kippt. Ich zeige, wie der Collector Ausführungszeit frisst, wo er Speicher rettet und wie ich durch zielgerichtetes Tuning messbar schnellere Antworten erreiche.
Zentrale Punkte
Diese Übersicht fasse ich in wenigen Kernaussagen zusammen, damit du sofort an den Stellschrauben drehst, die wirklich zählen. Ich priorisiere Messbarkeit, weil ich so Entscheidungen sauber validiere und nicht im Dunkeln tappe. Ich berücksichtige Hosting-Parameter, da sie die Wirkung der GC-Einstellungen stark prägen. Ich bewerte Risiken wie Leaks und Stalls, denn sie entscheiden über Stabilität und Geschwindigkeit. Ich nutze aktuelle PHP-Versionen, weil Verbesserungen ab PHP 8+ die GC-Last spürbar senken.
- Trade-off: Weniger GC-Läufe spart Zeit, mehr RAM puffert Objekte.
- FPM-Tuning: pm.max_children und pm.max_requests steuern Langlebigkeit und Leaks.
- OpCache: Weniger Compiles reduziert Druck auf Allocator und GC.
- Sessions: SGC per Cron entlastet Requests spürbar.
- Profiling: Blackfire, Tideways und Xdebug zeigen echte Hotspots.
Wie der Garbage Collector in PHP arbeitet
PHP nutzt Referenzzählung für die meisten Variablen und übergibt Zyklen an den Garbage Collector. Ich beobachte, wie der Collector zyklische Strukturen markiert, Wurzeln prüft und Speicher freigibt. Er läuft nicht bei jedem Request, sondern basierend auf Triggern und interner Heuristik. In PHP 8.5 verringern Optimierungen die Menge potenziell sammelbarer Objekte, was selteneres Scannen bedeutet. Ich setze gc_status() ein, um Läufe, gesammelte Bytes und Root-Buffer zu kontrollieren.
Trigger und Heuristiken verstehen
In der Praxis startet die Sammlung, wenn der interne Root-Buffer eine Schwelle überschreitet, beim Request-Shutdown oder wenn ich explizit gc_collect_cycles() aufrufe. Lange Objektketten mit zyklischen Referenzen füllen den Root-Buffer schneller. Das erklärt, warum bestimmte Workloads (ORM-Heavy, Event-Dispatcher, Closures mit $this-Captures) signifikant mehr GC-Aktivität zeigen als einfache Skripte. Neuere PHP-Versionen reduzieren die Anzahl der in den Root-Buffer aufgenommenen Kandidaten, was die Frequenz spürbar senkt.
Gezielt steuern statt blind deaktivieren
Ich deaktiviere die Sammlung nicht pauschal. In Batch-Jobs oder CLI-Workern lohnt es sich jedoch, den GC temporär auszuschalten (gc_disable()), den Job durchzurechnen und am Ende gc_enable() plus gc_collect_cycles() auszuführen. Für FPM-Webrequests bleibt zend.enable_gc=1 meine Standardeinstellung – andernfalls riskiere ich versteckte Leaks mit wachsendem RSS.
Performance-Einfluss unter Last
Profiling zeigt in Projekten regelmäßig 10–21% Ausführungszeit für die Sammlung, abhängig von Objektgraphen und Workload. In einzelnen Workflows lag die Einsparung durch temporäres Deaktivieren bei Dutzenden Sekunden, während der RAM-Verbrauch moderat stieg. Ich bewerte deshalb immer den Tausch: Zeit gegen Speicher. Häufige GC-Trigger erzeugen Stalls, die sich bei hohem Traffic häufen. Sauber dimensionierte Prozesse reduzieren solche Peaks und halten Latenzen stabil.
Tail-Latenzen glätten
Ich messe nicht nur den Mittelwert, sondern p95–p99. Genau dort schlagen GC-Stalls zu, weil sie mit Peaks im Objektgraphen zusammenfallen (z. B. nach Cache-Misses oder Cold-Starts). Maßnahmen wie größerer opcache.interned_strings_buffer, weniger String-Duplizierung und kleinere Batches drücken die Objektzahl pro Request – und damit die Varianz.
PHP Memory Management im Detail
Referenzen und Zyklen bestimmen, wie Speicher fließt und wann der Collector eingreift. Ich vermeide globale Variablen, weil sie Lebenszeit verlängern und den Graphen wachsen lassen. Generatoren statt großer Arrays drücken Spitzenlast und halten die Sammlungen kleiner. Zusätzlich prüfe ich Memory-Fragmentierung, weil zerklüfteter Heap die effektive Nutzung von RAM schwächt. Gute Scopes und das Freigeben großer Strukturen nach der Nutzung halten die Sammlung effizient.
Typische Quellen für Zyklen
- Closures, die $this capturen, während das Objekt wiederum Listener hält.
- Event-Dispatcher mit langlebigen Listener-Listen.
- ORMs mit bidirektionalen Relationen und Unit-of-Work-Caches.
- Globale Caches in PHP (Singletons), die Referenzen halten und Scopes aufblähen.
Ich breche solche Zyklen gezielt: schwächere Kopplung, Lifecycle-Reset nach Batches, bewusste unset() auf großen Strukturen. Wo passend, nutze ich WeakMap oder WeakReference, damit temporäre Objekt-Caches nicht zur Dauerlast werden.
CLI-Worker und Langläufer
Bei Queues oder Daemons steigt die Bedeutung von zyklischer Bereinigung. Ich sammle nach N Jobs (N je nach Payload 50–500) via gc_collect_cycles() und beobachte den RSS-Verlauf. Steigt er trotz Sammlung, plane ich einen selbständigen Neustart des Workers ab einem Schwellenwert. Das spiegelt die FPM-Logik von pm.max_requests in der CLI-Welt.
FPM- und OpCache-Tuning, das GC entlastet
PHP-FPM bestimmt, wie viele Prozesse parallel leben und wie lange sie existieren. Ich kalkuliere pm.max_children grob als (Gesamt-RAM − 2 GB) / 50 MB pro Prozess und passe mit realen Messwerten an. Über pm.max_requests recycele ich Prozesse regelmäßig, sodass Leaks keine Chance haben. OpCache senkt Compile-Overhead und mindert String-Duplizierung, was das Allocationsvolumen und damit den Druck auf die Sammlung senkt. Details feile ich an der OpCache-Konfiguration und beobachte Trefferquoten, Restarts und interned Strings.
Process Manager: dynamic vs. ondemand
pm.dynamic hält Worker warm und federt Lastspitzen mit geringer Wartezeit ab. pm.ondemand spart RAM in Phasen niedriger Last, startet aber Prozesse bei Bedarf – die Startzeit kann sich in p95 bemerkbar machen. Ich wähle das Modell passend zur Lastkurve und teste, wie sich der Wechsel auf Tail-Latenzen auswirkt.
Beispielrechnung und Grenzen
Als Startpunkt ergibt (RAM − 2 GB) / 50 MB schnell hohe Werte. Auf einem 16-GB-Host wären das ca. 280 Worker. CPU-Kerne, externe Abhängigkeiten und tatsächlicher Prozess-Footprint begrenzen die Realität. Ich kalibriere mit Messdaten (RSS pro Worker unter Peak-Payload, p95 Latenzen) und lande oft deutlich niedriger, um CPU und IO nicht zu überfahren.
OpCache-Details mit GC-Effekt
- interned_strings_buffer: Höher ansetzen reduziert String-Duplizierung im Userland und damit Allocation-Druck.
- memory_consumption: Genügend Platz verhindert Code-Eviction, senkt Recompiles und beschleunigt Warmstarts.
- Preloading: Vorab geladene Klassen reduzieren Autoload-Overhead und temporäre Strukturen – mit Bedacht dimensionieren.
Empfehlungen auf einen Blick
Diese Tabelle bündelt Startwerte, die ich anschließend mit Benchmarks und Profiler-Daten feinjustiere. Ich passe Zahlen an konkrete Projekte an, da Payloads stark variieren. Die Werte liefern einen sicheren Einstieg ohne Ausreißer. Nach dem Ausrollen halte ich ein Lasttest-Fenster offen und reagiere auf Metriken. So bleibt die GC-Last unter Kontrolle und die Antwortzeit kurz.
| Kontext | Schlüssel | Startwert | Hinweis |
|---|---|---|---|
| Process Manager | pm.max_children | (RAM − 2 GB) / 50 MB | RAM gegen Concurrency abwägen |
| Process Manager | pm.start_servers | ≈ 25% von max_children | Warmstart für Peak-Phasen |
| Process Lifecycle | pm.max_requests | 500–5.000 | Recycling reduziert Leaks |
| Memory | memory_limit | 256–512 MB | Zu klein fördert Stalls |
| OpCache | opcache.memory_consumption | 128–256 MB | Hohe Hit-Rate spart CPU |
| OpCache | opcache.interned_strings_buffer | 16–64 | Strings teilen senkt RAM |
| GC | zend.enable_gc | 1 | Messbar lassen, nicht blind deaktivieren |
Session Garbage Collection gezielt steuern
Sessions besitzen eine eigene Entsorgung, die bei Standard-Setups Zufall nutzt. Ich deaktiviere die Wahrscheinlichkeit über session.gc_probability=0 und rufe den Aufräumer per Cron auf. So blockiert kein Nutzer-Request das Löschen tausender Dateien. Die Laufzeit plane ich alle 15–30 Minuten, abhängig von session.gc_maxlifetime. Entscheidender Vorteil: Die Web-Antwortzeit bleibt glatt, während der Cleanup zeitlich entkoppelt passiert.
Session-Design und GC-Druck
Ich halte Sessions klein und serialisiere keine großen Objektbäume hinein. Extern gespeicherte Sessions mit niedriger Latenz glätten den Request-Pfad, weil Dateizugriffe und Aufräumläufe keinen Backlog im Webtier erzeugen. Wichtig ist, die Lebenszeit (session.gc_maxlifetime) an das Nutzungsverhalten zu koppeln und Bereinigungsläufe mit Off-Peak-Fenstern zu synchronisieren.
Profiling und Monitoring: Zahlen statt Bauchgefühl
Profiler wie Blackfire oder Tideways zeigen, ob die Sammlung wirklich bremst. Ich vergleiche Läufe mit aktivem GC und mit zeitweiser Deaktivierung in einem isolierten Job. Xdebug liefert GC-Statistiken, die ich für tiefergehende Analysen heranziehe. Wichtige Kennzahlen sind Anzahl der Läufe, gesammelte Zyklen und Zeit pro Zyklus. Mit wiederholten Benchmarks sichere ich mich gegen Ausreißer ab und treffe belastbare Entscheidungen.
Mess-Playbook
- Baseline ohne Änderungen aufnehmen: p50/p95, RSS pro Worker, gc_status()-Werte.
- Eine Variable ändern (z. B. pm.max_requests oder interned_strings_buffer), erneut messen.
- Vergleich mit identischer Datenmenge und Warmlauf, mindestens 3 Wiederholungen.
- Rollout in Stufen, Monitoring eng anlegen, schnelle Reversibilität sicherstellen.
Grenzwerte, memory_limit und RAM-Kalkulation
memory_limit setzt den Deckel pro Prozess und beeinflusst die Häufigkeit von Sammlungen indirekt. Ich plane zuerst den realen Footprint: Baseline, Peaks, plus OpCache und C-Extensions. Dann wähle ich einen Deckel mit Luft für kurzzeitige Lastspitzen, typischerweise 256–512 MB. Für Details zum Zusammenspiel verweise ich auf den Beitrag zu PHP memory_limit, der die Seiteneffekte transparent macht. Ein sinnvolles Limit beugt Out-of-Memory-Fehlern vor, ohne die GC-Last unnötig zu erhöhen.
Container- und NUMA-Einflüsse
In Containern zählt der cgroup-Deckel, nicht nur der Host-RAM. Ich richte memory_limit und pm.max_children auf die Containergrenze aus und halte Sicherheitsabstände ein, damit der OOM-Killer nicht zuschlägt. Bei großen Hosts mit NUMA achte ich darauf, Prozesse nicht zu dicht zu packen, um Speicherzugriffe konsistent schnell zu halten.
Architektur-Tipps für High-Traffic
Skalierung löse ich in Stufen: erst Prozessparameter, dann horizontale Verteilung. Read-heavy Workloads profitieren stark von OpCache und kurzer Startzeit. Für Schreibpfade kapsle ich teure Operationen asynchron, damit der Request leicht bleibt. Caching nah an PHP reduziert Objektmengen und damit den Prüfaufwand der Sammlung. Gute Hoster mit starkem RAM und sauberem FPM-Setup, etwa webhoster.de, erleichtern diesen Ansatz erheblich.
Code- und Build-Aspekte mit GC-Auswirkung
- Composer-Autoloader optimieren: Weniger Dateizugriffe, kleinere temporäre Arrays, stabilere p95.
- Payload klein halten: DTOs statt riesiger Arrays, Streaming statt Bulk.
- Strikte Scopes: Funktions- statt Datei-Scope, Variablen nach Gebrauch freigeben.
Diese scheinbaren Kleinigkeiten reduzieren Allokationen und Zyklengrößen – das wirkt sich direkt auf die Arbeit des Collectors aus.
Fehlerbilder und Anti-Pattern
Symptome erkenne ich an Zickzack-Latenzen, schubweise CPU-Spitzen und wachsenden RSS-Werten pro FPM-Worker. Häufige Ursachen sind große Arrays als Sammelbehälter, globale Caches in PHP und fehlende Prozessrestarts. Auch Session-Cleanup im Request-Pfad verursacht schleppende Antworten. Ich begegne dem mit Generatoren, kleineren Batches und klaren Lifecycles. Zusätzlich prüfe ich, ob externe Services Retries auslösen, die verdeckte Objektfluten erzeugen.
Praxis-Checkliste
- gc_status() regelmäßig loggen: Läufe, Zeit pro Lauf, Root-Buffer-Auslastung.
- pm.max_requests so wählen, dass RSS stabil bleibt.
- interned_strings_buffer hoch genug, um Duplikate zu vermeiden.
- Batchgrößen so schneiden, dass keine massiven Spitzengraphen entstehen.
- Sessions entkoppelt bereinigen, nicht im Request.
Ergebnisse einsortieren: Was wirklich zählt
Unterm Strich liefert die PHP Garbage Collection spürbare Stabilität, wenn ich sie bewusst steuere statt sie zu bekämpfen. Ich kombiniere geringere Sammler-Frequenz mit genug RAM und nutze FPM-Recycling, damit Leaks verdampfen. OpCache und kleinere Datensätze senken den Druck auf den Heap und helfen, Stalls zu vermeiden. Sessions lasse ich per Cron aufräumen, damit Requests frei atmen. Mit Metriken und Profiling sichere ich die Wirkung ab und halte die Antwortzeiten verlässlich niedrig.


