PHP Memory Model — Zval & Copy-on-Write
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). Memory issues from misunderstanding CoW don't trigger any compiler or linter warnings. Detection requires profiling with memory_get_peak_usage() or observing unexpected memory spikes in production. No detection_hints.tools specified for this term, and standard PHP linters don't catch CoW-related misconceptions.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix suggests profiling to identify the problem, but fixing CoW-related memory issues often requires understanding data flow through multiple functions. Changing how arrays are passed or restructuring code to avoid unintended copies can span several files, though it rarely requires architectural changes.
Closest to 'persistent productivity tax' (b5). CoW behavior applies across all PHP contexts (web, cli) and affects any code working with arrays and objects. It's a fundamental aspect of PHP internals that shapes how developers should think about memory, but it doesn't define system architecture — it's more of an ongoing consideration when writing performant code with large data structures.
Closest to 'serious trap' (t7). The misconception states developers believe 'PHP copies arrays on assignment like a value type' when it actually uses CoW. This contradicts intuition from other languages and leads to unnecessary reference passing (&$arr) or confusion about when memory is actually freed. The object-reference vs array-CoW distinction adds another layer of confusion that trips up experienced developers.
Also Known As
TL;DR
Explanation
In PHP 7+, a zval is a compact C struct holding a type tag and a union of the actual value (long, double, pointer to string/array/object). For scalar values (int, float, bool, null), the zval holds the value directly — no heap allocation. For strings and arrays, the zval holds a pointer to a reference-counted heap structure. When you assign '$b = $a' where $a is an array, PHP does not copy the array — it increments the reference count and both variables point to the same structure. Only when either variable is modified does PHP perform a real copy ('copy-on-write'). Objects behave differently — they are always reference types; assignment copies the handle, not the object.
Common Misconception
Why It Matters
Common Mistakes
- Assuming unset() immediately frees memory — it decrements the reference count; memory is only freed when the count reaches zero, which won't happen if other variables reference the same data.
- Passing arrays by reference (&$arr) to avoid copying — in PHP 7+ this is usually unnecessary; CoW makes value passing cheap for read-only operations.
- Forgetting that objects are always reference types — '$b = $a' for objects copies the handle, not the object; both variables see mutations.
- Not accounting for circular references — reference counting cannot collect cycles (A → B → A); PHP's cycle collector runs periodically but not immediately.
Code Examples
<?php
// ❌ Accidentally triggering CoW — sorting inside function copies the array
function getTopItems(array $items, int $n): array
{
sort($items); // Modifies $items — triggers full CoW copy of the passed array
return array_slice($items, 0, $n);
}
// ❌ Holding references to large arrays longer than needed
$cache = [];
foreach ($largeDataset as $row) {
$cache[] = $row; // $largeDataset reference count stays high
}
<?php
// ✅ Sort a copy explicitly — makes intent clear, CoW still applies
function getTopItems(array $items, int $n): array
{
$sorted = $items; // Cheap — increments refcount only
sort($sorted); // CoW copy happens here, only $sorted is affected
return array_slice($sorted, 0, $n);
}
// ✅ Unset large variables when done to reduce refcount
foreach ($largeDataset as $row) {
process($row);
}
unset($largeDataset); // Drops refcount — memory freed if no other refs