0Pricing
PHP Academy · Lesson

Memory Management and Garbage Collection

See how zvals, refcounting and the GC work.

Memory Management and Garbage Collection is a free PHP Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

How PHP Manages Memory

PHP values live in zvals, managed mostly by reference counting, with a cycle collector to catch what refcounting can't. Understanding this explains memory leaks in long-running workers, the cost of copies, and when gc_collect_cycles() matters.

The zval

A zval is the container for any PHP value: a type tag plus a union of value storage. Scalars (int, float, bool) are stored by value inside the zval and are NOT refcounted. Complex types (string, array, object) point to a refcounted structure on the heap.

<?php
// debug_zval_refcount-style inspection via gc + count
$arr = [1, 2, 3];
$copy = $arr;   // refcount of the array bumps, no data copy yet (CoW)

// Modifying triggers copy-on-write separation
$copy[] = 4;

var_dump($arr);   // still [1,2,3]
var_dump($copy);  // [1,2,3,4]
?>

Refcounting

Each refcounted value tracks how many zvals point to it. Assigning increments the count; unsetting or leaving scope decrements it. When the count hits zero, the memory is freed immediately — no waiting for a GC pass. Most PHP memory is reclaimed this way.

<?php
$a = str_repeat('x', 1000);  // refcount 1 on the string
$b = $a;                      // refcount 2
unset($a);                    // refcount 1
unset($b);                    // refcount 0 -> freed instantly

echo "Refcounting frees deterministically\n";
?>

Copy-on-Write

Assignment doesn't copy data; it shares it and bumps the refcount (copy-on-write). The actual duplication happens only on the first write to a shared value. This makes passing big arrays cheap until they're modified.

<?php
$big = range(1, 100000);
$shared = $big;          // O(1): shared, refcount++

// Reads stay shared and cheap
echo count($shared), PHP_EOL;

// First write separates (copies) — now it costs O(n)
$shared[0] = -1;
echo $big[0], ' vs ', $shared[0], PHP_EOL;  // 1 vs -1
?>

The Problem: Cycles

Refcounting alone can't free reference cycles. If object A holds a reference to B and B back to A, their counts never reach zero even after you unset every external variable — the memory leaks until the cycle collector runs.

<?php
class Node { public ?Node $ref = null; }

$a = new Node();
$b = new Node();
$a->ref = $b;   // a -> b
$b->ref = $a;   // b -> a  (cycle!)

unset($a, $b);  // external refs gone, but internal cycle keeps refcount > 0

// The objects are unreachable but NOT yet freed by refcounting alone.
echo gc_collect_cycles() . " cycles collected\n";
?>

The Cycle Collector

PHP's GC implements synchronous mark-and-sweep over roots. Potential cycle roots (values whose refcount was decremented but didn't reach zero) are buffered. When the root buffer fills (default 10,000), GC runs: it simulates decrements, finds truly unreachable groups, and frees them.

<?php
// GC status exposes the root buffer and run stats
for ($i = 0; $i < 3; $i++) {
    $a = new stdClass();
    $b = new stdClass();
    $a->b = $b; $b->a = $a;   // create a cycle
    unset($a, $b);            // becomes a GC root
}

print_r(gc_status());
?>

Controlling GC

You can steer the collector: gc_disable()/gc_enable() toggle it, gc_collect_cycles() forces a pass and returns the number freed. In throughput-critical batch jobs people sometimes disable GC during a tight phase, then collect once at the end.

<?php
gc_disable();              // no automatic cycle collection

// ... heavy phase that creates and drops many cycles ...
for ($i = 0; $i < 1000; $i++) {
    $x = new stdClass(); $y = new stdClass();
    $x->y = $y; $y->x = $x; unset($x, $y);
}

$freed = gc_collect_cycles();   // reclaim all at once
gc_enable();
echo "Freed $freed objects\n";
?>

Long-Running Workers

In CLI daemons, queue consumers, and Swoole/RoadRunner workers, memory isn't reclaimed by request teardown — the process lives for hours. Watch for: static caches that grow unbounded, accumulating cycles, and unclosed resources. Monitor with memory_get_usage(true) and restart workers periodically.

<?php
$peakStart = memory_get_usage(true);

// Simulate processing a batch of jobs
for ($job = 0; $job < 5; $job++) {
    $payload = range(0, 10000);
    // ... process ...
    unset($payload);
}

printf("start: %d KB, peak: %d KB, now: %d KB\n",
    $peakStart >> 10,
    memory_get_peak_usage(true) >> 10,
    memory_get_usage(true) >> 10
);
?>

WeakReference & WeakMap

To cache or associate data with an object without keeping it alive, use WeakReference (8.0) and WeakMap (8.0). A WeakMap entry doesn't increment the key object's refcount, so the object can be collected and its entry auto-removed — ideal for metadata side-tables that must not leak.

<?php
$map = new WeakMap();

$obj = new stdClass();
$map[$obj] = 'metadata';      // does NOT keep $obj alive

echo count($map) . PHP_EOL;   // 1
unset($obj);                  // object collectible -> entry removed
echo count($map) . PHP_EOL;   // 0
?>

Interned Strings & Immutables

Literal strings in your source are interned: stored once, shared, never refcounted/freed during the request. OPcache moves interned strings and immutable (cached) op_arrays into shared memory across requests. This is why string literals are effectively free to copy.

Memory Limit & Allocator

PHP uses its own emalloc allocator (Zend Memory Manager) layered over the system allocator, tracking per-request usage against memory_limit. Hitting the limit throws a fatal error. true in memory_get_usage(true) reports real allocated blocks from the OS, not just what your script currently holds.

<?php
echo 'limit: ' . ini_get('memory_limit') . PHP_EOL;
echo 'emalloc usage: ' . memory_get_usage() . " bytes\n";   // ZendMM tracked
echo 'real usage:    ' . memory_get_usage(true) . " bytes\n"; // from OS
?>

Quick Check

Why can refcounting alone leak some objects?

Recap

PHP stores values in zvals; scalars are by-value, complex types are heap-allocated and refcounted with copy-on-write. Refcounting frees deterministically at count zero but can't reclaim cycles, so a synchronous mark-and-sweep collector runs when the root buffer fills (or via gc_collect_cycles()). Use WeakMap/WeakReference for non-owning associations, and watch long-running workers for unbounded growth.

Frequently asked questions

Is the “Memory Management and Garbage Collection” lesson free?

Yes — the full text of “Memory Management and Garbage Collection” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.

What will I learn in “Memory Management and Garbage Collection”?

See how zvals, refcounting and the GC work. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start PHP Academy?

No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Memory Management and Garbage Collection” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this PHP Academy lesson?

Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. How the Zend Engine Works
  2. Memory Management and Garbage Collection
  3. OPcache and JIT Compilation
  4. Writing a Basic PHP Extension in C
← Back to PHP Academy