Understanding PHP’s Garbage Collection and Reference Counting: Deep Dive with Code Examples

This article explains how PHP’s automatic garbage collector works, detailing reference‑counting, zval structures, handling of scalar and composite types, cycle detection, configuration options, and practical debugging techniques using Xdebug and code samples.

Open Source Tech Hub
Open Source Tech Hub
Open Source Tech Hub
Understanding PHP’s Garbage Collection and Reference Counting: Deep Dive with Code Examples

Concept

PHP’s garbage collection is automatic and relies on a built‑in collector. When a PHP object is no longer referenced it becomes garbage; the collector periodically scans memory, marks unreferenced objects, and frees their memory.

The mechanism uses reference counting . Each object has a counter that increments when a variable is assigned ( +1) and decrements when the variable no longer references the object ( -1). When the counter reaches zero, the object is freed.

For cyclic references the collector cannot free the objects, so PHP allows manual breaking of references by assigning null to the variable, forcing the counter to zero.

Reference Counting Basics

PHP variables are stored in a container called zval. Besides type and value, a zval holds two bits: is_ref (whether the variable is part of a reference set) and refcount (how many symbols point to this zval).

Creating a new scalar variable creates a single zval with refcount=1 and is_ref=false. Example:

<?php
$a = "new string";

Running xdebug_debug_zval('a'); shows: a: (refcount=1, is_ref=0)='new string' Assigning the variable to another name increments the refcount:

<?php
$a = "new string";
$b = $a;
xdebug_debug_zval('a');
a: (refcount=2, is_ref=0)='new string'

Calling unset($a) or letting the scope end reduces the counter, and when it reaches zero the zval is destroyed.

Composite Types

Arrays and objects store their elements in separate symbol tables, creating multiple zvals. Example of an array:

<?php
$a = array('meaning' => 'life', 'number' => 42);
xdebug_debug_zval('a');
a: (refcount=1, is_ref=0)=array (
    'meaning' => (refcount=1, is_ref=0)='life',
    'number'  => (refcount=1, is_ref=0)=42
)

Adding an element that references an existing element increases the refcount of the shared zval:

<?php
$a = array('meaning' => 'life', 'number' => 42);
$a['life'] = $a['meaning'];
xdebug_debug_zval('a');
a: (refcount=1, is_ref=0)=array (
    'meaning' => (refcount=2, is_ref=0)='life',
    'number'  => (refcount=1, is_ref=0)=42,
    'life'    => (refcount=2, is_ref=0)='life'
)

When an array element is unset, the refcount of the referenced zval decreases, and the container is removed once the count reaches zero.

Cleanup Issues

Even without any external symbols, a structure may remain uncollected if an array element still references the array itself, creating a memory leak. This is problematic for long‑running scripts or massive test suites, where such leaks can consume gigabytes of RAM.

Cycle Collection

Since PHP 5.3, a cyclic‑GC algorithm is integrated to break reference cycles. The algorithm works on a "root buffer" that stores possible garbage roots (zval containers). When the buffer fills (default 10 000 entries), a simulated delete‑restore process runs:

Simulate deleting each purple (possible‑root) variable, decreasing refcounts of reachable variables.

If a variable’s refcount drops to zero, it is also simulated for deletion.

After the simulation, restore variables whose refcount is still > 0 (black nodes).

Remaining blue nodes are truly unreachable and are finally freed.

Configuration

The GC can be toggled via zend.enable_gc in php.ini, or at runtime with gc_enable() and gc_disable(). Manual collection is possible with gc_collect_cycles(), which returns the number of cycles reclaimed.

Even when disabled, possible roots are still recorded because tracking them is cheaper than checking the GC state on each potential root.

For high‑performance sections of an application you may disable the GC, but you should invoke gc_collect_cycles() beforehand to empty the root buffer and avoid hidden leaks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Memory ManagementGarbage Collectionreference countingzvalXdebug
Open Source Tech Hub
Written by

Open Source Tech Hub

Sharing cutting-edge internet technologies and practical AI resources.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.