skip to content

An object on the collected heap is referenced only from code outside that heap — what keeps it alive?

level: seniorimportance: should knowfreq 38%

answer

  1. the collector cannot read foreign frames
  2. registration turns a reference into a root
  3. an unknown address keeps nothing alive
  4. handle table scanned as roots
  5. forgetting to release leaks invisibly

basics

~20 s

A registered handle. Code outside the collected heap holds an opaque handle that the runtime records in a table, and that table is scanned as a root set. A raw address held abroad is invisible to the collector and keeps nothing alive.

solid answer

~50 s

The collector can enumerate roots in frames it understands, because its own compiler emitted them. Frames belonging to code outside the collected heap carry no such description, so the collector cannot tell which of their words are references — and it will not guess. The standard arrangement is explicit **registration**: the foreign side receives an opaque handle, the runtime records it in a handle table, and that table is scanned as roots on every collection. Two symmetrical failures follow. Forget to register and the object can be reclaimed, or relocated, under an address the foreign side still holds — a stale pointer and, usually, memory corruption. Forget to release and you have created a root with no natural end: the object and its whole closure stay alive, and the retaining edge lives in a runtime-owned table rather than anywhere in your own data structures.

go deeper

for a junior

The takeaway is that a reference only counts if the collector knows about it. Code outside the collected heap has to register what it holds, and release it afterwards.

for a middle

Explain why registration exists: the collector has no description of foreign frames, so it cannot identify references there, and it requires a declared handle table it can scan as roots instead.

for a senior

Show that you can diagnose both directions — a missing registration as corruption under a stale address, a missing release as growth whose retaining edge is not in your object graph at all — and that you would audit boundary crossings in pairs.

for a principal

The architectural point is that a foreign boundary imports a second, manual lifetime discipline into a runtime that had none. Decide where that discipline is enforced — a single wrapper that acquires and releases in one place — rather than leaving it to every call site.

## The collector's blind spot Root enumeration works because the collector knows the layout of the frames it is scanning: which words at this point in this function hold references. That knowledge comes from the same toolchain that produced the code. A frame belonging to code outside the collected heap — a library called through a foreign interface, a driver, an operating-system callback — was produced by a different toolchain that emitted no such description. The collector therefore has two options, and only one of them is used by design: guess which words look like references, or require the foreign side to declare what it holds. ## Registration, step by step 1. Managed code hands an object across the boundary; instead of a bare address, the runtime issues an **opaque handle** and records an entry in a handle table. 2. The foreign code stores the handle, not the object address, and calls back into the runtime whenever it needs to read the object. 3. Every collection scans the handle table as part of the root set, so each registered object is marked and its closure retained. 4. When the foreign side is finished, it **releases** the handle; the entry is removed, and the object becomes collectable as soon as nothing else reaches it. The handle is doing two jobs at once: it is a root, and it is an indirection. The indirection matters when the collector may relocate objects — the table entry can be updated after a move, while an address already copied into foreign memory cannot be. ## The two failure modes | What goes wrong | Immediate mechanism | Symptom | |---|---|---| | Object never registered | No root reaches it; it is reclaimed or relocated | Foreign code dereferences a stale address; corruption or a crash, often far from the cause | | Handle never released | The table entry stays a root forever | The object and its closure are retained; memory grows with the number of leaked handles | | Handle released too early | The last root disappears while foreign code is still using it | Same as never registering, but intermittent and timing dependent | | Handle held across an unexpectedly long call | Legitimate root, but pinned longer than intended | Retention spikes that track the slowest foreign operation | The second row deserves emphasis as a diagnosis problem. When a reference inside your own structures retains an object, the retaining edge is somewhere in the program's own object graph, and reasoning about it is ordinary reasoning about your data. When a handle retains an object, the edge originates in a runtime-owned table, so the graph you wrote does not explain it — nothing in your code points at the object, and it is still alive. ## Why the collector cannot simply scan the foreign side - **No map of the frames.** It does not know which words are references, and treating everything as a possible reference has its own costs. - **The memory may not even be a stack it can walk.** References can sit in structures allocated by that code, in thread-local storage it knows nothing about, or in a device's memory. - **Lifetime is unknown.** Even if it found an address, it could not know when the foreign side stops caring about it — which is exactly what an explicit release announces. ## Relocation and the address that must not move If the collector may move objects to compact the heap, then any address that has escaped into memory the collector cannot rewrite must be prevented from becoming stale. Runtimes solve this in different ways — some forbid the object from moving for the duration, some copy the contents across the boundary instead and let the original move freely, some hand out only handles and require every access to go back through the table. Each choice trades safety against a copy or against the fragmentation that immovable objects cause. The property that all of them preserve is the same one: **no address the collector cannot update is allowed to outlive the object's current position**. ## The rule to carry A reference is only a root if the collector knows about it. Holding an address somewhere the collector cannot see does not keep an object alive — it merely makes the eventual reclamation dangerous. Registration is what converts a foreign reference into a root, and release is what ends it; both are explicit, and both are the program's responsibility.

  • Why is a leaked handle harder to track down than a forgotten reference in program data?
    Because the retaining edge is not in the object graph you wrote. Walking your own structures never explains why the object is alive; the path starts in a runtime-owned table populated by a boundary crossing. You have to look at the root categories rather than at your data to see it at all.
  • What breaks if foreign code keeps a raw address instead of a handle?
    Nothing keeps the object alive, so it may be reclaimed and its memory reused, and in a runtime that compacts, it may also be moved while the address is retained. Either way the foreign side later reads or writes memory that no longer belongs to that object, which is corruption rather than a clean failure.
  • Does registering a handle prevent the collector from moving the object?
    Not by itself — the handle is an indirection the runtime can update after a move. What prevents movement is an address escaping into memory the collector cannot rewrite, which is why designs either pin such objects, copy the data across the boundary, or force every access back through the table.

saying these in an interview costs you the question

  • Assumes any address held anywhere keeps an object alive
  • Thinks the collector can scan foreign frames like its own
  • Believes a registered handle never needs releasing
  • Says registration records the object without making it a root
  • Treats a crash after reclamation as a collector defect