Garbage Collection
debt(d7/e3/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list Blackfire and php-meminfo as tools — these are specialist profilers that require active instrumentation of the running process. Memory growth from circular references in a queue worker is silent in development and only visible when monitoring memory over time in production-like conditions. It doesn't surface as a compiler or linter warning, and default static analysis won't flag missing gc_collect_cycles() calls.
Closest to 'simple parameterised fix' (e3). The quick_fix cites calling gc_collect_cycles() manually in long-running scripts, and common_mistakes point to unsetting large arrays/objects and avoiding circular references in listeners. These fixes are small but require identifying all affected loops and batch-processing points — slightly more than a single one-line patch but confined within one or a few scripts/components.
Closest to 'persistent productivity tax' (b5). The applies_to scope is cli and queue-worker contexts, not all PHP, so it doesn't reach the entire codebase. However, in any long-running PHP process the concern is pervasive — every loop, every object graph, every event listener registration must be written with GC awareness, imposing an ongoing mental tax on maintainers working in those contexts.
Closest to 'serious trap' (t7). The misconception field states directly: developers believe PHP's garbage collector handles all memory automatically, but circular references are only freed by the cycle collector which runs periodically — not immediately on unset. This contradicts the intuitive model that 'when I'm done with an object, memory is freed,' which is how reference counting works for non-circular structures. This mismatch causes real memory exhaustion bugs in production queue workers.
Also Known As
TL;DR
Explanation
PHP's primary GC is reference counting: each value tracks how many variables point to it; when the count reaches 0, the value is freed immediately. Problem: circular references (A → B → A) never reach 0. PHP's cycle collector (since 5.3) detects and frees circular reference groups. In PHP 8, the GC is more efficient. Long-running processes (queue workers, PHP-FPM long-lived workers) accumulate memory through leaks or slow GC; calling gc_collect_cycles() manually in batch jobs can reduce memory usage.
Diagram
flowchart TD
subgraph Reference Counting
OBJ[Object<br/>refcount=1] -->|assign to 2nd var| RC2[refcount=2]
RC2 -->|unset 1st var| RC1[refcount=1]
RC1 -->|unset all| RC0[refcount=0<br/>free immediately]
end
subgraph Cycle Collector
A[Object A<br/>points to B] -->|circular ref| B[Object B<br/>points to A]
B --> A
RC0B[Neither reaches 0<br/>both stranded]
CYCLE[Cycle Collector<br/>runs periodically] -->|detects cycle| FREE[Frees both]
end
style RC0 fill:#238636,color:#fff
style FREE fill:#238636,color:#fff
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Not unsetting large arrays and objects after use in loops — reference count stays > 0 until scope ends.
- Circular references in event listeners — object A registers a listener on object B; both hold references to each other.
- Not calling gc_collect_cycles() in very long batch jobs — cycle collector runs automatically but not immediately.
- PHP generators as memory optimisation — they yield one item at a time, but the generator object itself stays in memory.
Avoid When
- Do not disable gc_disable() in long-running workers without a manual gc_collect_cycles() strategy — circular references accumulate silently until OOM.
- Avoid creating large object graphs with circular parent/child references when a simple array or flat structure would suffice.
When To Use
- Call gc_collect_cycles() explicitly after processing large batches of objects in long-running scripts to reclaim memory from circular references immediately.
- Use WeakReference for cache or observer registrations where the GC should be free to collect the target without the reference preventing it.
Code Examples
// Circular reference — not immediately freed:
class Node {
public ?Node $parent = null;
public array $children = [];
}
$root = new Node();
$child = new Node();
$root->children[] = $child;
$child->parent = $root; // Circular: root → child → root
unset($root, $child); // Reference count > 0 — not freed immediately
// Freed later by cycle collector — or manually: gc_collect_cycles()
// Break circular references explicitly:
function processTree(Node $root): void {
foreach ($root->children as $child) processTree($child);
// Break circular refs before returning:
foreach ($root->children as $child) $child->parent = null;
$root->children = [];
}
// Or use WeakReference for parent pointers — does not prevent GC
class Node {
public ?WeakReference $parent = null; // Weak — GC can collect parent
}
Tags
Edits history 1 edit
- refs PF Media Bot Claude Opus 4.5 · 25 Apr 2026