wiki

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].

§ 01

Example

An OCaml weak array with two slots, before and after a collection.

Running it.

slot 0: full, slot 1: full
slot 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.

§ 02

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].

§ 03

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].

weak key, strong valuetablekeyvalueweakstrongmentionsthe table keeps the value,the value keeps the key:nothing is ever collectedephemerontablekeyvalueweakonly whilekey livesmentionskey unreachable elsewhere:entry and value both go
With a weak key and a strong value, the table keeps the value alive and the value keeps the key alive. An ephemeron retains the value only while the key is reachable from outside the ephemeron, so when the program drops the key, both go.

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 = struct
type 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 v
let 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; !n
end
(* Fill a table with 1000 entries while the program keeps only every tenth
key, collect, and count the entries whose keys survived. *)
let experiment name (add : int -> key -> value -> unit) (count : unit -> int) =
let live = ref [] in
for i = 0 to 999 do
let k = { id = i; payload = Array.make 100 (float i) } in
add i k (compute k);
if i mod 10 = 0 then live := k :: !live
done;
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 keys
weak keys, strong values 1000 entries for 100 live keys
ephemerons 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].

§ 04

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].

§ 05

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.

§ 06

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

further reading

  1. [1]B. Hayes, “Ephemerons: a new finalization mechanism”, OOPSLA (1997).
  2. [2]The OCaml manual, the Weak and Ephemeron modules.
  3. [3]The Java SE API documentation, java.lang.ref and java.util.WeakHashMap.
  4. [4]ECMAScript 2021 Language Specification (ECMA-262, 12th edition), WeakRef and FinalizationRegistry.
  5. [5]R. Jones, A. Hosking, E. Moss, The Garbage Collection Handbook, ch. 12, CRC Press (2011).
  6. [6]S. Peyton Jones, S. Marlow, C. Elliott, “Stretching the storage manager: weak pointers and stable names in Haskell”, IFL (1999).
  7. [7]R. K. Dybvig, C. Bruggeman, D. Eby, “Guardians in a generation-based garbage collector”, PLDI (1993).

last updated