gc_collect_cycles
Forces PHP's cycle collector to run immediately and returns how many cycles it freed.
gc_collect_cycles(): int- Added in PHP 5.3.0
Return value
intThe number of collected cycles.
Changelog
- PHP 8.5.0The return value no longer includes strings and resources that were indirectly collected through cycles, so the number now reflects collected cycles more narrowly than on earlier versions.
PHP frees most values the instant their reference count hits zero. Cycles are the exception: if object A holds B and B holds A, neither count ever reaches zero, so the pair survives until the cycle collector notices. gc_collect_cycles() runs that collector on demand instead of waiting for the root buffer to fill up.
When forcing a collection is worth it
Almost never in a normal web request — the request ends and everything is freed anyway. It earns its place in long-running processes: queue workers, daemons, batch importers, and anything built on a persistent runtime. Calling it once per batch, after you have dropped the references to that batch, keeps the memory curve flat instead of sawtoothing upward.
What it will not do
It only reclaims cycles. If memory is not falling and this function returns 0, the objects are still referenced by something live — a static array, an event listener registry, a closure captured in a container — and no amount of garbage collection will help. Reach for it after you have confirmed a cycle exists, not as a first response to rising memory.
gc_status() reports the collector's own counters and is the better diagnostic tool.
Example
When to use it
- Call it at the end of each batch in a long-running import worker to keep the memory curve flat.
- Reclaim memory in a daemon after tearing down objects that hold parent and child references to each other.
- Confirm in a test that a component genuinely releases its objects rather than leaking them in a cycle.
Frequently asked questions
Do I need to call gc_collect_cycles manually?
Not in a normal web request, where the process ends and reclaims everything anyway. It is worth calling in long-running CLI workers and daemons, once per batch, after you have released the references to that batch.
Why does gc_collect_cycles return 0 when my memory keeps growing?
Because nothing is stuck in a cycle — the objects are still reachable from something alive, such as a static property, a singleton container or a registered listener. Garbage collection cannot free values that are still referenced; you have to drop the reference.
What is the difference between gc_collect_cycles and unset?
unset removes one reference and frees the value immediately if that was the last one. gc_collect_cycles runs the separate cycle collector, which is the only thing that can free groups of objects that reference each other and therefore never reach a refcount of zero.
More examples
A cycle that reference counting cannot free
The parent holds its children and each child holds the parent, so dropping the variables leaves the whole graph with non-zero refcounts. The collector finds the cycle and the check prints bool(true).
<?php
class Parent_ { public array $kids = []; }
class Kid { public ?Parent_ $parent = null; }
$p = new Parent_();
for ($i = 0; $i < 3; $i++) {
$k = new Kid();
$k->parent = $p;
$p->kids[] = $k;
}
unset($p, $k);
var_dump(gc_collect_cycles() > 0);Per-batch cleanup in a worker loop
Each batch creates self-referencing objects, and the explicit collection at the end of the batch keeps usage roughly level across all three iterations instead of climbing until the root buffer happens to fill.
<?php
class Job { public ?Job $next = null; public string $data = ''; }
foreach (range(1, 3) as $batch) {
for ($i = 0; $i < 2000; $i++) {
$j = new Job();
$j->data = str_repeat('.', 200);
$j->next = $j; // self-cycle
}
unset($j);
$freed = gc_collect_cycles();
printf("batch %d: freed %d cycles, usage %.2f MB\n", $batch, $freed, memory_get_usage() / 1048576);
}Inspecting the collector with gc_status
Before the forced run, roots shows objects waiting in the buffer. Afterwards runs has incremented, collected has grown, and roots has dropped back — which is how you tell a real cycle problem from objects that are simply still referenced.
<?php
class Ring { public ?Ring $link = null; }
for ($i = 0; $i < 1000; $i++) {
$r = new Ring();
$r->link = $r;
}
unset($r);
print_r(array_intersect_key(gc_status(), array_flip(['runs', 'collected', 'roots'])));
gc_collect_cycles();
print_r(array_intersect_key(gc_status(), array_flip(['runs', 'collected', 'roots'])));
Discussion