Why Do We Even Think About Memory in PHP?
\nMost PHP developers are used to thinking of memory as an infinite resource. Requested an array with a million elements? No problem. Created an object and then forgot about it? That's fine, PHP will clean it up itself. But when your project starts crashing with the error Allowed memory size exhausted or unexpectedly slows down for no apparent reason, it's time to understand what's really happening under the hood.
Today we'll look inside the black box called "Memory and Garbage Collector in PHP". You'll learn how PHP counts references, why cyclic references are evil, and how to make the garbage collector work for you, not against you. Let's go!
\n\nHow PHP Manages Memory: From Request to Response
\nUnlike C or C++, where you allocate and free memory yourself, PHP works through the Zend Engine. Each PHP process has its own memory pool. When you write:
\n$data = [1, 2, 3];\nPHP allocates memory for the array, stores the values, and increments the so-called refcount (reference counter). As long as someone references the $data variable, the memory is not freed.
\nBut as soon as you do:
\nunset($data);\nthe reference counter drops to zero, and the Zend Engine instantly returns the memory back to the pool. Simple and fast. However, there are nuances…
\n\nReference Arithmetic: refcount and is_ref
\nEach variable in PHP is a zval structure that stores the value, type, the is_ref flag (whether it's a reference), and refcount. Let's look at an example:
$a = "Hello";\n$b = $a; // refcount increases to 2 (copy-on-write)\n$b = "World"; // now $a and $b are different zvals, refcount for "Hello" drops to 1\n\nThis is called copy-on-write. As long as you don't change the value, PHP saves memory by using the same area. But as soon as you modify a variable, a copy is created. Clever, isn't it?
\n\nGarbage Collector (GC): When refcount Doesn't Help
\nThe normal reference counting mechanism works perfectly for 99% of cases. But there is one problem — cyclic references. Imagine you have two objects that reference each other:
\nclass Node {\n public $next;\n}\n\n$a = new Node();\n$b = new Node();\n$a->next = $b;\n$b->next = $a;\n\nunset($a);\nunset($b);\n\nAfter unset, both objects still have refcount = 1 (each references the other). Formally, they are inaccessible from the code, but the memory is not freed. This is a cyclic reference.
If there are many such objects, memory will leak. To combat this, starting from version 5.3, PHP introduced a full-fledged garbage collector (GC).
\n\nHow Does the GC Work in PHP?
\nThe garbage collector does not run on every line of code. It is activated when the root buffer (an array of potential cyclic references) is full. By default, its size is 10,000 elements. When the buffer is full, the GC runs an algorithm that:
\n- \n
- Scans the roots — finds all zvals with refcount > 0 that could be part of a cycle. \n
- Decrements refcount for all child elements (as if the references didn't exist). \n
- Checks — if after decrementing, the refcount becomes zero, it means it's "garbage" and can be deleted. \n
- Restores refcount for those that turned out not to be garbage, and frees the memory. \n
This process is called garbage collection. You can force it to run:
\ngc_collect_cycles();\nBut don't do this for every little thing — it's an expensive operation.
\n\nHow to Track Memory Leaks in PHP?
\nIf your script runs for a long time (e.g., a queue worker or a daemon), leaks become critical. Here are a few tools