Lifetime and memory7 min · 12 of 16

Construction and destruction order

Trace the exact order fields, base constructors and subclass bodies run in, and stop trusting finalisers to release anything.

new Manager("Ada", 25) is not one step. The runtime allocates the object, gives every field its default value, runs the base constructor to completion, runs this class's field initialisers, and only then runs the constructor body you wrote. Between the second step and the fourth, the object exists, has an address, answers method calls — and is wrong.

Almost every construction bug is code that runs inside that window. Almost every cleanup bug is code that assumes the mirror window at the end is guaranteed.

What a constructor call runs, in order

Java fixes that order, and no field initialiser can jump ahead of it. super(...) runs first, implicitly inserted if you did not write it, so a base constructor always finishes before a single subclass field initialiser runs:

class Employee {
    protected final String name;
    Employee(String name) { this.name = name; }
}

class Manager extends Employee {
    private final long created = System.nanoTime();  // 2. field initialisers
    private final int rate;
    Manager(String name, int rate) {
        super(name);                  // 1. base constructor, to completion
        this.rate = rate;             // 3. constructor body, last
    }
}

Python does not insert anything. __new__ allocates, __init__ initialises, and the base __init__ is a plain method call you write yourself — so where you put super().__init__() decides the order:

class Manager(Employee):
    def __init__(self, name):
        self.rate = 25             # before or after super() — your choice
        super().__init__(name)

There is no zeroing step in Python: an attribute does not exist until it is assigned, so a half-initialised object raises AttributeError rather than handing back a quiet zero — louder, and it names the line.

The half-initialised subclass

The order above has one consequence people meet the hard way. A base constructor that calls an overridable method dispatches to the override — which belongs to a subclass whose fields have not been assigned yet.

class Employee {
    Employee(String name) { System.out.println(describe()); }
    String describe() { return "employee"; }
}

class Manager extends Employee {
    private final int rate;
    Manager(String name, int rate) { super(name); this.rate = rate; }
    @Override String describe() { return "manager at " + rate + "/hr"; }
}
// new Manager("Ada", 25) prints: manager at 0/hr

rate is final and the constructor assigns it 25, and describe() still reads 0, because super(name) finishes before that assignment runs. The blank final is what makes the demonstration honest: write private final int rate = 25; and the field is a constant variable (JLS 4.12.4), so JLS 13.1 has javac fold the simple-name read at compile time — it prints manager at 25/hr without touching a field. Box it (private final Integer rate = 25;) and the folding stops: manager at null/hr.

Since Java 25, flexible constructor bodies (JEP 513) let a constructor run a prologue before super(...) that may assign this class's own fields, so { this.rate = rate; super(name); } compiles and prints 25. It is a narrow escape: the prologue may not read a field or call an instance method, and a declaration-site initialiser still runs after super(...). The rule stands — you cannot know what an override reads.

The base constructor is a caller of your subclass, and it calls at the one moment your subclass is not ready.
A base constructor calls an overridable method and reaches the subclass before the subclass has assigned its fieldsnew Manager(...)fields zeroed: rate = 01 super(...) first2 describe() down3 reads rate = 04 base finishedonly now: this.rate = rateCallerManager ctorEmployee ctor

Scroll to zoom · drag to pan · 0 fits · Esc closes

C++ changes the answer rather than the risk. During Employee's constructor the object's dynamic type is Employee, so a virtual call resolves to Employee::describe and never reaches the subclass — defined behaviour, and a surprise in the opposite direction. Calling a pure virtual from a constructor is undefined behaviour; most implementations abort with pure virtual method called.

The rule that survives all three languages: a constructor may call only methods that cannot be overridden — private, static or final in Java, non-virtual in C++, name-mangled __private in Python. If a subclass has to supply a value, it arrives as a constructor parameter, or a factory constructs first and calls the hook second. Every object is then valid the moment its constructor returns, which is the point of having one (why one object owns a rule).

Destruction is the mirror, where it exists at all

C++ runs the reverse order and runs it deterministically: subclass destructor body, then subclass members in reverse declaration order, then the base destructor, triggered by the closing brace of a scope or by delete. The one trap is deleting through a base pointer with a non-virtual destructor — that is undefined behaviour, and in practice the subclass destructor is simply skipped. A base class meant for inheritance declares virtual ~Base() = default;.

