Beginner11 min readObject-Oriented Foundationslive prototype

Object Lifecycle

Every object is born (constructor), lives while something points to it, becomes unreachable when the last reference is dropped, and is finally cleaned up (destructor or garbage collector).

The idea

What it is

An object isn't permanent. It has a life — it gets created, it does its job for a while, and eventually it goes away. Understanding that journey is what stops you from leaking memory, holding files open forever, or using something that's already gone.

Think of a hotel room. When a guest checks in, the room is constructed for them (clean towels, fresh sheets — the constructor runs). While the guest is staying, the room is active and in use. When the guest checks out, nobody is pointing to that room anymore — it's unreachable. Later, housekeeping comes through, resets the room, and makes it available again — that's cleanup. The room had a clear beginning, middle, and end.

The four stages to remember

Constructed (the constructor runs once) → Active (something still references it) → Unreachable (the last reference is dropped) → Reclaimed (memory freed, destructor/finalizer runs). Every object you ever create walks this path.

Mechanics

How it works

Birth: the constructor runs once

When you write new Connection(), the runtime first allocates memory for the object, then calls its constructor. The constructor runs exactly once per object and its job is initialization — setting fields to valid starting values and acquiring any resources the object needs, like opening a file or a network socket. After it finishes, the object exists and is ready to use.

  • Allocation — space is reserved in memory for the object's fields.
  • Initialization — the constructor stores the passed-in values and opens any resources (a db.connect(), a file handle, a socket).
  • Once the constructor returns, the object is alive and references to it can be handed around.

Life: in active use while referenced

An object stays alive as long as something points to it — a variable, a field on another object, an entry in a list. Call its methods, read its fields, pass it around; it keeps doing its job. The key idea is reachability: if your program can still get to the object through some chain of references, it must be kept alive.

Becoming unreachable: the last reference drops

The moment nothing references an object anymore, it becomes unreachable. The variable went out of scope, you reassigned it, or you removed it from a collection. The object is still sitting in memory, but your program has no way to reach it. It's now garbage — eligible for cleanup.

reachability.ts
let c = new Connection("db://localhost"); // constructed, 1 reference
const also = c;                            // 2 references now
c.query("SELECT 1");                       // active use
c = null;                                  // 1 reference left (via 'also')
// still reachable through 'also' — NOT collected yet
// ...later...
// when 'also' goes out of scope too → 0 references → unreachable

Cleanup: destructor vs garbage collection

How the cleanup happens depends entirely on the language, and this is where the two big worlds split apart:

  • Manual / deterministic (C++, Rust) — cleanup happens at a known moment. In C++, when an object goes out of scope, its destructor runs immediately and predictably. This powers RAII (Resource Acquisition Is Initialization): acquire a resource in the constructor, release it in the destructor, and scope exit guarantees cleanup.
  • Garbage collected (Java, JavaScript, Python, Go, C#) — a garbage collector periodically finds unreachable objects and frees their memory for you. You don't call delete. The catch: it runs whenever it decides to, not the instant the object becomes unreachable.

Finalizers are non-deterministic — don't rely on them

Python's __del__, Java's finalize(), and similar finalizers may run when the GC collects an object — but you can't predict when, and they might not run at all before the program exits. Never put critical cleanup (closing a file, releasing a lock) in a finalizer. Use try/finally, with, try-with-resources, or RAII instead — they guarantee cleanup at a known point.

So the lifecycle is universal — born, live, become unreachable, get reclaimed — but who triggers the final cleanup, and when, differs. In the prototype, Run GC stands in for that sweep: it walks the Unreachable lane, runs each object's finalizer, frees it, and removes the card.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Watch one object travel through four lanes — Constructed → Active → Unreachable → Reclaimed. Press new Object() to build one, use() it, drop reference until its count hits 0 so it slides to Unreachable, then Run GC to reclaim it. The console narrates every step.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Construct and use an object

Press new Object() to build a fresh Conn. Watch it appear in the Constructed lane (the constructor logs Conn#N constructed), then advance into Active with a reference count of 1. Hit use() a couple of times — the card pulses and the console logs each method call. This is an object doing its job while alive.

try 02

Drop the last reference

On an active object, press drop reference to decrement its count. Get it down to 0 and the card slides into the Unreachable lane — nothing in your program can reach it anymore. Notice it still exists in memory; it just isn't gone yet.

try 03

Reclaim with the garbage collector

Press Run GC (or wait for the automatic sweep). Every object in the Unreachable lane has its destructor/finalizer run — logged as GC: Conn#N finalized & reclaimed — and then fades out and disappears. Keep an eye on the live objects stat: it only counts objects that still exist.

In practice

When to use it — and what trips people up

Why lifecycle awareness matters

  • Resource leaks — files, sockets, and DB connections opened in a constructor must be closed. The GC frees memory, but it won't promptly close your file handle. Forget to close and you'll run out of handles.
  • Dangling / use-after-free — in manual-memory languages, touching an object after it's been freed is a crash or a security bug. Know when an object's life ends.
  • Knowing when to free — pick the right tool: deterministic cleanup (finally, with, RAII) for resources; let the GC handle plain memory.

Memory is reclaimed automatically; resources are not

Even in garbage-collected languages, treat external resources (files, sockets, locks) differently from memory. Memory gets cleaned up for you, eventually. Resources need an explicit, deterministic release — close them yourself the moment you're done.

What it gives you

  • You know exactly when an object is safe to use and when it's gone — no use-after-free surprises.
  • You release files, sockets, and locks at the right moment instead of leaking them.
  • You can reason about memory pressure — why objects linger, and what keeps them alive (still-reachable references).
  • You pick the right cleanup mechanism per language: RAII/finally/with for resources, GC for plain memory.

Common mistakes

  • Resource leaks — opening a file/socket in a constructor and never closing it because 'the GC will handle it' (it won't, in time).
  • Use-after-free / dangling references — keeping a pointer to an object after its lifetime ended.
  • Relying on finalizers — putting cleanup in __del__/finalize() and assuming it runs promptly, or at all.
  • Heavy constructors — doing slow or failure-prone work (network calls, big allocations) in the constructor, so creating an object becomes expensive or throws midway.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// JS/TS is garbage-collected — no manual delete, no reliable destructor.
class Connection {
  private open = false;

  constructor(public url: string) {
    this.open = true;
    console.log(`Conn(${url}) constructed — socket opened`);
  }

  query(sql: string) {
    if (!this.open) throw new Error("use after close");
    return `result of ${sql}`;
  }

  // GC won't reliably call this — close explicitly instead.
  close() {
    this.open = false;
    console.log(`Conn(${this.url}) closed`);
  }
}

let c: Connection | null = new Connection("db://localhost");
c.query("SELECT 1");   // active use
c.close();             // deterministic cleanup — do it yourself
c = null;              // last reference dropped → unreachable
// the GC will reclaim the memory at some later, unspecified time.

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Nothing is scored or saved.

question 01 / 04

What runs exactly once when an object is first created, to set up its starting state?

question 02 / 04

In a garbage-collected language, when does an object become eligible for collection?

question 03 / 04

Why shouldn't you rely on Python's __del__ or Java's finalize() to close a file?

question 04 / 04

In C++, when does an object's destructor run for a local variable?

0/4 answered