Who frees memory, and when
Think of a library book. While anyone has it checked out, it stays off the shelf. Reference counting is a librarian who keeps a tally: every new borrower adds one, every return subtracts one, and when the tally hits zero, the book goes back on the shelf immediately.
A garbage collector is a different librarian. Nobody keeps a tally. Every so often, the librarian walks through the whole building, notes every book someone is actually holding, and puts all the others back on the shelf in one go.
Both get books back eventually. They differ in when, and in what can trick them.
ARCAutomatic Reference CountingSwift's memory management: each object counts how many strong references point to it and is freed at zero.Open in glossary → (Swift) inserts retain/release operations at compile time; each class instance carries a strong reference count and is deallocated synchronously, on the releasing thread, when it reaches zero. Value types aren’t reference counted (though their heap-backed storage is). Tracing garbage collectionGarbage collectionThe runtime periodically finds objects nothing can reach any more and frees them for you.Open in glossary → (JVM, Android ART, V8) periodically marks everything reachable from roots (thread stacks, static fields, globals, JNI handles) and reclaims the rest; modern collectors are generational and mostly concurrent. ARC trades cycles and per-operation overhead for deterministic, low-latency reclamation; GC trades pauses, CPU and memory headroom for cycle-proof, allocation-cheap memory management.
final class Photo {
let name: String
init(name: String) { self.name = name }
deinit { print("freed \(name)") } // runs the moment it's freed
}
var a: Photo? = Photo(name: "beach")
var b = a // two references: count is 2
a = nil // count 1: still alive
print("a is nil")
b = nil // count 0: freed right now
print("b is nil")
// a is nil
// freed beach
// b is nilclass Photo(val name: String)
var a: Photo? = Photo("beach")
var b = a // two references to one object
a = null
b = null // now unreachable…
println("both null")
// …but it's freed whenever the garbage collector next runs,
// which might be in a millisecond or in a minute. There is no reliable "freed" callback.class Photo {
final String name;
Photo(String name) { this.name = name; }
}
Photo a = new Photo("beach");
Photo b = a; // two references to one object
a = null;
b = null; // now unreachable…
System.out.println("both null");
// …but it's freed whenever the garbage collector next runs.
// finalize() is deprecated; java.lang.ref.Cleaner exists but runs at an unknown time.class Photo {
constructor(readonly name: string) {}
}
let a: Photo | null = new Photo("beach");
let b = a; // two references to one object
a = null;
b = null; // now unreachable…
console.log("both null");
// …but the engine's garbage collector frees it later, at a time it chooses.
// FinalizationRegistry callbacks may run late, or never.- Swift (ARC): each object counts how many strong references point to it. When the count drops to zero, it’s freed immediately, and its
deinitruns. You can see exactly when. - Kotlin, Java, TypeScript (garbage collection): nobody counts. The garbage collector runs from time to time, finds every object the program can still reach (from local variables, static fields, the running code), and frees everything else. “Unreachable” doesn’t mean “freed yet”.
Either way, you never call free yourself. Your job is only to stop pointing to things you no longer need.
Predict the output
When is the Photo freed: right after b is set to nil/null, or later?
With ARC, releasing the last strong reference frees the object on that line: deinit runs before the next statement. That's why Swift code can rely on deinit to stop timers or remove observers (as long as nothing else is still holding the object).
The object becomes unreachable when b is cleared, but the garbage collector frees it later, when it decides to run (usually when new memory is needed). Code must never rely on an object being freed at a particular moment.
The object becomes unreachable when b is cleared, but the garbage collector frees it later, at a time it chooses. That's why cleanup (closing files, stopping listeners) belongs in explicit methods or try-with-resources, not in "when the object is freed".
The object becomes unreachable, but V8 (or another engine) frees it later, on its own schedule. Cleanup goes in explicit dispose/destroy methods or finally blocks, never in "when it's collected".
The stack and the heap
Memory a program uses comes in two main areas:
- The stack holds a function’s local values while it runs. It’s cleaned up automatically when the function returns, instantly and for free.
- The heap holds objects that must live longer than the function that created them: class instances, arrays, closures that are stored. This is the memory ARC and garbage collectors manage.
A Swift struct or a Kotlin Int local variable can live on the stack; anything you create with new, a class initializer, or {} in TypeScript lives on the heap. The details are up to the compiler, but the rule of thumb helps: heap objects are the ones that can leak.
Costs of each model
| ARC (Swift) | Tracing GC (JVM, ART, V8) | |
|---|---|---|
| When memory is reclaimed | Immediately at count zero | At the next collection of that generation |
| Cycles | Leak unless broken with weak/unowned | Collected |
| Runtime cost | Atomic increment/decrement on retain/release (often optimized away) | Allocation is a pointer bump; collection costs CPU and may pause threads |
| Memory headroom | Low: little garbage waiting | Higher: heap sized larger than live data to amortize collections |
| Latency risk | Large deinit cascades on the main thread (freeing a big object graph at once) | GC pauses; jank on Android when many short-lived objects are allocated per frame |
| Deterministic cleanup | deinit is reliable for owned resources | No; use close()/use/try-with-resources/using |
Modern JVM collectors (G1 by default; ZGC and Shenandoah for sub-millisecond pauses) and ART’s concurrent copying collector make pauses rare, so on Android the practical GC concern is allocation churn in hot paths (per-frame allocations in onDraw or during scrolling), which shows up as frequent young-generation collections.
- ARC: compile-time inserted retain/release, deterministic deallocation at count zero,
deinitreliable; cycles leak. - Tracing GC (JVM G1/ZGC, ART, V8): reachability from roots, generational, mostly concurrent; cycles are fine, timing isn’t deterministic. Never rely on finalization (
finalizedeprecated,CleanerandFinalizationRegistryare best-effort). - Costs: ARC → atomic refcounting,
deinitcascades; GC → headroom, pauses, allocation churn in hot paths.
| Swift | Kotlin | Java | TypeScript | |
|---|---|---|---|---|
| Mechanism | ARC | GC (JVM or ART) | GC (JVM) | GC (V8, JavaScriptCore…) |
| Cleanup hook | deinit (deterministic) | use {} / close() | try-with-resources | try/finally, using (TS 5.2) |
| Weak reference | weak / unowned | WeakReference | WeakReference | WeakRef, WeakMap |
Check this part
In Swift, when is an object freed?In Kotlin (on the JVM or Android), when is an unreachable object freed?In Java, when is an unreachable object freed?In TypeScript (JavaScript), when is an unreachable object freed?
Do you have to free objects yourself in these four languages?
Where should cleanup like closing a file live on the JVM or in TypeScript?