Not every object needs to live on the heap. If the JIT can prove an allocated object never "escapes" the method that created it -- never returned, never stored into another (escaping) object's field, never handed to an opaque call -- it can allocate it on the stack instead, where it's freed for free when the method returns, skipping GC entirely for that object. This is escape analysis, and it's one of the biggest wins C2 (HotSpot's JIT) gets from proving things about your code beyond what the bytecode alone says.
Implement analyze_escapes(instructions) over a small straight-line
IR (allocations + assignments + field stores + calls + a return):
("ALLOC", var)--varnow holds a freshly allocated object. EachALLOCis a distinct allocation SITE (numbered0, 1, 2, ...in program order) -- even a variable reused across twoALLOCs refers to two independent sites, one at a time.("ASSIGN", dst, src)--dstnow aliases whatever objectsrccurrently refers to.("STORE_FIELD", obj_var, field, val_var)--obj_var's object gets a reference toval_var's object stored into one of its fields. Ifobj_var's allocation site ever turns out to escape,val_var's site escapes too (transitively) -- because anything that can reach the container can now reach what's stored inside it. Storing into a site that never escapes doesn't force an escape.("CALL", [arg_var, ...])-- an opaque call; every object passed as an argument escapes (the JIT can't see what the callee does with it).("RETURN", var)-- the returned object escapes to the caller.
Return the set of allocation-site ids that never escape by any of these paths -- the stack-allocation candidates.
Example
analyze_escapes([("ALLOC", "x")])
# -> {0} -- purely local, never touched again
analyze_escapes([
("ALLOC", "container"), # site 0
("ALLOC", "payload"), # site 1
("STORE_FIELD", "container", "data", "payload"),
("RETURN", "container"),
])
# -> set() -- container escapes directly (RETURN),
# payload escapes transitively through it
Constraints
- This IR is straight-line (no branches/loops) -- each variable is bound to exactly one allocation site at any point in the walk.
- Escaping propagates through
STORE_FIELDin only one direction: storing an escaping object into a container does NOT make the container escape; storing ANY object into an ALREADY (or eventually) escaping container DOES make the stored object escape. - A variable name is just a name -- reusing it for a second
ALLOCcreates a brand-new, independent site.
Real-world grounding
- HotSpot C2's escape analysis pass -- its
PointsToNodestates (NoEscape,ArgEscape,GlobalEscape) are exactly the "does this allocation site escape" classification this exercise's return value collapses into a binary stack-allocate/don't:src/hotspot/share/opto/escape.cpp
Real C2 builds a full points-to CONNECTION GRAPH over SSA-form IR
(handling branches, loops, and merges a straight-line walk can't
express), and a NoEscape classification enables both scalar
replacement (fields become individual SSA values, no object at all)
and true stack allocation -- this exercise implements the core
"does escaping propagate through a field store" propagation rule
underneath both.