Skip to content

Object Lifecycle — Junior

What? The object lifecycle is the sequence of phases a Java object passes through from the moment you write new until the JVM reclaims its memory: allocation → field defaulting → constructor execution → reachable use → unreachable → finalization (rarely) → reclamation. How? You drive the early phases with the new operator and constructors. The JVM's garbage collector handles the late phases — you do not free objects manually.


1. The seven phases at a glance

new Foo()             use the object              GC sweeps it
   │                      │                            │
   ▼                      ▼                            ▼
[1 allocate] [2 default] [3 construct] [4 reachable] [5 unreachable] [6 (finalize)] [7 reclaim]
Phase Who triggers it What happens
1 Allocate new operator Heap memory carved out for the object header + fields
2 Default-init JVM All fields set to 0 / null / false
3 Construct constructor body Field assignments and side effects from your <init> code
4 Reachable program holds a reference Object is alive; methods can be called on it
5 Unreachable last reference is dropped GC marks the object as garbage at next collection
6 Finalize (legacy) finalize() Deprecated in Java 9+, removed for new types — avoid
7 Reclaim GC Memory returned to the heap free-list / region

Phase 6 is essentially dead in modern Java. Use Cleaner (Java 9+) or try-with-resources for cleanup. We'll cover that in senior.md.


2. Allocation: what new actually does

Dog rex = new Dog("Rex");

The JVM performs (roughly) four steps:

  1. Resolve the class Dog — load it from the classpath if not already loaded, link it, run <clinit> (static initializers) once.
  2. Allocate memory on the heap big enough for the Dog object's header + fields.
  3. Default-initialize every field to its type's zero value.
  4. Run the constructor (<init>), which assigns final values and runs your code.

The reference returned by new lives on the stack (inside rex). The object itself lives on the heap.

stack             heap
┌──────┐          ┌───────────────────┐
│ rex ─┼────────▶│ header (12-16 B)  │
└──────┘          │ name = "Rex"      │
                  │ age  = 0          │
                  └───────────────────┘

3. Default values vs constructor values

Before the constructor body runs, every field gets its type's default value. Only then does the constructor overwrite some of them.

class Box {
    int width;            // default: 0
    String label;         // default: null
    boolean sealed;       // default: false

    Box(int w) {
        this.width = w;   // overrides default
        // label and sealed keep their defaults
    }
}

Box b = new Box(10);
System.out.println(b.width);   // 10
System.out.println(b.label);   // null
System.out.println(b.sealed);  // false

Why this matters: you can read a field inside a constructor before assigning it, and you'll see the default — not random garbage. Java is memory-safe by design.

Type Default
int, long, short, byte 0
float, double 0.0
char ''
boolean false
any reference type null

4. Constructors are the entry point

A constructor is a special method with no return type and the same name as the class. It runs exactly once per object — at birth.

public class Point {
    private final double x;
    private final double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }
}

If you don't write any constructor, the compiler synthesizes a no-arg constructor for you. The moment you write any constructor, that gift disappears — you must declare a no-arg constructor explicitly if you still want one.

class A { }                 // synthetic no-arg constructor exists
class B { B(int x) { } }    // no-arg gone — `new B()` won't compile

5. Reachability: the heart of garbage collection

An object is reachable if you can follow references from a GC root — a static field, a local variable on a live thread's stack, a JNI reference — and eventually arrive at it.

void scope() {
    Dog rex = new Dog("Rex");   // reachable: local var rex points to it
    rex.bark();
}                                // method returns; rex goes out of scope
                                 // → Dog object is now unreachable, eligible for GC

The garbage collector cannot see "no one will use this object again." It only sees: is there a reference path from a root? If yes, alive. If no, garbage.

This means you can keep an object alive accidentally just by holding a reference you forgot about — that's a memory leak.


6. Going out of scope vs being collected

A common beginner confusion: "the variable went out of scope, so the object is freed." Not exactly.

Dog rex = new Dog("Rex");
// rex still in scope here
rex = null;          // the *reference* is gone; object is now unreachable
                      // → eligible for GC, but GC may not run for ages

"Eligible for GC" ≠ "freed right now." The JVM decides when to actually run the collector. You should not assume any timing — write code that doesn't depend on when objects are reclaimed.


7. The constructor chain

When you call new SubClass(), the constructor chain runs from the topmost ancestor down:

class Animal {
    Animal() { System.out.println("Animal ctor"); }
}
class Dog extends Animal {
    Dog() { System.out.println("Dog ctor"); }
}
class Puppy extends Dog {
    Puppy() { System.out.println("Puppy ctor"); }
}

new Puppy();
// Animal ctor
// Dog ctor
// Puppy ctor

Internally, the first statement of every constructor is implicitly super() unless you write super(...) or this(...) explicitly. This is what guarantees the parent is fully constructed before the child runs.


8. Static vs instance initialization

Two completely different lifecycles:

Static (per class) Instance (per object)
Runs once, at class-load time Runs every new
static fields regular fields
static { ... } blocks { ... } blocks (rare)
<clinit> in bytecode <init> in bytecode
class Counter {
    static int total;          // initialized once at class load
    int id;                    // initialized each new

    static { total = 0; }      // static initializer
    Counter() { id = ++total; } // constructor
}

We dive deeper into the order in middle.md.


9. Common newcomer pitfalls

Pitfall 1: leaking this from the constructor

class Bad {
    Bad() {
        Registry.register(this);   // 'this' is incompletely constructed!
    }
}

Other threads could see a half-built Bad. Don't escape this from a constructor. Move registration to a factory method.

Pitfall 2: assuming finalize() runs

@Override protected void finalize() {     // deprecated; may never run
    closeFile();
}

Use try-with-resources for closeable resources. Don't write finalize().

Pitfall 3: confusing null with "deleted"

Dog rex = new Dog("Rex");
rex = null;
// the variable is now null, but the OBJECT itself isn't "deleted" — it's just unreachable
// the JVM will collect it eventually

Pitfall 4: thinking constructors can return values

public Dog() {
    return new Dog();   // compile error
}

Constructors don't return anything (not even void). They initialize the object that new already allocated.


10. Quick mental checklist

When you write new Foo(args):

  • Is Foo loaded? If not, JVM loads it now (and runs <clinit> for the first time).
  • Allocate a Foo-shaped chunk on the heap.
  • Default-init every field.
  • Run <init> (your constructor body, prefixed by super(...) or this(...)).
  • Return the reference.

When you stop using an object:

  • Drop all references (let them go out of scope, or assign null if needed).
  • Trust the GC. Do not call System.gc() in production.

Question Read
In what order do fields, blocks, and ctors run? middle.md
How does GC actually work? G1, ZGC, generations? senior.md
What happens at the bytecode level (<init>/<clinit>)? professional.md
What does the JLS say about instance creation? specification.md

Memorize this: An object's life is allocate → default → construct → use → unreachable → reclaim. You control the first three. The GC owns the last two. Anything between your new and the last reference dropping is use.