← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

PHP Garbage Collection Internals (Cycle Collector)

PHP PHP 5.3+ Advanced
debt(d7/e5/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). Memory leaks from circular references don't produce errors — they silently accumulate until the process is killed by the OS. Detection requires active monitoring with memory_get_usage() or observing OOM kills in production. No linter or static tool detects improper GC handling; it requires runtime profiling or careful code review of object lifecycle patterns.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix suggests calling gc_collect_cycles() after batches, which is simple, but properly fixing the underlying issue often requires refactoring ORM entity relationships, breaking bidirectional references, or restructuring how objects are created in loops across multiple files in the data layer.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). Applies only to cli/queue-worker contexts (not typical web requests), but within those contexts it's a persistent concern. Every long-running worker must be designed with memory lifecycle in mind — monitoring memory_get_usage(), considering when to call gc_collect_cycles(), and being careful about ORM patterns. This shapes how you architect batch processing and queue consumers.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception explicitly states developers believe unset() calls the garbage collector — it does not. This contradicts how garbage collection works in other languages where 'freeing' memory is more immediate. The reference-counting + separate cycle-collector model is PHP-specific and unintuitive. Developers also wrongly expect WeakReference to prevent GC rather than allow it.

About DEBT scoring →

Also Known As

PHP GC garbage collection PHP cycle collector gc_collect_cycles

TL;DR

PHP uses reference counting as its primary memory management strategy — when a value's reference count drops to zero it is freed immediately. A secondary cycle collector handles circular references that reference counting alone cannot free.

Explanation

Reference counting frees most PHP values immediately when they go out of scope. The problem is circular references: object A holds a reference to object B, and B holds a reference back to A. Neither can ever reach a reference count of zero, so reference counting alone would leak them forever. PHP's cycle collector (enabled by default, based on the Bacon-Rajan algorithm) periodically scans for reference cycles and frees them. It is triggered when the root buffer (a list of potential cycle roots) reaches 10,000 entries. You can trigger it manually with gc_collect_cycles(). The collector has a cost — it pauses execution while it scans. Long-running CLI scripts and queue workers that create many objects are most affected.

Common Misconception

unset() calls the garbage collector. It does not — unset() decrements the reference count. The cycle collector runs separately on its own trigger threshold. Calling gc_collect_cycles() is the explicit way to run it.

Why It Matters

Queue workers and long-running PHP processes that process thousands of jobs accumulate circular references — often from ORM entities with bidirectional relationships. Without understanding the cycle collector, you see memory grow unboundedly until the worker is killed. Knowing when to call gc_collect_cycles() or how to break cycles with WeakReference prevents this.

Common Mistakes

  • Disabling the cycle collector with gc_disable() for 'performance' without profiling — the collector pause is usually negligible; disabling it causes memory leaks in long-running processes.
  • Expecting WeakReference to prevent GC — WeakReference does not prevent collection; it allows you to hold a reference that does not prevent the referent from being freed.
  • Not monitoring memory in queue workers — memory_get_usage() logged per batch reveals leaks early before the process is killed by the OS.
  • Creating ORM entities with lazy-loaded bidirectional relationships in loops — Doctrine and Eloquent frequently create cycles via their identity map and proxy objects.

Code Examples

✗ Vulnerable
<?php
// ❌ Circular reference — both objects keep each other alive
class Order
{
    public ?Customer $customer = null;
}

class Customer
{
    public ?Order $order = null;
}

for ($i = 0; $i < 100000; $i++) {
    $order = new Order();
    $customer = new Customer();
    $order->customer = $customer;
    $customer->order = $order; // Cycle: Order → Customer → Order
    // Both go out of scope here but refcount never hits zero
    // Memory grows unboundedly
✓ Fixed
<?php
// ✅ Option 1: Break the cycle with WeakReference (PHP 7.4+)
class Customer
{
    private WeakReference $order;

    public function setOrder(Order $order): void
    {
        $this->order = WeakReference::create($order);
    }

    public function getOrder(): ?Order
    {
        return $this->order->get();
    }
}

// ✅ Option 2: Periodically trigger the cycle collector in workers
for ($i = 0; $i < 100000; $i++) {
    processJob($jobs[$i]);
    if ($i % 1000 === 0) {
        gc_collect_cycles(); // Explicit sweep
    }
}

Added 23 Mar 2026
Views 86
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 1 ping T 0 pings F 0 pings S
No pings yet today
No pings yesterday
Ahrefs 7 Google 6 Perplexity 6 Bing 5 SEMrush 5 PetalBot 4 Scrapy 3 Twitter/X 2 Brave Search 2 Applebot 2 ChatGPT 1 Meta AI 1 Claude 1
crawler 44 crawler_json 1
🧱 FUNDAMENTALS — new to this? Start with the ground floor.
PHP php A server-side scripting language that generates web pages and APIs — the code runs on the server, and only its output (usually HTML or JSON) reaches the browser.

PHP is often the first server-side language people meet, and understanding its execution model — script starts fresh on every request, no memory between requests — explains most of how the web backend works: sessions, databases, and caching all exist to bridge that per-request amnesia.

💡 Start with PHP 8.x, declare(strict_types=1), and PDO — skip any tutorial that mentions mysql_query().

Ask Codex about PHP →
DEV INTEL Tools & Severity
⚡ Quick Fix
In long-running workers, call gc_collect_cycles() after processing each batch to free circular references proactively, and monitor memory_get_usage() to confirm the effect.
📦 Applies To
PHP 5.3+ cli queue-worker


✓ schema.org compliant