Garbage Collection

JA JASWANT Updated 19 Oct 2026
6 min read ·Lesson 120 of 143
In this post

How PHP Frees Memory In simple terms: Every variable, array, and object needs memory. When a value is no longer used, PHP gives its memory back automatically — you never call free() as in C. Usually t

How PHP Frees Memory

In simple terms: Every variable, array, and object needs memory. When a value is no longer used, PHP gives its memory back automatically — you never call free() as in C. Usually this happens immediately. In one special situation — objects that refer to each other in a circle — PHP needs an extra helper, the garbage collector, to find and free them.

Technically: PHP manages memory with reference counting: every value (zval) tracks how many variables, array elements, or properties refer to it. When the count drops to zero, the value is destroyed immediately (and an object's destructor runs). Reference counting cannot free circular references — structures that point to themselves directly or indirectly — because their counts never reach zero. PHP's cycle collector periodically finds such unreachable cycles and frees them. Additionally, PHP releases all memory of a web request when the request ends.


Reference Counting in Action

<?php
final class Report
{
    public function __construct(private string $name)
    {
        echo "Created {$this->name}\n";
    }

    public function __destruct()
    {
        echo "Freed {$this->name}\n";
    }
}

$a = new Report('sales');   // refcount 1
$b = $a;                    // refcount 2 — both variables point to the same object

unset($a);                  // refcount 1 — still alive
echo "After unset(\$a)\n";

$b = null;                  // refcount 0 — destroyed immediately
echo "After \$b = null\n";

Output

Created sales
After unset($a)
Freed sales
After $b = null

Memory is released at the exact moment the last reference disappears. This makes PHP's memory behavior predictable: local variables are freed when a function returns, and temporary arrays are freed when they are overwritten (see PHP Destructor).

Copy-on-Write

Assigning an array or string does not copy it immediately:

$prices = range(1, 100_000);   // one array in memory
$copy = $prices;               // no copy yet — both share the same array
$copy[] = 100_001;             // now PHP copies the array, because it is modified

PHP shares the value and copies it only when one side writes to it. This is why passing large arrays to functions is cheap as long as the function does not modify them — there is no need to pass arrays by reference "for performance".


The Problem: Circular References

<?php
final class Node
{
    public ?Node $partner = null;

    public function __construct(public string $name) {}

    public function __destruct()
    {
        echo "Freed {$this->name}\n";
    }
}

$a = new Node('A');
$b = new Node('B');
$a->partner = $b;   // A → B
$b->partner = $a;   // B → A

unset($a, $b);
echo "Variables are gone, but the objects still reference each other\n";

$collected = gc_collect_cycles();
echo "Cycle collector ran\n";
var_dump($collected > 0);

Output (the two "Freed" lines may appear in either order)

Variables are gone, but the objects still reference each other
Freed A
Freed B
Cycle collector ran
bool(true)

After unset(), no variable refers to A or B, but each object still holds a reference to the other, so both counts stay at 1. Only the cycle collector — here triggered manually with gc_collect_cycles() — detects that the pair is unreachable and frees it. gc_collect_cycles() returns the number of freed values.

When the Collector Runs Automatically

PHP records potential cycle roots in a buffer. When the buffer fills (10,000 roots by default; the threshold adapts automatically to the workload), the collector runs. You rarely need to call it yourself, except in long-running processes with many short-lived cyclic structures.

FunctionPurpose
gc_collect_cycles()Run the cycle collector now; returns the number of freed values
gc_enabled(), gc_enable(), gc_disable()Check or switch automatic collection
gc_status()Statistics: runs, collected values, threshold, buffered roots (PHP 8.3 adds timing information)
gc_mem_caches()Return cached memory blocks of the memory manager to the operating system

Avoiding Cycles: Weak References

Many cycles come from objects that know their parent (a tree node referencing its parent) or caches that refer to objects. Weak references refer to an object without keeping it alive:

<?php
final class Customer
{
    public function __construct(public string $name) {}
}

$customer = new Customer('Asha');
$reference = WeakReference::create($customer);

echo $reference->get()?->name . "\n";

unset($customer);
var_dump($reference->get());

Output

Asha
NULL

WeakReference::get() returns the object while it exists elsewhere and null after it has been destroyed. The weak reference itself does not count toward the reference count.

WeakMap: Data Attached to Objects

<?php
final class Order
{
    public function __construct(public int $id) {}
}

$computedTotals = new WeakMap();

$order = new Order(501);
$computedTotals[$order] = 380.00;

echo count($computedTotals) . " cached entry\n";

unset($order);
echo count($computedTotals) . " cached entries\n";

Output

1 cached entry
0 cached entries

A WeakMap uses objects as keys without keeping them alive. When an object is destroyed, its entry disappears automatically — ideal for caches attached to objects, which otherwise grow forever in long-running processes.


Memory in Web Requests vs Long-Running Processes

SituationBehaviorWhat to Watch
Web request (PHP-FPM)All request memory is released when the request endsPeak memory per request (memory_limit)
CLI scripts and importsMemory accumulates over the runProcess data in streams and batches (see PHP Generators)
Queue workers and daemonsRun for hours; leaks add upFree large structures, avoid growing static caches, restart workers periodically
Application servers (Swoole, RoadRunner, FrankenPHP worker mode)Objects survive between requestsReset state between requests; avoid static caches and cycles

Measuring Memory

<?php
$before = memory_get_usage();

$rows = array_fill(0, 100_000, ['sku' => 'PEN-01', 'qty' => 1]);
$during = memory_get_usage();

unset($rows);
$after = memory_get_usage();

printf("Array used about %.1f MB; after unset: %.1f MB above the start\n",
    ($during - $before) / 1048576,
    ($after - $before) / 1048576
);

Output (approximate; varies by PHP version and platform)

Array used about 2.0 MB; after unset: 0.0 MB above the start

All 100,000 elements are copies of the same small array, which PHP shares between them (copy-on-write), so the outer list needs only about 16 bytes per element (PHP 8.2+ stores simple lists compactly). unset() frees it immediately, because its reference count reaches zero. memory_get_peak_usage() reports the highest usage of the whole script — the figure to compare with memory_limit.


Common Mistakes

Calling gc_collect_cycles() Everywhere

Effect: Wasted CPU time; reference counting already frees most memory immediately.

Fix: Call it only in long-running processes after creating many cyclic structures, if measurements show a benefit.

Disabling the Garbage Collector "for Speed"

Effect: Memory grows without limit when cycles occur.

Fix: Leave it enabled; tune only with measurements.

Ever-Growing Static Caches in Workers

Effect: Memory leaks in long-running processes.

Fix: Bound caches, use WeakMap, or reset state between jobs.

Passing Arrays by Reference to "Save Memory"

Effect: No gain (copy-on-write already avoids copies) and harder-to-read code.

Fix: Pass by value unless the function must modify the caller's array.


Best Practices

  • Rely on reference counting: release large data with unset() or by letting variables go out of scope.
  • Avoid unnecessary circular references; use weak references and WeakMap for back-references and caches.
  • Stream and batch large data instead of loading it all at once.
  • In long-running processes, monitor memory and restart workers periodically.
  • Measure with memory_get_usage(), memory_get_peak_usage(), and gc_status() before tuning.

Official reference: https://www.php.net/manual/en/features.gc.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning
Ask about this post
AI Ask about this post

Ask questions about Garbage Collection and get answers drawn from it.

Signed-in readers only.