Java has no destructor at all. finalize() was deprecated in Java 9, deprecated for removal in 18 (JEP 421, which also lets you switch finalisation off entirely with --finalization=disabled), and is scheduled to be removed; Cleaner replaced it with the same disclaimer — the action runs on another thread, at an unspecified time, possibly never.

Python's __del__ looks deterministic and is not:

  • It fires when the reference count reaches zero, and reference counting is a CPython implementation detail. On PyPy the same code releases later.
  • In a reference cycle it waits for the cycle collector, which runs on allocation thresholds, not on your schedule. Since Python 3.4 those objects are collected, but the order finalisers run in is arbitrary.
  • At interpreter shutdown module globals may already be None, so a __del__ calling os.close can fail or be skipped.
  • An exception inside __del__ is printed and swallowed, so a failed flush propagates to nobody.

Release belongs to a scope, not a collector

The reliable version puts release in the caller's control flow, where an exception cannot skip it: with in Python, and try-with-resources in Java, which closes in reverse order of declaration and attaches a close failure as a suppressed exception.

class Session:
    def __enter__(self): return self
    def __exit__(self, exc_type, exc, tb): self.sock.close()  # runs on the throw path too

with Session(host) as s:
    s.send(payload)
try (Session s = new Session(host)) {
    s.send(payload);
}   // close() runs on both paths, before the exception leaves

The cost of getting this wrong is not a heap problem, which is why it takes so long to find. Take a service at 1 million requests/day: 1,000,000 ÷ 86,400 ≈ 12 QPS average, and peaks run 2–5× that, so call it 50 QPS. If each request opens a socket that only a finaliser closes, and that generation is collected roughly every 30 seconds, then 12 × 30 = 360 sockets sit open at average load and 50 × 30 = 1,500 at peak — past the common 1,024 open-file limit per process. The process dies with Too many open files while the heap graph looks calm. Deterministic release, or ownership a type enforces, removes the arithmetic entirely; the estimate itself is ordinary back-of-envelope work.

In an interview

What is being tested is whether you know the language runs code you did not write, in an order you can state. Say the order out loud: allocate, defaults, base constructor, field initialisers, constructor body — reverse on the way out where a destructor exists.

Useful phrasing: "I don't call overridable methods from a constructor. If a subclass has to contribute a value, it comes in as a parameter, so the object is valid the instant the constructor returns." And for cleanup: "The resource is released by the scope — with or try-with-resources. A finaliser is at most a leak detector, never the mechanism."

The mistake that loses points: answering "the finaliser closes it". It says you have never watched a service run out of file descriptors with a calm heap.

Check yourself

A Java base constructor calls an overridden method that reads a subclass field declared private final int rate; and assigned this.rate = rate; after super(name). What prints, and what would the same code print in C++?

Java prints 0 — nothing in the subclass runs until super() returns. (Declared private final int rate = 25; it prints 25, but only because of constant folding.) C++ prints the base class's version of the method: during base construction the dynamic type is still the base, so the virtual call never dispatches down.

A worker holds 200 sockets and each must be closed within a second of its request finishing. Do you put the close in __del__? If not, what do you write instead, and what does the cycle collector have to do with the answer?

No. Bind release to the scope that finished the request — __exit__ under a with, or close() in a finally — the only version an exception cannot skip. __del__ waits for the reference count to reach zero, and one cycle (a handler holding the socket, the socket's callback holding the handler) defers that to the cycle collector, which fires on allocation thresholds, not on a one-second deadline. At shutdown globals may already be None and the finaliser is skipped; an exception inside __del__ is swallowed.

1 million requests/day, each holding one socket until a finaliser runs, with that generation collected every 30 seconds and peak at 4× average. How many sockets are open at peak, and what fails first?

1,000,000 ÷ 86,400 ≈ 12 QPS average, so peak ≈ 50 QPS; 50 × 30 = 1,500 sockets held. The file-descriptor limit (commonly 1,024) fails before memory does, so the symptom is Too many open files, not an out-of-memory error.