PHP Closures and Generators can hold circular references

Circular references are a common cause of memory leaks in PHP. They occur when objects reference each other, directly or indirectly. Thankfully, PHP has a garbage collector that can detect and clean up circular references. However, this consumes CPU cycles and can slow down your application. The garbage collector is triggered whenever 10,000 possible cyclic objects or arrays are currently in memory, and one of them falls out of scope. The garbage collection is never triggered if you have a small number of objects using a lot of memory. You can reach the memory limit even if the memory is used by orphaned objects that should be collected by the garbage collector. That's why you should identify situation where you create circular references and avoid them. Ideally for a web application, you would want to disable the garbage collector and let the PHP release all memory when after the response is sent. But this is risky for long-running scripts, such as daemons or workers, where memory leaks can accumulate over time and slow down the application by calling the garbage collector very frequently. In this article, we explore how closures and generators can hold circular references, and how to prevent them. About circular references Typical example of a circular reference Preventing circular references with weak references Closures and circular references Generators and circular references Conclusion About circular references Typical example of a circular reference class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { public function __construct(public A $a) {} } In this example, A and B reference each other. When you create an instance of A, it creates an instance of B that references A. This creates a circular reference. To detect circular references, we can trigger the garbage collector manually using gc_collect_cycles() and read the number of collected references using gc_status(). // Object created but not assigned to a variable new A(); gc_collect_cycles(); print_r(gc_status()); This will output: Array ( ... [collected] => 2 ... ) This example shows that the garbage collector has detected and removed 2 objects with circular references. You can also see the number of references to an object using the xdebug_debug_zval() function. Preventing circular references with weak references When confronted to a circular reference, an easy solution is to use a weak reference. A weak reference is an object that holds a reference that does not prevent the garbage collector from collecting the object it references. In PHP, you can create a weak reference using the WeakReference class. This requires some changes to the code. The B class now stores a WeakReference object instead of an A object. You must access the A object using the get() method of the WeakReference object. class A { public B $b; public function __construct() { $this->b = new B($this); } } class B { /** @var WeakReference $a */ public WeakReference $a; public function __construct(A $a) { $this->a = WeakReference::create($a); } } // Object created but not assigned to a variable new A(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 0 In the output, you will see that the number of collected references is now 0. Tip 1: Use weak references to prevent circular references only when necessary. Closures and circular references The concept of closures in PHP is to create a function that can access variables from the parent scope. This can lead to circular references if you are not careful. function createCircularReference() { $a = new stdClass(); $a->b = function () use ($a) { return $a; }; return $a; } In this example, the closure $a->b references the variable $a from the parent scope. The circular reference is easy to spot as the reference is explicit. But the same issue can occur in a more subtle way if you use the short syntax for closures. Using an arrow function, the variable $a is not explicitly referenced in the closure, but it is still captured by reference. function createCircularReference() { $a = new stdClass(); $a->b = fn() => $a; return $a; } createCircularReference(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 2 In this example, the number of collected references is 2, indicating a circular reference. Reference to $this in closures Any non-static closure that is created within a class method will have a reference to the object instance ($this), even if $this is not accessed. class A { public Closure $closure; public function __construct() { $this->closure = function () {}; } } new A(); gc_collect_cycles(); print_r(gc_status()); // [collected] => 2 This is beca

Jan 17, 2025 - 22:57
PHP Closures and Generators can hold circular references

Circular references are a common cause of memory leaks in PHP. They occur when objects reference each other, directly or indirectly. Thankfully, PHP has a garbage collector that can detect and clean up circular references. However, this consumes CPU cycles and can slow down your application.

The garbage collector is triggered whenever 10,000 possible cyclic objects or arrays are currently in memory, and one of them falls out of scope.

The garbage collection is never triggered if you have a small number of objects using a lot of memory. You can reach the memory limit even if the memory is used by orphaned objects that should be collected by the garbage collector.

That's why you should identify situation where you create circular references and avoid them.

Ideally for a web application, you would want to disable the garbage collector and let the PHP release all memory when after the response is sent. But this is risky for long-running scripts, such as daemons or workers, where memory leaks can accumulate over time and slow down the application by calling the garbage collector very frequently.

In this article, we explore how closures and generators can hold circular references, and how to prevent them.

  • About circular references
    • Typical example of a circular reference
    • Preventing circular references with weak references
  • Closures and circular references
  • Generators and circular references
  • Conclusion

About circular references

Typical example of a circular reference

class A {
    public B $b;

    public function __construct()
    {
        $this->b = new B($this);
    }
}

