Building caches without artificially keeping objects alive
An ordinary array used as a cache with objects as keys or values prevents those objects from ever being collected by the garbage collector as long as the cache itself exists, and that is exactly what leads to creeping memory leaks in long running processes. WeakMap solves this problem structurally by removing the cache's own influence on the lifetime of the referenced objects.
Table of Contents
- 1. Why ordinary caches keep objects artificially alive
- 2. WeakReference: weakly referencing a single object
- 3. WeakMap: objects as keys without extending lifetime
- 4. Building an object cache with WeakMap in practice
- 5. Interplay with reference counting and garbage collection
- 6. Use cases: metadata, observers, memoization
- 7. Limits of WeakMap: what does not work
- 8. Making object lifecycles visible and debuggable
- 9. WeakMap compared to SplObjectStorage and array
- 10. Summary
- 11. FAQ
1. Why ordinary caches keep objects artificially alive
An obvious pattern for caching expensive computations tied to an object is an associative array or an SplObjectStorage that uses an object as the key and the computation result as the value. The problem: as long as the cache itself exists, it holds a strong reference to every stored object, so its reference count never drops to zero, even if the rest of the application no longer holds any other reference to that object.
In short lived PHP processes, such as a classic PHP-FPM request, this problem rarely shows up, because all memory gets freed at the end of the request anyway. In long running processes, on the other hand, such as worker processes with Swoole, RoadRunner, or long lived CLI commands that process thousands of objects over their lifetime, such a cache grows uncontrolled, because every processed object stays in the cache permanently and can therefore never be collected by the garbage collector.
WeakMap and WeakReference, available since PHP 8.0 and 7.4 respectively, solve exactly this problem. They let you reference an object without increasing its reference count, so the garbage collector can collect the object as soon as no strong reference remains, regardless of whether it is still noted in a WeakMap or as a WeakReference.
2. WeakReference: weakly referencing a single object
WeakReference is the simpler of the two classes and fits the case where a single object should be observed without influencing its lifetime. WeakReference::create($object) creates a wrapper whose get() method either returns the original object as long as it is still alive, or null once it has been collected by the garbage collector. The WeakReference itself does not count as a reference for the reference counter.
A typical use case is an observer that should be informed about an object's lifecycle without itself forcing its existence, for example a debugging tool that checks whether a given object has already been freed at the expected point in time. Since get() can return null at any time, every access to a WeakReference must handle that case explicitly, a simple nullsafe access is usually entirely sufficient.
declare(strict_types=1);
final class ExpensiveResource
{
public function __construct(public readonly string $id) {}
}
$resource = new ExpensiveResource('res-1');
$weakRef = WeakReference::create($resource);
// While $resource still holds a strong reference, get() returns the object
var_dump($weakRef->get()?->id); // string(5) "res-1"
// After releasing the only strong reference, the object becomes collectible
unset($resource);
gc_collect_cycles();
// The WeakReference itself never prevented garbage collection
var_dump($weakRef->get()); // NULL
3. WeakMap: objects as keys without extending lifetime
WeakMap builds on the same principle but lets you manage several objects as keys with arbitrary values, similar to an associative array, except with objects instead of strings or integers as keys and with no influence on their reference counting. As soon as an object used as a key no longer has any other strong reference, PHP automatically removes the corresponding entry from the WeakMap, with no manual cleanup by the developer required.
The syntax is deliberately modeled on ArrayAccess, so a WeakMap can be used like an ordinary array with square brackets: $map[$object] = $value and $map[$object] work directly, as do isset() and unset(). This familiar syntax lowers the entry barrier considerably compared to the older SplObjectStorage class, which uses its own, less intuitive method interface with attach() and detach().
declare(strict_types=1);
/** @var WeakMap<object, array<string, mixed>> $metadata */
$metadata = new WeakMap();
$user = new stdClass();
$user->name = 'Alice';
// Attach metadata to the object without extending its lifetime
$metadata[$user] = ['lastAccess' => time(), 'requestCount' => 1];
echo $metadata[$user]['requestCount'], PHP_EOL; // 1
// Once $user has no other strong reference left, the entry disappears
// automatically — no manual cleanup, no leaked metadata array
unset($user);
gc_collect_cycles();
4. Building an object cache with WeakMap in practice
A practical example is a cache for expensive derived values tied to an object, for example a computed checksum or a serialized representation that is needed repeatedly for the same object as long as it exists. Instead of storing this value as a property on the class itself, which would pollute the class with cache logic, a separate cache class with an internal WeakMap encapsulates this responsibility cleanly separated from the actual domain object.
The decisive advantage over a classic array cache: the cache class itself can exist for as long as needed, for example as a singleton for the entire lifetime of a worker process, without every ever cached object remaining permanently in memory because of it. As soon as a domain object is no longer needed and no other reference exists, its cache entry also disappears automatically, with no explicit invalidation logic at all.
declare(strict_types=1);
/**
* Memoizes an expensive computation per object instance without
* extending the lifetime of the cached objects.
*/
final class ChecksumCache
{
/** @var WeakMap<object, string> */
private WeakMap $cache;
public function __construct()
{
$this->cache = new WeakMap();
}
/**
* Compute and cache a checksum for the given object.
*
* @param object $target Domain object to compute a checksum for.
* @return string The cached or freshly computed checksum.
*/
public function checksumFor(object $target): string
{
return $this->cache[$target] ??= hash('sha256', serialize($target));
}
}
$cache = new ChecksumCache(); // lives for the entire worker process lifetime
$order = new stdClass();
$order->total = 129.90;
echo $cache->checksumFor($order), PHP_EOL; // computed once, cached afterwards
5. Interplay with reference counting and garbage collection
PHP's primary memory reclamation mechanism is reference counting: as soon as an object's counter drops to zero, its memory is freed immediately, with no need to wait for a periodic garbage collector run. WeakMap entries do not count toward the key object's reference counter, which is why an object whose last strong reference is removed gets freed immediately, even if it was still listed as a key in a WeakMap.
The cyclic garbage collector only additionally comes into play with WeakMap in one special case: when objects reference each other cyclically through normal, strong references and their reference count therefore never drops to zero despite the absence of external references. Such cycles are still only resolved by the periodic cycle collector, regardless of whether one of the involved objects also happens to be a key in a WeakMap. For practical work this means: WeakMap does not replace the need to avoid cyclic references in your own domain model, it exclusively solves the problem of artificially extended lifetime caused by cache structures.
6. Use cases: metadata, observers, memoization
Besides caches, WeakMap is excellent for metadata mappings, where additional information about an object needs to be stored without extending the class itself with extra properties. One example: a request tracking system that notes timestamps and processing status for every processed domain object, without that information needing to become part of the actual domain class, which keeps the separation of business logic and infrastructure concerns considerably cleaner.
In the observer pattern, WeakMap prevents a subtle but common problem: a subject that holds its observers in a normal list prevents their garbage collection, even if the actual owner of an observer no longer holds a reference to it. If the observer list is implemented as a WeakMap instead, unsubscribed or forgotten observers can disappear automatically without any explicit unsubscribe method needing to be called, which prevents typical memory leaks in event heavy architectures.
For memoization, meaning caching function results depending on an object argument, WeakMap is also the natural choice over a static array, because memoized values automatically disappear once the underlying object no longer exists, instead of accumulating uncontrolled over the lifetime of a long running process.
7. Limits of WeakMap: what does not work
An important restriction: WeakMap only accepts objects as keys, no scalars like strings, integers or arrays. Attempting to use a non object value as a key raises a TypeError. Anyone needing weak references to primitive values must first wrap them in a wrapper object, which is rarely necessary in practice but occasionally the only way out.
A second important point: WeakMap does not prevent values stored within the map from themselves holding strong references to other objects. If an object is stored as a value that itself holds a strong reference back to the key, a reference cycle can form that is ultimately resolved by the cyclic garbage collector, but not immediately through the weak referencing of the key itself. WeakMap therefore targets specifically the key reference problem, not every conceivable memory leak scenario in complex object graphs.
8. Making object lifecycles visible and debuggable
To verify that a WeakMap actually works as expected and does not keep objects alive longer than necessary, a simple test with gc_collect_cycles() followed by checking the WeakMap size through count($map) works well. If the entry count does not drop to the expected value after removing all external references, that points to an accidentally remaining strong reference somewhere in the code, often a closure that implicitly captured the object via use.
For analysis in more complex applications, memory_get_usage() before and after a processing phase with many created and discarded objects provides a reliable signal for whether WeakMap based caches actually take effect. Stable memory usage across many processing cycles confirms that objects are reliably freed, while continuously growing memory usage instead points to a hidden strong reference persisting elsewhere in the code despite WeakMap usage.
9. WeakMap compared to SplObjectStorage and array
A direct comparison shows which structure is the right choice for object based mappings in which scenario.
| Structure | Prevents GC of the key? | Syntax | Recommendation |
|---|---|---|---|
| Array with object as value | Yes, strong reference | Ordinary array syntax | Only for short lived requests |
| SplObjectStorage | Yes, strong reference | attach() / detach() | When lifetime extension is deliberately wanted |
| WeakMap | No, weak reference | ArrayAccess, like an array | Caches and metadata in long running processes |
| WeakReference | No, weak reference | create() / get() | Observing a single object |
The comparison makes it clear: as soon as an object only needs to serve as a key for additional information, without that mapping being allowed to influence its lifetime, WeakMap is the right choice. SplObjectStorage and ordinary arrays remain sensible where a deliberate lifetime extension is actually wanted.
Mironsoft
PHP memory analysis, worker processes and memory safe cache architectures
Looking for memory leaks in your long running PHP processes?
We analyze object based caches, observer structures and metadata mappings in worker processes and replace artificially lifetime extending structures with WeakMap based, memory safe alternatives.
Cache architecture review
Identifying array and SplObjectStorage based caches with leak risk
WeakMap migration
Rebuilding existing cache and observer structures on weak references
Memory monitoring
Continuous observation of memory_get_usage() in production worker processes
10. Summary
WeakMap and WeakReference solve a structural problem with classic object caches: they let you reference objects without increasing their reference count, allowing the garbage collector to collect objects as soon as no other strong reference remains. For long running processes such as workers with Swoole or RoadRunner, that is the decisive difference between stable memory usage and a creeping memory leak.
WeakMap is particularly suitable for caches, metadata mappings, observer structures and memoization, while WeakReference is meant for observing individual object lifecycles. It remains important that WeakMap does not resolve reference cycles between normal, strong references, but specifically addresses the problem of artificially extended lifetime caused by key references in cache structures.
WeakMap and WeakReference in PHP, the Key Takeaways
No reference counter increase
WeakMap keys and WeakReference targets are not counted as a strong reference.
Automatic cleanup
Entries disappear automatically as soon as the key object has no other reference left.
Familiar array syntax
WeakMap implements ArrayAccess and behaves syntactically like an ordinary array.
Objects only as keys
Scalar values must be wrapped in a wrapper object, direct scalar keys are not allowed.