0

Memory model — how PHP stores variables (zval internals)

Expert5 min read·php-02-020
interviewperformance

Concept

Every PHP variable is backed by a zval (Zend Value) — the internal C struct that stores the variable's value, type, and reference-counting metadata. Understanding zvals explains PHP's memory behavior, copy-on-write semantics, and why certain operations are expensive or cheap.

A zval in PHP 8 contains: a value union (stores the actual data — int, float, pointer to string/array/object), a type byte (IS_NULL, IS_LONG, IS_DOUBLE, IS_STRING, IS_ARRAY, IS_OBJECT, IS_TRUE, IS_FALSE, IS_UNDEF), and two additional bytes for type_flags and extra. Small integers and floats are stored directly in the zval (no heap allocation). Strings, arrays, and objects are heap-allocated and the zval holds a pointer to them.

Reference counting and copy-on-write: Heap-allocated values (strings, arrays, objects) maintain their own refcount — the number of zvals pointing to them. When you assign $b = $a where $a holds an array, PHP doesn't copy the array; it increments the array's refcount and both zvals point to the same heap allocation. Only when you write to $b does PHP "separate" — decrement the refcount, allocate a new copy, and write to the copy. This is copy-on-write (COW).

Interned strings: PHP interns (de-duplicates) string literals and function/class/variable names at compile time. Interned strings have IS_STR_INTERNED flag set, which disables reference counting for them — they live for the entire request lifecycle and are never freed individually. This is why string-heavy PHP code uses less memory than you'd expect.

Objects are always reference-like: Object zvals store an object_handle — an index into a global object store. Two variables "holding" the same object actually hold two separate zvals, both containing the same handle. This is why object assignment feels like pass-by-reference but isn't: you can reassign $b = new Foo() without affecting $a, but $b->prop = 1 does affect $a->prop because both handles point to the same object data.

Code Example

php
<?php
declare(strict_types=1);

// Observe copy-on-write with debug_zval_refcount (requires Xdebug)
$a = ['x' => 1, 'y' => 2];
$b = $a;        // No copy yet — refcount becomes 2
$b['z'] = 3;    // Now PHP separates — copies the array, refcount drops back to 1

// memory_get_usage to observe allocation
$before = memory_get_usage();
$large = array_fill(0, 100_000, 'value');
$copy  = $large;            // COW: no copy yet, cheap
$after_assign = memory_get_usage();

$copy[0] = 'modified';      // Forces separation — actual 100k copy happens here
$after_write = memory_get_usage();

echo "After assign:  " . ($after_assign - $before) . " bytes\n";   // ~0 extra
echo "After write:   " . ($after_write  - $before) . " bytes\n";   // ~4MB extra

// Objects are handles, not copies
class Counter { public int $n = 0; }

$c1 = new Counter();
$c2 = $c1;          // Both hold the same object handle
$c2->n = 42;
echo $c1->n;        // 42 — same underlying object

$c2 = new Counter(); // Now $c2 holds a different handle — $c1 unchanged
echo $c1->n;        // still 42

// Integers stored directly in zval — zero heap allocation
$x = PHP_INT_MAX;  // 9223372036854775807 — stored inline, no heap
$y = 3.14;         // also stored inline for doubles