class B {
    public function __construct(public A $a) {}
}

In this example, A and B reference each other. When you create an instance of A, it creates an instance of B that references A. This creates a circular reference.

To detect circular references, we can trigger the garbage collector manually using gc_collect_cycles() and read the number of collected references using gc_status().

// Object created but not assigned to a variable
new A();

gc_collect_cycles();
print_r(gc_status());

This will output:

Array
(
    ...
    [collected] => 2
    ...
)

This example shows that the garbage collector has detected and removed 2 objects with circular references.

You can also see the number of references to an object using the xdebug_debug_zval() function.

Preventing circular references with weak references

When confronted to a circular reference, an easy solution is to use a weak reference. A weak reference is an object that holds a reference that does not prevent the garbage collector from collecting the object it references. In PHP, you can create a weak reference using the WeakReference class.

This requires some changes to the code. The B class now stores a WeakReference object instead of an A object. You must access the A object using the get() method of the WeakReference object.

class A {
    public B $b;

    public function __construct()
    {
        $this->b = new B($this);
    }
}

class B {
    /** @var WeakReference $a */
    public WeakReference $a;

    public function __construct(A $a)
    {
        $this->a = WeakReference::create($a);    
    }
}
// Object created but not assigned to a variable
new A();

gc_collect_cycles();
print_r(gc_status());
// [collected] => 0

In the output, you will see that the number of collected references is now 0.

Tip 1: Use weak references to prevent circular references only when necessary.

Closures and circular references

The concept of closures in PHP is to create a function that can access variables from the parent scope. This can lead to circular references if you are not careful.

function createCircularReference()
{
    $a = new stdClass();
    $a->b = function () use ($a) {
        return $a;
    };

    return $a;
}

In this example, the closure $a->b references the variable $a from the parent scope. The circular reference is easy to spot as the reference is explicit.

But the same issue can occur in a more subtle way if you use the short syntax for closures. Using an arrow function, the variable $a is not explicitly referenced in the closure, but it is still captured by reference.

function createCircularReference()
{
    $a = new stdClass();
    $a->b = fn() => $a;

    return $a;
}

createCircularReference();

gc_collect_cycles();
print_r(gc_status());
// [collected] => 2

In this example, the number of collected references is 2, indicating a circular reference.

Reference to $this in closures

Any non-static closure that is created within a class method will have a reference to the object instance ($this), even if $this is not accessed.

class A {
    public Closure $closure;
    public function __construct()
    {
        $this->closure = function () {};
    }
}

new A();

gc_collect_cycles();
print_r(gc_status());
// [collected] => 2

This is because the $this reference is always captured by reference in the closure. It can be accessed using Reflection::getClosureThis().

class A {
    public Closure $closure;
    public function __construct()
    {
        $this->closure = static function () {};
    }
}

new A();

gc_collect_cycles();
print_r(gc_status());
// [collected] => 0

If the closure is created from the global scope or inside a static method, the $this reference is null.

Tip 2: Always use static function () {} or static fn () => when creating a closure if you don't need $this.

Generators and circular references

We come to the reason for this article. Something I discovered recently:
Generators keep references, as long as they are not exhausted.

In this example, the class stores the generator in a property, but the generator has $this reference to the object instance.
The generator behaves like a closure and keeps a reference to the object instance.

class A
{
    public iterable $iterator;

    public function __construct()
    {
        $this->iterator = $this->generator();
    }

    private function generator(): Generator
    {
        yield;
    }
}

new A();

gc_collect_cycles();
print_r(gc_status());
// [collected] => 1

The class instance was collected by the garbage collector because it has a reference to the generator, which has a reference to the object instance.

As soon as the generator is exhausted, the reference is released, and the object instance is removed from the memory.

iterator_to_array((new A())->iterator);

gc_collect_cycles();
print_r(gc_status());
// [collected] => 0

Tip 3: Ensure you always exhaust the generator by iterating over it.

Tip 4: Use static methods or closure to create generators to not keep references to the object instance.

Conclusion

Circular references are a common cause of memory leaks in PHP. Even if the garbage collector can detect and clean up circular references, it consumes CPU cycles and can slow down your application. You must detect situations where you create such circular reference and adapt your code to prevent them. Using weak references can prevent circular references, but some simple tips can help you to prevent circular references in the first place:

  • Use static function () {} or static fn () => when creating a closure if you don't need $this.
  • Ensure you always exhaust the generator by iterating over it.
  • Use static methods or closure to create generators to not keep references to the object instance.

Read more