A JVM maintains per-thread execution frames and shared areas for objects and class-related information; object lifetime follows reachability rather than a source variable’s name.
Java JVM memory: stack frames, heap objects, and reachability
Java 8+. Use a JDK that supports this release.
A reference is not an object copy
A local reference can reach the same array as a field or another local. The array stays usable while at least one live route to it remains. Returning from the method that allocated it does not necessarily make it unreachable.
The sample returns a snapshot of an internal byte buffer. The copy costs time and storage, but it prevents a reader from overwriting the owner’s retained array. Returning the original array would expose a second writer.
A stack frame holds method execution state. Do not use that statement to claim every primitive always lives on a stack or every source-level allocation becomes a distinct heap allocation: a VM can optimize representation while preserving observable behavior.
Retained size matters
A cache entry may look small while reaching a large payload, an enclosing instance, or a chain of listeners. Shallow size is the entry itself. Retained memory concerns what becomes collectible when that ownership route disappears.
Static collections, thread-local state, and callback registries deserve explicit bounds and removal policies. A service that accepts unlimited unique keys can retain unlimited values even when each request eventually returns.
Heap exhaustion and stack exhaustion have different failure paths. Deep unbounded recursion can exhaust a thread’s stack; accumulating retained payloads can exhaust heap capacity. Increasing one budget does not necessarily fix the other.
Working program
import java.util.Arrays;
public class PayloadOwnership {
static final class PayloadStore {
private final byte[] payload;
PayloadStore(byte[] payload) { this.payload = Arrays.copyOf(payload, payload.length); }
byte[] snapshot() { return Arrays.copyOf(payload, payload.length); }
}
public static void main(String[] args) {
byte[] imported = {10, 20, 30};
PayloadStore store = new PayloadStore(imported);
imported[0] = 99;
byte[] reader = store.snapshot();
reader[1] = 88;
System.out.println(Arrays.toString(store.snapshot()));
}
}Output
[10, 20, 30]Cost and design choices
Copying n bytes takes O(n) work and O(n) destination storage. A snapshot isolates byte values because the elements are primitives; copying an array of mutable object references would not isolate those objects.
This ownership test proves a visible mutation boundary. It does not measure allocation bytes, GC pauses, or memory leaks; those require workload-specific diagnostics.
Common Mistakes
- Do not assume leaving scope closes a file or socket.
- Do not equate shallow size with retained size.
- Do not claim exact object byte sizes without a VM-specific measurement.
Connect the contracts
Follow reference values before deciding which objects remain reachable.
Use memory diagnostics to distinguish reachable retention from a capacity estimate.
