Language Runtime Memory Models
Every language must answer the same three questions: *where do objects live, how long do they live, and who frees them?* C answers "wherever you put them, as long as you say, and you free them".
Learning Objectives
Describe, language by language, how C, C++, Java, C#/.NET, JavaScript, Python, Rust and assembly/bare-metal code manage object lifetime and memory.
Explain manual management, RAII, reference counting, tracing garbage collection and ownership — and which language uses which.
Correct the common "value types live on the stack" and "del frees the object" misconceptions.
Use each ecosystem's tools to measure memory.
Plain-Language Explanation
Every language must answer the same three questions: *where do objects live, how long do they live, and who frees them?* C answers "wherever you put them, as long as you say, and you free them". C++ answers "objects clean up after themselves when they go out of scope". Java, C#, JavaScript and Python answer "the runtime keeps everything reachable alive and reclaims the rest". Rust answers "the compiler proves at build time who owns what". Each lesson below keeps one language's rules separate — the rules really are different, and mixing them up causes bugs.
Precise Technical Explanation
Object lifetime and storage duration — automatic, static, thread, allocated (Module 4). The program is responsible for every allocated object.
Pointer validity — a pointer is valid only while its object lives and only within the object's bounds (plus one past the end).
Effective type — accesses must use a compatible type (Module 7).
Undefined behavior — the compiler assumes it never happens (Module 9).
malloc family — manual lifetime management (Module 6).
Sequencing — C11 replaced "sequence points" with *sequenced-before*: modifying an object twice, or modifying and reading it, without sequencing (i = i++ + 1;, a[i] = i++;) is undefined behavior. Function arguments are evaluated in an unspecified order.
volatile, atomics and the C11 memory model — volatile for hardware, _Atomic + memory orders for threads (Module 14).
Storage durations — automatic, static, thread and dynamic (new/delete).
Constructors and destructors — every object's lifetime begins when its constructor completes and ends when its destructor starts.
RAII — resources bound to object lifetimes; destructors run deterministically at scope exit and during unwinding.
new/delete and arrays — new T[n] must be released with delete[]; mismatches are UB. Prefer std::vector and std::make_unique<T[]>.
Smart pointers — unique_ptr, shared_ptr, weak_ptr (Module 6).
Move semantics — transfer resources instead of copying (Module 7).
Object lifetime and temporary materialization — prvalues become temporary objects only when needed (C++17 guaranteed copy elision: T x = T(); constructs once); temporaries die at the end of the full-expression unless bound to a reference that extends them.
Exceptions and stack unwinding — Module 5.
Allocators and containers — containers obtain memory through an allocator (std::allocator, std::pmr::monotonic_buffer_resource); growth strategies and reserve decide how often they allocate (measured below).
UB and lifetime pitfalls — dangling references to temporaries, iterator invalidation, string_view of temporaries, using moved-from objects without reassigning them.
JVM overview — javac compiles to bytecode (.class); the JVM loads classes on demand (class loaders), verifies them, interprets bytecode and JIT-compiles hot methods (HotSpot C1/C2, Graal).
Stack frames and heap objects — each thread has a JVM stack of frames holding locals and an operand stack; *objects* are allocated on the heap (implementation overview — see escape analysis).
References and reachability — variables of class type hold references; an object is collectable when no chain of strong references from a GC root (thread stacks, static fields, JNI handles) reaches it. SoftReference, WeakReference and PhantomReference give weaker links.
Garbage collection basics — objects are never freed explicitly.
Generational, concurrent and compacting collectors — most objects die young, so new objects go to a young generation collected often and cheaply; survivors are promoted. G1 (default), ZGC and Shenandoah work concurrently with the application and compact to avoid fragmentation.
Stop-the-world pauses — some phases pause all application threads; modern collectors keep them in the millisecond or sub-millisecond range.
OutOfMemoryError and StackOverflowError — heap exhausted (after GC tried) / thread stack exhausted. Both are Errors, not exceptions you normally catch.
Escape analysis and scalar replacement — the JIT may prove an object never escapes a method and then keep its fields in registers instead of allocating it at all (JIT-dependent).
Heap dumps and profiling — jcmd <pid> GC.heap_dump, -XX:+HeapDumpOnOutOfMemoryError, Eclipse MAT, JDK Flight Recorder, -Xlog:gc.
A Java example for comparison (not run — no JDK on the build machine):
CLR, IL, JIT, runtime — C# compiles to IL in assemblies; the CLR JIT-compiles methods (tiered: quick then optimised; ReadyToRun and Native AOT precompile).
Value types and reference types (with caveats) — structs are values copied on assignment; class instances are heap objects accessed through references. *Where* a value lives depends on its container: a local int is usually in a register or stack slot, but an int field of a class lives inside that heap object, and locals captured by lambdas or used across await move to heap-allocated closures/state machines.
Stack versus heap misconceptions — "value types live on the stack" is false as a rule; the meaningful distinction is copy semantics versus reference semantics.
Managed heap and garbage collection — allocation is a pointer bump in a thread-local allocation context; the GC is generational (gen 0, 1, 2), compacting and mostly concurrent (background GC).
Generations and the large object heap — objects of 85,000 bytes or more go straight to the LOH, logically part of gen 2, which is not compacted by default.
Boxing and unboxing — converting a value type to object or an interface allocates a heap box and copies the value (measured below).
IDisposable, using, deterministic cleanup — the GC reclaims *memory*; files, sockets and handles must be released deterministically with Dispose, which using calls automatically.
Span<T> and Memory<T> — safe pointer+length views over arrays, stack memory or native memory; Span<T> is a ref struct that cannot escape to the heap.
Pinning and interop — fixed, GCHandle.Alloc(…, Pinned) and the pinned object heap stop the GC moving an object while native code holds its address.
stackalloc safety — allocates on the stack; safe when assigned to a Span<T>; keep sizes small and bounded (large or input-dependent sizes risk stack overflow).
Memory profiling — dotnet-counters, dotnet-gcdump, dotnet-trace, Visual Studio diagnostic tools, GC.GetGCMemoryInfo().
Engine pipeline — V8 parses source, generates bytecode for its Ignition interpreter, and tiers hot functions up to optimising compilers (Sparkplug, Maglev, TurboFan) with deoptimisation when assumptions break.
Execution contexts and call stack — each function call pushes an execution context; the stack depth is limited (RangeError: Maximum call stack size exceeded).
Lexical environments and closures — a closure keeps its outer variables alive as long as the closure is reachable.
Reachability-based GC — generational (young "scavenger", old mark-compact), incremental and concurrent (Orinoco).
Event loop — schedules tasks and microtasks; it is an execution mechanism, *not* a memory region.
Promises and asynchronous stack traces — awaiting code resumes in a new turn of the event loop; engines stitch "async stack traces" for debugging.
Typed arrays and ArrayBuffer — contiguous binary buffers with explicit element types; DataView reads with a chosen endianness.
Memory leaks — retained references: global caches, never-removed event listeners, closures capturing large objects, detached DOM nodes.
Heap snapshots and dev tools — Chrome DevTools Memory panel, node --inspect, --heapsnapshot-signal, process.memoryUsage().
CPython overview — the reference implementation compiles to bytecode and interprets it. PyPy, GraalPy and MicroPython manage memory differently; nothing below applies to every implementation.
Names, objects and references — variables are *names bound to objects*; assignment binds a name, never copies an object.
Reference counting and the cyclic garbage collector — every CPython object has a reference count and is freed the moment it reaches zero; a generational cycle collector finds unreachable cycles.
Immutable versus mutable objects — int, str, tuple cannot change; operations create new objects. Small ints and some strings are cached and shared.
Shallow and deep copy — copy.copy copies the outer container; copy.deepcopy copies recursively.
del removes a binding — the object is freed only when its last reference disappears.
Generators and object lifetime — a suspended generator keeps its frame and locals alive until it finishes or is closed/collected.
Memory profiling and retention problems — tracemalloc, sys.getsizeof, gc.get_referrers, objgraph, memray; common culprits: global caches, accumulating lists, cycles with __del__, closures.
Ownership and borrowing, move and copy semantics — each value has one owner; assignment moves non-Copy types; Copy types (integers) are duplicated.
Lifetimes and the borrow checker — references cannot outlive their referents; mutable access is exclusive.
Stack and heap allocation — values live inline (stack or inside their container); Box, Vec, String own heap buffers.
Box<T>, Rc<T>, Arc<T>, Cell<T>, RefCell<T> — unique heap ownership; single-thread / thread-safe reference counting; interior mutability checked at compile time (Cell) or run time (RefCell panics on conflicting borrows).
Drop and deterministic destruction — like C++ destructors; runs when the owner goes out of scope.
Safe versus unsafe code — unsafe blocks may dereference raw pointers and call FFI; the programmer must uphold the invariants there.
Data races and thread-safety traits — Send (may move to another thread) and Sync (may be shared) make data races compile-time errors in safe code.
FFI ownership contracts — when memory crosses a C boundary, document who frees it and with which allocator (Box::into_raw / Box::from_raw, CString::into_raw).
For comparison (not run — no Rust toolchain on the build machine):
Assembly / bare-metal Registers and memory addresses — no types, no lifetimes: just addresses and widths.
Linker script and section placement — Module 3 and 16.
Start-up code: .data copying and .bss clearing — Module 16 implements it.
Stack initialization — the reset handler (or the hardware, on Cortex-M) sets the stack pointer before any C code runs.
Heap setup (if present) — _sbrk in newlib grows a heap between .bss and the stack; many firmwares have no heap.
Interrupt vector table — Module 11.
MMIO and volatile access — Modules 7 and 16.
ABI and calling convention — AAPCS on Arm; assembly routines must preserve callee-saved registers.
Static allocation and memory budgets — everything sized at link time; the map file is the memory report.
import java.lang.ref.WeakReference;
public class Reachability {
public static void main(String[] args) {
byte[] big = new byte[10_000_000]; // heap object, referenced from the stack
WeakReference<byte[]> weak = new WeakReference<>(big);
big = null; // no strong references remain
System.gc(); // a request, not a command
System.out.println(weak.get() == null ? "collected" : "still reachable (GC chose not to run)");
}
}use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node { name: String, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>> }
impl Drop for Node {
fn drop(&mut self) { println!("drop {}", self.name); }
}
fn main() {
let root = Rc::new(Node { name: "root".into(), parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
let leaf = Rc::new(Node { name: "leaf".into(), parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]) });
*leaf.parent.borrow_mut() = Rc::downgrade(&root); // weak back-link: no cycle of strong counts
root.children.borrow_mut().push(Rc::clone(&leaf));
println!("leaf strong={} weak={}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
} // both nodes dropped: the parent link is weakWhy It Matters in Real Systems
Real systems mix languages: a Python service calls C libraries; an Android app mixes Kotlin and C++ through JNI; a browser runs JavaScript on a C++ engine. Bugs appear exactly at the boundaries, where one language's lifetime rules meet another's — and performance problems appear when a programmer applies the wrong mental model (boxing in hot loops, leaked listeners, accidental retention).
Key Terms
Frees objects unreachable from roots Frees objects when their count reaches zero
Collects young objects often and old ones rarely Pause of all application threads for GC
Proving an object does not outlive a method Wrapping a value type in a heap object IDisposable / using
Deterministic cleanup of non-memory resources Function plus the variables it captured
Reference that does not keep its target alive Ownership / borrowing Rust's compile-time lifetime model
Memory Layout and Execution Flow
flowchart LR
subgraph Manual[C]
A1[malloc] --> A2[use] --> A3[free]
end
subgraph RAII[C++ / Rust]
B1[constructor] --> B2[use] --> B3[scope exit → destructor / Drop]
end
subgraph Managed[Java / C# / JS / Python]
C1[new / literal] --> C2[use] --> C3[unreachable] --> C4[GC reclaims later]
endSyntax
int *p = malloc(sizeof *p); free(p); /* C */auto p = std::make_unique<int>(1); // C++: freed at scope exitusing var file = File.OpenRead(path); // C#: Dispose at scope exit
object boxed = 42; // boxing allocates
Span<int> tmp = stackalloc int[16]; // small stack bufferimport copy; b = copy.deepcopy(a); del a # Python: del unbinds a nameconst ref = new WeakRef(obj); // JS: does not keep obj aliveComplete Runnable Example
One program per runtime, each run on the build machine.
C++ — container allocation and RAII (G++ 11.2, C++20), counting every allocation through a replaced global operator new:
C# / .NET 10 — boxing, generations, the large object heap, using, Span and stackalloc:
JavaScript — closures, retention, typed arrays and endianness (Node.js 24, run with --expose-gc so the example can request collections):
Python — names, reference counts, cycles, copies, del and tracemalloc (CPython 3.10):
C — unsequenced modification, caught at compile time: GCC warns about both lines and the printed values are meaningless; another compiler may print something else.
#include <cstdio>
#include <cstdlib>
#include <new>
#include <string>
#include <vector>
static std::size_t allocations = 0, bytes = 0;
void *operator new(std::size_t n)
{
++allocations;
bytes += n;
if (void *p = std::malloc(n)) return p;
throw std::bad_alloc();
}
void operator delete(void *p) noexcept { std::free(p); }
void operator delete(void *p, std::size_t) noexcept { std::free(p); }
static void report(const char *what, std::size_t before_a, std::size_t before_b)
{
std::printf("%-44s %3zu allocations, %7zu bytes\n", what, allocations - before_a, bytes - before_b);
}
int main()
{
std::size_t a = allocations, b = bytes;
{
std::vector<int> v;
for (int i = 0; i < 1000; ++i) v.push_back(i); // grows geometrically
}
report("push_back 1000 ints, no reserve:", a, b);
a = allocations; b = bytes;
{
std::vector<int> v;
v.reserve(1000);
for (int i = 0; i < 1000; ++i) v.push_back(i);
}
report("push_back 1000 ints after reserve(1000):", a, b);
a = allocations; b = bytes;
{
std::string s = "short"; // fits the small-string buffer
}
report
…push_back 1000 ints, no reserve: 11 allocations, 8188 bytes
push_back 1000 ints after reserve(1000): 1 allocations, 4000 bytes
std::string of 5 chars (SSO): 0 allocations, 0 bytes
std::string of 54 chars: 1 allocations, 55 bytes
new int[100] / delete[]: 1 allocations, 400 bytesusing System;
class Resource : IDisposable
{
private readonly string name;
public Resource(string name) { this.name = name; Console.WriteLine($" open {name}"); }
public void Dispose() => Console.WriteLine($" dispose {name} (deterministic)");
}
static class Program
{
static long AllocatedBytes() => GC.GetAllocatedBytesForCurrentThread();
static void Main()
{
int sum = 0;
long before = AllocatedBytes();
for (int i = 0; i < 1000; i++) sum += i; // no allocation
long noBox = AllocatedBytes() - before;
before = AllocatedBytes();
object total = 0;
for (int i = 0; i < 1000; i++) total = (int)total + i; // a new box every iteration
long boxed = AllocatedBytes() - before;
Console.WriteLine($"int loop allocated {noBox} bytes; boxed loop allocated {boxed} bytes ({boxed / 1000} per box)");
var small = new byte[1_000];
var large = new byte[100_000]; // >= 85,000 bytes: large object heap
Console.WriteLine($"generation of a new 1,000-byte array: {GC.GetGeneration(small)}");
Console.WriteLine($"gen
…int loop allocated 0 bytes; boxed loop allocated 24024 bytes (24 per box)
generation of a new 1,000-byte array: 0
generation of a new 100,000-byte array: 2 (LOH is logically gen 2)
small array after one full collection: generation 1 (promoted)
open sensor.log
using the resource
dispose sensor.log (deterministic)
span slice: 4 9 16
arr[3] threw IndexOutOfRangeException: bounds are always checked
(sum 499500)function makeCounter() {
let count = 0; // lives on as long as the closure does
return () => ++count;
}
const next = makeCounter();
next(); next();
console.log("closure kept its variable:", next());
const mb = () => (process.memoryUsage().heapUsed / 1048576).toFixed(1) + " MiB";
global.gc();
console.log("heap used at start:", mb());
const cache = new Map(); // a classic leak: an unbounded cache
for (let i = 0; i < 200000; i++) cache.set("key" + i, { id: i, payload: new Array(10).fill(i) });
global.gc();
console.log("after caching 200,000 objects:", mb());
cache.clear(); // drop the references ...
global.gc(); // ... and the collector can reclaim them
console.log("after cache.clear() and a GC:", mb());
const buf = new ArrayBuffer(4);
const view = new DataView(buf);
view.setUint32(0, 0x11223344); // DataView defaults to big-endian
const bytes = new Uint8Array(buf);
console.log("DataView big-endian bytes:", [...bytes].map(b => b.toString(16)).join(" "));
view.setUint32(0, 0x11223344, true); // little-endian
console.log("DataView little-endian bytes:", [...bytes].map(b => b.toString(
…closure kept its variable: 3
heap used at start: 4.1 MiB
after caching 200,000 objects: 48.5 MiB
after cache.clear() and a GC: 4.1 MiB
DataView big-endian bytes: 11 22 33 44
DataView little-endian bytes: 44 33 22 11
Uint32Array of the same buffer (platform order): 11223344
deep recursion: RangeError: Maximum call stack size exceededimport copy
import gc
import sys
import tracemalloc
import weakref
class Node:
def __init__(self, name):
self.name = name
self.other = None
def __del__(self):
print(f" freed {self.name}")
a = Node("A")
b = a # a second name for the same object
print("same object:", a is b, "| sys.getrefcount(a) =", sys.getrefcount(a), "(includes the call's own reference)")
del a # removes one binding; the object survives
print("after del a, b.name =", b.name)
print("deleting the last name:")
del b # refcount reaches zero: freed immediately
gc.collect() # start from a clean slate: collect garbage left by start-up
gc.disable()
x, y = Node("X"), Node("Y")
x.other, y.other = y, x # reference cycle
del x, y
print("cycle deleted; nothing freed yet (refcounts are still 1)")
print("gc.collect() found", gc.collect(), "unreachable objects")
gc.enable()
outer = [[1, 2], [3, 4]]
shallow = copy.copy(outer)
deep = copy.deepcopy(outer)
outer[0].append(99)
print("shallow copy shares inner lists:", shallow, "| deep copy does not:", d
…same object: True | sys.getrefcount(a) = 3 (includes the call's own reference)
after del a, b.name = A
deleting the last name:
freed A
cycle deleted; nothing freed yet (refcounts are still 1)
freed X
freed Y
gc.collect() found 4 unreachable objects
shallow copy shares inner lists: [[1, 2, 99], [3, 4]] | deep copy does not: [[1, 2], [3, 4]]
weakref alive: True
freed weakly referenced
weakref after del: None
tracemalloc: current 10.6 MB, peak 10.6 MB
largest allocation site: line 47 (10.6 MB)#include <stdio.h>
int main(void)
{
int i = 1;
int a[4] = {0};
i = i++ + 1; /* undefined: i modified twice without sequencing */
a[i] = i++; /* undefined: i read and modified unsequenced */
printf("%d %d %d %d %d\n", i, a[0], a[1], a[2], a[3]);
return 0;
}compiler output:
m15_c.c: In function ‘main’:
m15_c.c:7:7: warning: operation on ‘i’ may be undefined [-Wsequence-point]
7 | i = i++ + 1; /* undefined: i modified twice without sequencing */
| ~~^~~~~~~~~
m15_c.c:8:13: warning: operation on ‘i’ may be undefined [-Wsequence-point]
8 | a[i] = i++; /* undefined: i read and modified unsequenced */
| ~^~
program output:
3 0 0 0 2Line-by-Line Explanation
C++: without reserve, the vector reallocates as it grows (each time to double capacity in libstdc++); reserve(1000) allocates once. libstdc++'s small-string optimisation stores strings of up to 15 characters inside the std::string object, so the short string allocates nothing. new[]/delete[] pair correctly.
C#: GetAllocatedBytesForCurrentThread shows that the plain int loop allocates nothing while the object version allocates a 24-byte box per iteration (on 64-bit .NET). A new small array is in gen 0; a ≥ 85,000-byte array is placed on the large object heap and reported as gen 2; a full collection promotes the surviving small array. using calls Dispose exactly at the end of the block. stackalloc + Span give a bounds-checked view of stack memory.
JavaScript: the closure keeps count alive; the Map keeps 200,000 objects reachable until cleared; DataView writes big-endian unless told otherwise, while Uint32Array uses the platform's (little-endian) order. Unbounded recursion becomes a catchable RangeError.
Python: del a removes a name, not the object; deleting the last reference frees it immediately (__del__ runs). The X/Y cycle is freed only by the cycle collector. Shallow copies share inner objects. Weak references do not keep objects alive. gc.collect() reports 4 unreachable objects here: the two Nodes and their two attribute dictionaries. tracemalloc attributes memory to source lines — the reported line is the list comprehension that built data.
Expected Output
The outputs are shown under each program above. The C#, JavaScript and Python memory figures are from one real run and vary slightly with runtime versions.
Step-by-Step Execution Trace
Python reference counts for the first block:
Names bound to the Node "A" refcount (excluding temporaries) a = Node("A") 0 → __del__ runs, memory freed
Interactive Visualization
The lifetime and ownership visualizer has a C++ unique_ptr move example, a shared_ptr example and a garbage-collected example: step through creation, aliasing, ownership transfer and destruction in each model.
Common Mistakes and Failure Modes
C/C++: assuming GC-like safety; delete instead of delete[]; using moved-from objects.
Java/C#: relying on finalizers for cleanup; forgetting Dispose/try-with-resources; boxing in hot loops; holding references in static collections (leaks in GC languages are *retention*).
JavaScript: event listeners and timers never removed; caches without limits; closures capturing large objects.
Python: expecting del or __del__ to run immediately in every implementation; mutable default arguments shared between calls; cycles with __del__ in old versions (< 3.4 could not collect them).
Rust: Rc cycles (they leak — use Weak); RefCell borrow panics at run time.
Corrected / Safe Pattern
single owner + goto cleanup error path; pair every acquire with a release
RAII everywhere; std::vector/std::string/unique_ptr by default try-with-resources; bounded caches (LinkedHashMap.removeEldestEntry, Caffeine)
using/await using; ArrayPool<T> for large temporary buffers; avoid boxing (generics) remove listeners (AbortController), WeakMap for metadata, bounded LRU caches
with statements; weakref for caches and back-links; generators for streaming ownership by default; Weak for back-links; minimise unsafe and document FFI ownership
Debugging and Diagnostic Techniques
ASan/LSan, Valgrind, heaptrack, mallinfo2, Visual Studio diagnostics
-Xlog:gc*, jcmd GC.heap_info, heap dumps + Eclipse MAT, JFR, VisualVM
dotnet-counters, dotnet-gcdump, dotnet-trace, PerfView, Visual Studio memory profiler
DevTools Memory (heap snapshots, allocation timeline), node --inspect, process.memoryUsage()
tracemalloc, gc module, memray, objgraph, sys.getsizeof
cargo miri (UB in unsafe code), dhat, heaptrack, ASan with nightly
Performance Implications
Manual and RAII memory management have predictable costs at known points; GC moves the cost to collection time and adds memory overhead (heaps typically run at 1.5–3× live data), but makes allocation extremely cheap. Reference counting costs on every pointer copy. The biggest wins in managed languages usually come from allocating less (reusing buffers, avoiding boxing, value types/structs, pooling) rather than tuning the collector.
Security and Safety Implications
Memory-safe languages remove whole classes of vulnerabilities (Module 9) — but their unsafe escape hatches (unsafe Rust and C#, JNI, ctypes, Node native addons) reintroduce them. Treat language boundaries as trust boundaries: validate sizes, document ownership, and fuzz the interface.
Platform and Language Caveats
Java and C# GC behavior depends on the collector chosen and its configuration (heap size, server/workstation GC, container limits).
CPython's behavior (immediate freeing by refcount, small-int cache) is an implementation detail, not Python-the-language.
V8 heap limits depend on the Node version and flags (--max-old-space-size).
Rust's guarantees hold for safe code only.
Real-World Application
Polyglot systems: Python data science on C/Fortran libraries (NumPy buffers), Android apps with JNI, Unity games (C# scripting over a C++ engine, where avoiding GC spikes is essential), Node.js native addons, Rust components inside C++ code bases (Firefox, Chromium, Windows, Android).
Best Practices Checklist
[ ] Learn the actual model of each language you use; never transplant another's.
[ ] Separate memory management from resource management (files, sockets, locks).
[ ] Prevent retention in GC languages: bound caches, remove listeners, use weak references.
[ ] Measure allocation rates and heap size with the runtime's tools.
[ ] Document ownership at every language boundary.
Exercises
E1 · Beginner. In Python, after a = [1]; b = a; b.append(2), what is a? Why?
Answer: [1, 2]: a and b are two names for the same list object; append mutates it.
E2 · Intermediate. Why can a C# int field "live on the heap"? Give a second case where a local int does.
Answer: Fields are stored inside their containing object, and class instances live on the managed heap. A local captured by a lambda (or used across an await in an async method) is hoisted into a compiler-generated heap object.
E3 · Advanced challenge. A Node.js service's heap grows by 50 MB per hour. Describe how you would find the cause.
Answer: Take two or three heap snapshots some minutes apart (DevTools or --heapsnapshot-signal), compare them by constructor to find object types whose counts keep growing, inspect their retainer paths to find the root (a global Map, an array, listener lists, closures), then fix the retention (bound the cache, remove listeners, use WeakMap) and confirm with another pair of snapshots.
Quiz
? In CPython, what does del x do?
- It frees the object x refers to. :: Only if that was the last reference.
+ It removes the name x; the object is freed when its reference count reaches zero. :: Other references keep it alive.
- It schedules the object for the next garbage collection. :: CPython frees by reference count immediately when possible.
- It sets the object to None. :: Other references still see the original object.
? Which statement about C# value types is accurate?
- They always live on the stack. :: A field of a class lives inside the heap object.
+ They have copy semantics; where they are stored depends on their container. :: That is the meaningful distinction.
- They are garbage-collected individually. :: They are not separate heap objects unless boxed.
- They cannot be boxed. :: Converting to object or an interface boxes them.
? Which can leak memory in a garbage-collected language? (select all that apply)
+ An unbounded static cache :: Reachable objects are never collected.
+ Event listeners that are never removed :: The listener's closure and captures stay reachable.
- Local variables that go out of scope :: Unreachable locals are collected.
+ Closures capturin
…Summary
C leaves lifetime to the programmer; C++ and Rust tie it to scope (RAII, ownership); Java, C#, JavaScript and Python reclaim unreachable objects with garbage collectors (CPython adding immediate reference counting). Each has characteristic pitfalls: dangling pointers in C/C++, retention and resource leaks in managed languages, cycles in reference counting. Use each language's own model and tools.
References and Further Reading
*The Java Virtual Machine Specification*; Oracle "HotSpot Virtual Machine Garbage Collection Tuning Guide".
Microsoft Docs, "Fundamentals of garbage collection", "Memory<T> and Span<T> usage guidelines".
V8 blog, "Trash talk: the Orinoco garbage collector".
Python docs: gc, tracemalloc, "Memory Management" in the C API reference.
*The Rust Programming Language*, chapters 4 and 15; *The Rustonomicon*.