Values, references and memory

PHP · Expert · 16 min

Knowing the memory model explains performance surprises, memory leaks in workers and odd reference bugs.

Internally every PHP value is a zval: a small struct with a type tag and either an inline scalar (int, float, bool, null) or a pointer to a reference-counted structure (string, array, object).

Arrays and strings use copy-on-write. $b = $a just increments a refcount; the actual copy happens only when one side is modified. That's why passing large arrays to functions is cheap as long as the function only reads them. Packed arrays (sequential integer keys) use less memory than hash maps with arbitrary keys.

Objects are different: the zval holds a handle to the object, so copies of the variable share one object. This is not the same as a reference.

A PHP reference (&) makes two names point to the same zval slot. References disable copy-on-write for that value and have surprising effects. The classic one: after foreach ($arr as &$v), $v still references the last element, and a later foreach ($arr as $v) overwrites it. Always unset($v) after a by-reference loop, or avoid references entirely.

Memory is freed as soon as a refcount drops to zero. Cycles (an object that references itself, parent and child pointing to each other) never reach zero, so PHP runs a cycle collector when its root buffer fills (10,000 possible roots). gc_collect_cycles() forces a run and gc_status() reports statistics. In short requests leaks rarely matter; in long-running workers and queue consumers they accumulate, so measure with memory_get_usage() and break cycles explicitly or use WeakMap.

Example

<?php
$a = range(1, 100_000);
$before = memory_get_usage();
$b = $a;                                   // no copy yet (refcount 2)
echo memory_get_usage() - $before, "\n";   // 0
$b[] = 1;                                  // now the array is separated
echo memory_get_usage() - $before > 1_000_000 ? "copied\n" : "shared\n"; // copied

$list = [1, 2, 3];
foreach ($list as &$v) { $v *= 10; }
foreach ($list as $v) {}                   // reuses the reference!
print_r($list);                            // [10, 20, 20]

class Node { public ?Node $other = null; }
$x = new Node(); $y = new Node();
$x->other = $y; $y->other = $x;            // cycle
unset($x, $y);
echo gc_collect_cycles(), "\n";            // 2 objects freed

Remember

  • Arrays and strings are copy-on-write: copies are free until modified
  • Objects are shared handles; & references are a different, rarer mechanism
  • Reference cycles need the cycle collector; long-running workers must watch memory

Watch out for

  • Leaving a by-reference foreach variable alive, then reusing its name in another loop.

Check yourself

  1. After $b = $a for a large array, when does PHP actually copy the data?
  2. Why is foreach ($arr as &$v) followed by foreach ($arr as $v) dangerous?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.php.net/manual/en/features.gc.refcounting-basics.php

Builds on

Unlocks

Nothing builds on this yet.