Weak reference
A weak pointer is one the collector ignores when computing reachability: the value it points at may be collected, and the pointer then reads as empty[5]. It lets a cache, an interning table or a registry of observers hold something without deciding its lifetime. Weak references are provided by the runtime, since only the garbage collector knows when a value has become unreachable: OCaml has the Weak and Ephemeron modules[2], Java WeakReference and its relatives[3], Haskell System.Mem.Weak[6], and JavaScript WeakRef and WeakMap[4]. A plain weak table is still wrong for memoization when the entry's value mentions its key. Hayes's ephemerons of 1997 solve this by making the value reachable only while the key is independently reachable[1].
Example
An OCaml weak array with two slots, before and after a collection.
Running it.
slot 0: full, slot 1: fullslot 0: full, slot 1: empty
Only the array that is still referenced by the program survives. The weak array's own pointers do not count, so the collector frees the other one and empties its slot.
Semantics
A tracing collector marks everything reachable from the roots by following strong pointers, and frees the rest[5]. A weak pointer is not followed during marking. After marking, the collector clears every weak pointer whose target was not marked. The program sees the weak reference either still pointing to the value or empty; it can never observe a freed value. Since the value may disappear at any collection, code must test the weak reference each time it reads it, and the moment of clearing depends on when the collector runs. Some runtimes distinguish soft references, cleared only under memory pressure, and phantom references, which only signal that a value has been collected[3].
The memo-table problem
A memo table maps arguments to results. With strong references it keeps every argument and result forever, a space leak. Making the keys weak seems to fix this, but the values are still strong, and a value often mentions its key, directly or through a closure. The table then holds the value, the value holds the key, and the key is never collected: the leak survives the weakness. Java's documentation for WeakHashMap warns about exactly this[3].
An ephemeron is a key–value pair with a special rule: the value is treated as reachable only if the key is reachable through a path that does not go through the ephemeron itself[1]. The collector marks from the roots without tracing ephemeron values, then repeatedly marks the values of ephemerons whose keys have become marked, until nothing changes, and finally clears the ephemerons whose keys are still unmarked. This fixed-point computation is what lets a value refer to its own key without keeping it alive.
Three memo tables, filled with 1000 entries while the program keeps only every tenth key.
(* Memoizing a function of large keys. The cached value mentions its key,as memoized results often do. *)type key = { id : int; payload : float array }type value = { source : key; sum : float }let compute k = { source = k; sum = Array.fold_left ( +. ) 0. k.payload }module H = Hashtbl.Make (struct type t = key let equal = ( == ) let hash k = k.id end)module E = Ephemeron.K1.Make (struct type t = key let equal = ( == ) let hash k = k.id end)(* A table with weak keys but strong values, built from a weak array. *)module Weak_keys = structtype t = { keys : key Weak.t; values : value option array }let create n = { keys = Weak.create n; values = Array.make n None }let add t i k v = Weak.set t.keys i (Some k); t.values.(i) <- Some vlet live t = let n = ref 0 in for i = 0 to Weak.length t.keys - 1 do if Weak.check t.keys i then incr n done; !nend(* Fill a table with 1000 entries while the program keeps only every tenthkey, collect, and count the entries whose keys survived. *)let experiment name (add : int -> key -> value -> unit) (count : unit -> int) =let live = ref [] infor i = 0 to 999 dolet k = { id = i; payload = Array.make 100 (float i) } inadd i k (compute k);if i mod 10 = 0 then live := k :: !livedone;Gc.full_major ();Printf.printf "%-26s %4d entries for %d live keys\n" name (count ()) (List.length !live);ignore (Sys.opaque_identity !live)
Running it.
strong table 1000 entries for 100 live keysweak keys, strong values 1000 entries for 100 live keysephemerons 100 entries for 100 live keys
Only the ephemeron table releases the 900 entries the program no longer uses. The ordering matters and is the whole reason the primitive exists: a two-field weak record would let the key die while the value lived, or let the value keep the key alive, and only the ephemeron rule gives the behavior a cache actually wants. JavaScript's WeakMap and OCaml's Ephemeron tables have this semantics[2][4].
Finalization
Weak references are closely tied to finalization, running code when a value is collected. Finalizers are commonly used to release external resources such as file descriptors, but they run at unpredictable times, possibly never, and may resurrect the value they finalize. Guardians in Scheme, from Dybvig, Bruggeman and Eby[7], and JavaScript's FinalizationRegistry[4] instead report collected values to the program, which then does the cleanup itself. Hayes introduced ephemerons as a finalization mechanism, so that the data needed to finalize a key could be kept without keeping the key alive[1]. OCaml's Gc.finalise attaches a finalizer to a heap value[2].
Costs
Weak references make collection more expensive: the collector must find and clear them after marking, and ephemerons require iterating to a fixed point. In a generational collector a weak reference from the old generation to a young value must be treated specially at each minor collection. Weak references also make program behavior depend on when the collector runs, which can hide bugs that appear only under a different heap size or collector setting.
History
Weak pointers appeared in Lisp and Smalltalk systems in the 1970s and 1980s, used for interning symbols and for caches. Java added WeakReference, SoftReference and PhantomReference in version 1.2 in 1998[3]. Dybvig, Bruggeman and Eby introduced guardians in 1993[7], and Hayes described ephemerons in 1997[1]. Peyton Jones, Marlow and Elliott designed weak pointers and finalizers for GHC with key–value semantics like ephemerons[6]. OCaml added the Ephemeron module in version 4.03 in 2016[2], and ECMAScript, which had WeakMap since 2015, added WeakRef and FinalizationRegistry in 2021[4].
see also
- Generational GCA garbage collector that splits the heap by age, because most objects die young: new objects go to a small nursery collected often by copying, and survivors are promoted to a larger heap collected rarely. A write barrier records old-to-young pointers so that a minor collection need not scan the old heap.
- Space leakMemory a program holds longer than it needs to. In a lazy language the usual cause is a chain of unevaluated thunks: a lazy left fold over a list builds one suspended addition per element and only performs them at the end. A heap profile by closure type shows it as a growing THUNK band.
- Lazy evaluationLazy evaluation, or call-by-need, delays computing an expression until its value is needed and then remembers the value, so it is computed at most once. It lets programs define infinite structures and consume only part of them, separates generating candidate values from choosing among them, and avoids work whose result is never used. Its costs are the memory held by unevaluated suspensions, which can cause space leaks, and harder reasoning about when work happens. Haskell is lazy by default; OCaml provides it explicitly through Lazy.t and Seq.
- Cheney's algorithmCheney's algorithm is a copying garbage collector that moves every live object from one half of the heap, from-space, to the other, to-space, in breadth-first order, using the copied objects themselves as the queue: a scan pointer follows the free pointer through to-space, copying whatever the scanned objects point to. It needs no stack or extra memory, leaves a forwarding address in each moved object so shared and cyclic structures are copied once, compacts the live data, and costs time proportional to the live data, not the heap. It is the basis of the young-generation collectors of most language runtimes.
further reading
- [1]B. Hayes, “Ephemerons: a new finalization mechanism”, OOPSLA (1997).
- [2]The OCaml manual, the Weak and Ephemeron modules.
- [3]The Java SE API documentation, java.lang.ref and java.util.WeakHashMap.
- [4]ECMAScript 2021 Language Specification (ECMA-262, 12th edition), WeakRef and FinalizationRegistry.
- [5]R. Jones, A. Hosking, E. Moss, The Garbage Collection Handbook, ch. 12, CRC Press (2011).
- [6]S. Peyton Jones, S. Marlow, C. Elliott, “Stretching the storage manager: weak pointers and stable names in Haskell”, IFL (1999).
- [7]R. K. Dybvig, C. Bruggeman, D. Eby, “Guardians in a generation-based garbage collector”, PLDI (1993).
last updated