0

WeakReference and WeakMap (PHP 8.0+) — preventing memory leaks

Expert5 min read·php-08-020
performancecompare

Concept

WeakReference and WeakMap (both PHP 8.0) provide ways to hold references to objects without preventing their garbage collection. They're essential tools for building caches, observer systems, and memoization that don't cause memory leaks in long-running applications.

WeakReference::create($object) creates a weak reference to an object. Calling $weakRef->get() returns the object if it's still alive, or null if it has been garbage collected. The WeakReference itself does not increment the object's refcount — the object can be freed even while the WeakReference exists.

WeakMap is a hash map where keys are objects and values are arbitrary data — but the key objects are held weakly. If an object used as a key is garbage collected, its entry is automatically removed from the WeakMap. This prevents the map from growing forever in a long-running process.

Use cases:

  • Caching computed properties on objects: A WeakMap<object, ComputedValue> stores computed results associated with objects; when the object is GC'd, the cache entry is automatically cleaned up.
  • Observer/event systems: Hold weak references to listeners. When a listener object is destroyed, it stops receiving events without needing explicit unregistration.
  • Circular reference prevention: When two objects logically reference each other but you want one direction to be non-owning, use WeakReference for that direction.
  • ORM identity map: An ORM's session can use a WeakMap to track loaded entities. If the application discards the entity reference, the identity map entry is automatically removed.

Code Example

php
<?php
declare(strict_types=1);

// WeakReference — observe without owning
class HeavyObject
{
    public string $data;
    public function __construct(string $data) { $this->data = $data; }
    public function __destruct() { echo "HeavyObject freed\n"; }
}

$obj  = new HeavyObject(str_repeat('x', 1_000_000));
$weak = WeakReference::create($obj);

echo $weak->get()?->data[0]; // 'x' — object still alive

unset($obj); // refcount drops to 0 (WeakRef doesn't count)
// "HeavyObject freed" — destructor runs

var_dump($weak->get()); // NULL — object is gone

// WeakMap — cache that cleans itself up
$cache = new WeakMap();

class User { public function __construct(public string $name) {} }
class UserStats { public int $loginCount = 0; }

function getStats(User $user, WeakMap $cache): UserStats
{
    if (!isset($cache[$user])) {
        $cache[$user] = new UserStats(); // expensive computation
    }
    return $cache[$user];
}

$alice = new User('Alice');
$bob   = new User('Bob');

$aliceStats = getStats($alice, $cache);
$aliceStats->loginCount = 5;
echo count($cache); // 1

$bobStats = getStats($bob, $cache);
echo count($cache); // 2

unset($alice); // Alice is no longer referenced
echo count($cache); // 1 — Alice's entry automatically removed

// WeakReference in event listener system
class EventDispatcher
{
    /** @var array<string, WeakReference[]> */
    private array $listeners = [];

    public function listen(string $event, object $listener): void
    {
        $this->listeners[$event][] = WeakReference::create($listener);
    }

    public function dispatch(string $event, array $data = []): void
    {
        foreach ($this->listeners[$event] ?? [] as $ref) {
            $listener = $ref->get();
            if ($listener !== null) {
                $listener->handle($data);
            }
            // If $listener is null, the listener was GC'd — silently skip
        }
    }
}