Object lifecycle

Every object you create with new goes through the same life: memory is reserved and zero-filled, the constructor fills it in, your code uses it, and one day the last reference to it is lost. You'll see each stage inside the JVM, learn exactly what “reachable” means, why finalize() is deprecated, how try-with-resources and Cleaner release resources properly, and then practise with 8 problems.

Module 8 · JVM Memory⏱ 22 minIntermediate8 practice problems
1 The story

Who deletes the old orders?

Riya is building the canteen's pre-order app. Every time a student edits an order, her code does order = new Order(...). After testing, she has created 2,000 Order objects but never deleted one. In C, her friend Kabir says, she would have had to call free() on every one of them. She starts to worry that her app is slowly eating the server's memory.

1. Allocated2. Constructed3. In use4. Unreachable5. Collected id = 0item = nullall zeros id = 7item = "Dosa"fields filled id = 7item = "Dosa"order → here id = 7item = "Dosa"no references space reused order order = new … new does steps 1 and 2 done by the GC, not you myphatshala.in
Riya

Sir, I keep replacing orders with new ones. Shouldn't I be deleting the old Order objects somewhere?

Arjun Sir

Java has no delete. The moment no variable, field or array can reach the old order any more, it becomes garbage, and the garbage collector reclaims its memory when it needs space.

Riya

So I can't leak memory in Java at all?

Arjun Sir

You can, if you keep references you don't need, like storing every order in a static list forever. Today we follow one object from new to collection, so you'll know exactly when it is alive and when it is gone.

2 Definition

What is the object lifecycle?

Formal definition (write this in exams)

The lifecycle of a Java object has four stages. Creation: new allocates memory on the heap, initialises every field to its default value, and then runs the constructor chain (superclass constructor, field initializers and instance initializer blocks, constructor body). Use: the object is reachable, that is, there is a chain of references to it from a GC root, such as a local variable in an active frame or a static field. Unreachable: no such chain exists any more, so the object is eligible for garbage collection. Collection: the garbage collector reclaims its memory at a time of its own choosing, possibly never if the program ends first.

In simple words

An object lives as long as someone can still find it. Lose every way to reach it, and it is rubbish waiting to be swept up. You never sweep; the JVM does, whenever it decides to.

Words you'll meet in this lesson

GC rootA starting point the collector trusts: locals in live frames, static fields, running threads.
ReachableSome chain of references leads to the object from a GC root.
UnreachableNo such chain exists. The object is eligible for collection.
Island of isolationObjects that refer to each other but can't be reached from any root. Still garbage.
finalize()An old Object method called before collection. Deprecated since Java 9.
CleanerA Java 9 class that runs a cleanup action after an object becomes unreachable.

Lifecycle facts at a glance

Creation orderAllocate + zero-fill → parent constructor → field initializers and { } blocks (in source order) → constructor body
Becomes unreachable byReassigning the variable, setting it to null, the method returning (its frame is popped), or the object holding it becoming unreachable
GC rootsLocal variables and parameters of active frames, static fields of loaded classes, live threads, references held by native code
When collectedNot immediately. Only when the GC runs, and never guaranteed before the program exits
finalize()Deprecated in Java 9, marked for removal in Java 18. Avoid it
Release resources withtry-with-resources and AutoCloseable (Java 7); Cleaner (Java 9) only as a safety net

Exam tip: “Is the object eligible for GC?” and “Has it been collected?” are different questions. After o = null; the old object is eligible; whether and when it is collected is up to the JVM. Answer “eligible”, never “destroyed”.

3 Types and ways to use it

Three ways to lose an object, and the right way to clean up

Reassignment

order = new Order(102);
The variable now points elsewhere. The old object loses that reference.

Point elsewhere

Set to null

order = null;
Useful for long-lived fields or arrays. Rarely needed for locals.

Drop it

Scope end

A method returns and its frame is popped. Objects referenced only by its locals become unreachable.

Frame popped

Releasing resources: try-with-resources and Cleaner

Memory is freed by the GC, but files, connections and sockets must be closed by you, at a known moment. Here both mechanisms run, in a predictable order.

import java.lang.ref.Cleaner;

public class Main {
    static final Cleaner CLEANER = Cleaner.create();

    static class ReportFile implements AutoCloseable {
        private final Cleaner.Cleanable cleanable;

        ReportFile(String name) {
            System.out.println("open " + name);
            Runnable action = () -> System.out.println("cleanup " + name);
            cleanable = CLEANER.register(this, action);   // safety net
        }

        @Override
        public void close() {
            cleanable.clean();                             // runs the action now, only once
        }
    }

    public static void main(String[] args) {
        try (ReportFile f = new ReportFile("sales-june.csv")) {
            System.out.println("writing rows");
        }                                                  // close() is called here
        System.out.println("after try");
    }
}
open sales-june.csv writing rows cleanup sales-june.csv after try
finalize()Cleanertry-with-resources
SinceJava 1.0Java 9Java 7
StatusDeprecated (9), for removal (18)Recommended safety netRecommended main tool
When it runsSome time after unreachable, maybe neverOn clean(), or some time after unreachableExactly at the end of the try block
Can bring the object back?Yes (resurrection), a classic bugNo: the action can't see the objectNot applicable
Cost for the GCObjects need extra GC cyclesSmallNone
Use it forNothing in new codeBackup cleanup of native resourcesFiles, connections, streams, locks

finalize() is a trap: it may run late, in an unknown thread, or not at all; it slows down collection; and an exception thrown inside it is ignored. Since Java 18, javac warns [removal] finalize() in Object has been deprecated and marked for removal, and java --finalization=disabled turns it off completely.

4 Step by step

From new to garbage, one stage at a time

  1. Make sure the class is ready

    Order o = new Order(101, 499.0);
    If Order hasn't been loaded and initialised yet, the JVM does that first (Lesson 8.4). Now it knows exactly how many bytes an Order needs.
  2. Allocate and zero-fill

    The bytecode instruction new reserves the memory on the heap.

    The JVM writes an object header (which class it belongs to, data for hashing and locking) and sets every field to 0, 0.0, false or null. That's why fields have default values and why a constructor can never see garbage.
  3. Run the constructor chain

    Order(int id, double amount) {
        super();              // Object() first
        // then field initializers and { } blocks
        this.id = id;         // then the body
        this.amount = amount;
    }
    invokespecial Order.<init> pushes a constructor frame with this in slot 0. Parent constructors run first, so a parent's fields are ready before the child's code uses them.
  4. Use it while it is reachable

    double total = o.amount * 1.18;
    The reference in o (a slot in main's frame, which is a GC root) keeps the object alive. So would a reference from a static field, from an array, or from another reachable object.
  5. Lose the last reference

    o = new Order(102, 99.0);   // or o = null; or the method returns
    Nothing happens immediately: no method is called, no memory is freed. The object is simply no longer found when the GC next traces from the roots, so its space is reused (Lesson 8.3).
5 See it happen

One order's life: both views

Left: the code and what it tells you. Right: the GC roots, the heap, and how the collector decides what lives.

From new to collected 6 steps
What you see Order o = new Order(101, 499.0); double t = o.amount * 1.18; o = new Order(102, 99.0); // later: the heap is getting full // memory for order 101 reserved // id=0 amount=0.0 status=null // constructor fills the fields id=101 amount=499.0 status=NEW t = 588.8199999999999 // order 101 is in use o now refers to order 102 // 101: no references left // a GC cycle starts // (you see nothing at all) // 101's memory is reused // no message, no callback new reserves zeroed memory first. Then the constructor fills it in. Reachable from main, so it is alive. Reassigned: order 101 is now garbage. The collector starts from the roots. Garbage is reclaimed, silently. myphatshala.in
Behind the screen GC roots Heap main() frame o = (not yet set) o → #1 o → #2 Order(…) frame this → #1 static fields (also roots) #1 Order id=0 amount=0.0 status=null id=101 amount=499.0 status="NEW" unreachable ✗ not marked free space #2 Order id=102 amount=99.0 status="NEW" ✓ marked Header written, every field zero or null. The constructor frame sets the fields via this. Root (main's frame) → #1: reachable. #1 still exists, but no path from a root. Mark: follow arrows from roots. #1 isn't found. Unmarked memory is reclaimed for new objects. myphatshala.in
6 Real-world use

Object lifecycles in real systems

Most bugs here are not about creating objects. They are about holding on to objects (or resources) longer than needed.

Watch it with an example: the listener that never leaves (a memory leak)

Each opened screen registers a listener in a static list. Closing the screen forgets to remove it. Watch why the garbage collector can't free those objects.

Reachable objects are never collected 6 steps
Animated example GC roots static listeners main's locals #1 ArrayList (listeners) size = 0 size = 1 size = 2 size = 2 size = 2 size = 0 #2 Listener → Screen #4 (holds the screen's images) #3 Listener → Screen #5 (holds the screen's images) #2 screen closed, still listed reachable → NOT collected GC runs… #2 survives listeners.remove(l) on close → GC frees them myphatshala.in
connection pool

1. Database connections

A pool holds a few expensive connections. Code borrows one inside try-with-resources, and close() returns it to the pool at once. Forget to close it and the pool runs dry long before memory does.

Request→try (Connection c = pool.get())→Returned on close
req → Order → Cart → Response

2. Short-lived request objects

A web request creates dozens of objects (parsed input, DTOs, responses) that become unreachable when the handler method returns. This is the normal, healthy pattern: modern collectors are built for many short-lived objects.

HTTP request→objects in handler frames→Garbage after return
listener

3. Event listeners

A screen registers itself as a listener with a long-lived event bus. If it never unregisters, the bus keeps the whole screen (and everything it references) reachable after it is closed.

Screen closed→bus.unregister(this)→Screen can be collected
native bufferCleaner → free

4. Native memory behind Java objects

Some objects own memory outside the heap, such as direct ByteBuffers used for fast I/O. The JDK uses a cleaner mechanism to free that native memory once the Java object becomes unreachable.

ByteBuffer.allocateDirect→unreachable→Native memory freed

Examples are simplified. Frameworks such as Spring manage many object lifecycles for you (creating, wiring and closing beans), but the same reachability rules apply underneath.

7 Run the code

Three orders, three endings

public class Main {
    static class Order {
        int id;
        double amount;
        String status = "NEW";

        Order(int id, double amount) {
            System.out.println("  constructor: status is already " + status + ", id is still " + this.id);
            this.id = id;
            this.amount = amount;
        }
    }

    static double billFor(int id, double price) {
        Order temp = new Order(id, price);     // only this frame refers to it
        return temp.amount * 1.18;
    }

    public static void main(String[] args) {
        System.out.println("Creating order 101");
        Order current = new Order(101, 499.0);
        System.out.println("Created: " + current.id + " " + current.status);

        current = new Order(102, 99.0);        // order 101 is now unreachable
        System.out.println("Current is now " + current.id);

        double bill = billFor(103, 1000.0);    // order 103 dies with the frame
        System.out.println("Bill: " + bill);

        current = null;                        // order 102 is now unreachable
        System.out.println("No orders referenced");
    }
}
Creating order 101 constructor: status is already NEW, id is still 0 Created: 101 NEW constructor: status is already NEW, id is still 0 Current is now 102 constructor: status is already NEW, id is still 0 Bill: 1180.0 No orders referenced
  • String status = "NEW";A field initializer: it runs before the constructor body, so the body already sees NEW.
  • id is still 0Zero-filled memory: the body hasn't assigned id yet.
  • Order temp = new Order(id, price);Referenced only by billFor's frame, so it becomes unreachable on return.
  • current = null;Drops the last reference to order 102. No output: the JVM never announces garbage.
8 Line by line

Build a ticket, replace it, lose the old one

Recorded from the real JVM. Watch ticket #1 start with zeros, get its field initializer, then its constructor body, and vanish from the heap the moment t points elsewhere.

Worked examples: real problems, solved and traced in memory

Three business programs where objects are born, used and abandoned. In each trace, an object disappears from the heap view at the exact step where the last reference to it is lost: that is the moment it becomes garbage.

Example 1Retail · order processing

Three ways an order object becomes garbage

Problem statement

An order screen creates an order, replaces it with a corrected one, computes a GST bill using a temporary order inside a helper method, and finally clears the current order. Trace when each Order object becomes unreachable.

Input
order 1: Rs 100, order 2: Rs 250, bill for order 3: Rs 500 + 18% GST
Expected output
Bill: Rs 590.0

Approach

  1. Reassignment: pointing current at a new Order abandons the old one.
  2. Scope end: the Order made inside billWithGst is referenced only by that method's local temp; when the frame pops, it is unreachable.
  3. null: setting current = null drops the last reference to order 2.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 2Food delivery · rider app

Two objects that point at each other still die

Problem statement

An order and its rider reference each other (order.rider and rider.delivering). The rider is also in the on-duty list. Show that the pair stays alive while the list holds the rider, and that both become garbage together when the shift ends, even though they still point at each other.

Input
order 501, rider "Kabir", onDuty = 1 slot
Expected output
Kabir has order 501
Shift over

Approach

  1. Create the order and rider, link them both ways, and put the rider in the onDuty array.
  2. Clear the local variables: the pair is still reachable through onDuty, a path from a GC root.
  3. Clear the array slot: now no path from any root reaches them. Their references to each other don't count; Java's GC is based on reachability, not on counting references.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

Example 3Payments · receipts

A temporary builder dies, the receipt is kept by a static list

Problem statement

A payment service builds a receipt text for each transaction with a StringBuilder and stores the final String in a static list for the end-of-day report. Trace which objects survive each call.

Input
txn 71: Rs 450, txn 72: Rs 1200
Expected output
[TXN-71 Rs 450, TXN-72 Rs 1200]

Approach

  1. The StringBuilder is a scratch object: only the local sb refers to it.
  2. The receipt String is added to the static list receipts. Static fields are GC roots, so whatever they reference stays alive.
  3. Even after main sets last = null, the receipt survives because the list still holds it.

Solution: run it live

What happens in memory, step by step

Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.

9 Quiz and revision

Check yourself

Quick revision

Lesson 8.2 in one screen

  • Lifecycle: created → in use (reachable) → unreachable (eligible) → collected (whenever the GC decides).
  • new: allocate + zero-fill, then parent constructor, field initializers and blocks, constructor body.
  • Reachable = a chain of references from a GC root (live frames' locals, static fields, threads).
  • Unreachable via reassignment, null, or scope end. Mutual references don't matter (island of isolation).
  • Nothing runs when an object becomes garbage; there is no destructor in Java.
  • finalize(): deprecated in 9, for removal in 18. Don't use it.
  • Release resources with try-with-resources (AutoCloseable); use Cleaner only as a safety net.

Interview and viva questions

When does an object become eligible for garbage collection?

When it is no longer reachable through any chain of references from a GC root: local variables and parameters of active frames, static fields, live threads and native references. Typical causes are reassigning the variable, setting it to null, or the method that held it returning. Eligibility doesn't mean immediate collection.

Does Java have destructors?

No. Memory is reclaimed by the garbage collector without calling anything on the object. finalize() looked like a destructor but was unreliable and is deprecated. For cleanup of resources, implement AutoCloseable and use try-with-resources.

Why was finalize() deprecated?

There is no guarantee when, on which thread, or whether it runs at all. It delays reclamation (objects with finalizers need extra GC work), exceptions inside it are silently ignored, and it can “resurrect” an object by storing this somewhere. Java 9 deprecated it and Java 18 marked it for removal.

What is an island of isolation?

A group of objects that reference each other but that no GC root can reach, for example an Order and a Rider pointing at each other after every external reference is gone. Because Java's collectors trace from roots instead of counting references, the whole island is garbage.

How does Cleaner differ from finalize()?

You register an object and a separate cleanup action with a Cleaner. The action runs once, either when you call clean() (typically from close()) or some time after the object becomes unreachable. The action never receives the object, so it can't resurrect it. It is meant as a safety net behind try-with-resources, not a replacement.

Practice problems

These problems are about the life of an object: what runs when it is created, how long it stays reachable, and how its resources are released. Predict the order of the printed lines before you run anything. The last three are business tasks.

P1Easy

Count the objects created

Count how many Order objects have been created, using a static counter that the constructor increases.

Input
create 3 orders, one of them inside a loop body
Expected output
Temporary order 2
Temporary order 3
First order id: 1
Orders created: 3
Show solution and explanation
public class Main {
    static class Order {
        static int created = 0;
        final int id;
        Order() {
            created++;
            id = created;
        }
    }

    public static void main(String[] args) {
        Order a = new Order();
        for (int i = 0; i < 2; i++) {
            Order temp = new Order();
            System.out.println("Temporary order " + temp.id);
        }
        System.out.println("First order id: " + a.id);
        System.out.println("Orders created: " + Order.created);
    }
}
Temporary order 2 Temporary order 3 First order id: 1 Orders created: 3

How it works: created is static, so there is one copy for the whole class, not one per object. Each new Order() runs the constructor once. The two temporary orders become unreachable at the end of each loop iteration, but they were still created, so the count is 3. Counting creations is not the same as counting live objects.

P2Easy

What runs first when you say new?

Print a line from a field initializer, an instance initializer block and the constructor, and see the order in which they run.

Input
new Invoice()
Expected output
1. field initializer, gstRate was 0.0
2. initializer block, status = null
3. constructor body
Ready: DRAFT at 18.0%
Show solution and explanation
public class Main {
    static class Invoice {
        double gstRate = initRate();
        String status;

        {
            System.out.println("2. initializer block, status = " + status);
        }

        Invoice() {
            System.out.println("3. constructor body");
            status = "DRAFT";
        }

        double initRate() {
            System.out.println("1. field initializer, gstRate was " + gstRate);
            return 18.0;
        }
    }

    public static void main(String[] args) {
        Invoice inv = new Invoice();
        System.out.println("Ready: " + inv.status + " at " + inv.gstRate + "%");
    }
}
1. field initializer, gstRate was 0.0 2. initializer block, status = null 3. constructor body Ready: DRAFT at 18.0%

How it works: memory is zero-filled first, which is why gstRate is seen as 0.0 and status as null. Then, after the parent constructor, field initializers and initializer blocks run in the order they are written, and finally the constructor body.

P3Easy

Parent first, then child

A SavingsAccount extends Account. Show that the parent's constructor finishes before the child's constructor body starts.

Input
new SavingsAccount("SB-101")
Expected output
Account part ready: SB-101
Savings part ready: rate 3.5%
SB-101 created
Show solution and explanation
public class Main {
    static class Account {
        String number;
        Account(String number) {
            this.number = number;
            System.out.println("Account part ready: " + number);
        }
    }

    static class SavingsAccount extends Account {
        double rate;
        SavingsAccount(String number) {
            super(number);
            rate = 3.5;
            System.out.println("Savings part ready: rate " + rate + "%");
        }
    }

    public static void main(String[] args) {
        SavingsAccount s = new SavingsAccount("SB-101");
        System.out.println(s.number + " created");
    }
}
Account part ready: SB-101 Savings part ready: rate 3.5% SB-101 created

How it works: there is one object on the heap with both the number and rate fields. Its constructors run from the top of the hierarchy down: super(number) must be the first statement, so the Account part is set up before the SavingsAccount part.

P4Medium

Resources close in reverse order

Open a database connection and then a file inside one try-with-resources statement. Show the order in which they are closed.

Input
Resource("db"), Resource("file")
Expected output
open db
open file
export orders from db to file
close file
close db
both released
Show solution and explanation
public class Main {
    static class Resource implements AutoCloseable {
        final String name;
        Resource(String name) {
            this.name = name;
            System.out.println("open " + name);
        }
        @Override
        public void close() {
            System.out.println("close " + name);
        }
    }

    public static void main(String[] args) {
        try (Resource db = new Resource("db");
             Resource file = new Resource("file")) {
            System.out.println("export orders from " + db.name + " to " + file.name);
        }
        System.out.println("both released");
    }
}
open db open file export orders from db to file close file close db both released

How it works: resources are closed automatically when the try block ends, in the reverse order of opening, because the file may depend on the connection. This is the deterministic, modern way to release resources; you never wait for the garbage collector.

P5Medium

Close still runs when something fails

An export fails half-way with an exception. Show that the connection is still closed before the error is handled.

Input
export throws IllegalStateException
Expected output
connection opened
exporting row 1
connection closed
export failed: disk full
Show solution and explanation
public class Main {
    static class Connection implements AutoCloseable {
        Connection() { System.out.println("connection opened"); }
        @Override
        public void close() { System.out.println("connection closed"); }
    }

    static void export(Connection c) {
        System.out.println("exporting row 1");
        throw new IllegalStateException("disk full");
    }

    public static void main(String[] args) {
        try (Connection c = new Connection()) {
            export(c);
        } catch (IllegalStateException e) {
            System.out.println("export failed: " + e.getMessage());
        }
    }
}
connection opened exporting row 1 connection closed export failed: disk full

How it works: when export throws, Java first closes the resource declared in the try header, then runs the matching catch block. Without try-with-resources a failed export would leak the connection until someone noticed the pool was empty. (Exceptions are covered fully in Module 9.)

P6Medium

Recently viewed: old items fall out

Keep only the last 3 products a user viewed in a fixed-size array. When a 4th arrives, the oldest is overwritten and becomes unreachable. Print the list after each view.

Input
views: "Phone", "Cover", "Charger", "Earbuds", "Stand"
Expected output
[Phone, null, null]
[Phone, Cover, null]
[Phone, Cover, Charger]
[Earbuds, Cover, Charger]  dropped Phone
[Earbuds, Stand, Charger]  dropped Cover
Show solution and explanation
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] recent = new String[3];
        int next = 0;
        String[] views = {"Phone", "Cover", "Charger", "Earbuds", "Stand"};
        for (String product : views) {
            String dropped = recent[next];
            recent[next] = product;
            next = (next + 1) % recent.length;
            System.out.println(Arrays.toString(recent) + (dropped != null ? "  dropped " + dropped : ""));
        }
    }
}
[Phone, null, null] [Phone, Cover, null] [Phone, Cover, Charger] [Earbuds, Cover, Charger] dropped Phone [Earbuds, Stand, Charger] dropped Cover

How it works: a ring buffer: next cycles 0, 1, 2, 0, 1… Overwriting a slot removes the array's reference to the old product, so (if nothing else refers to it) it becomes garbage. Memory stays bounded at 3 slots however many products the user views, unlike a list that only ever grows.

P7Business problem

Ticket counter with live and total counts

A railway booking desk issues tickets. Track how many tickets were ever issued and how many are still active, where a ticket stops being active when it is cancelled (closed). Use try-with-resources for a ticket that is used once and released.

Input
issue T1, T2; T3 used in try-with-resources; cancel T1
Expected output
Using PNR1003, active = 3
PNR1003 released
PNR1001 released
Issued: 3, active: 1
Still holding PNR1002
Show solution and explanation
public class Main {
    static class Ticket implements AutoCloseable {
        static int issued = 0;
        static int active = 0;
        final String pnr;

        Ticket() {
            issued++;
            active++;
            pnr = "PNR" + (1000 + issued);
        }

        @Override
        public void close() {
            active--;
            System.out.println(pnr + " released");
        }
    }

    public static void main(String[] args) {
        Ticket t1 = new Ticket();
        Ticket t2 = new Ticket();
        try (Ticket t3 = new Ticket()) {
            System.out.println("Using " + t3.pnr + ", active = " + Ticket.active);
        }
        t1.close();
        t1 = null;
        System.out.println("Issued: " + Ticket.issued + ", active: " + Ticket.active);
        System.out.println("Still holding " + t2.pnr);
    }
}
Using PNR1003, active = 3 PNR1003 released PNR1001 released Issued: 3, active: 1 Still holding PNR1002

How it works:

  1. Two static counters belong to the class, not to any ticket.
  2. “Active” is a business state that we manage ourselves with close(). Garbage collection is not a signal you can count on: nothing tells you when (or whether) an unreachable ticket is collected.
  3. Setting t1 = null after closing it makes the object unreachable; it is not what released it.
P8Business problem

Object pool for expensive payment clients

Creating a payment-gateway client is slow (it sets up a secure connection), so a service keeps a small pool of 2 clients and reuses them for 5 payments instead of creating 5 objects. Print which client handles each payment.

Input
payments: 499, 1250, 80, 3000, 640 (Rs)
Expected output
client 1 paid Rs 499
client 2 paid Rs 1250
client 1 paid Rs 80
client 2 paid Rs 3000
client 1 paid Rs 640
Clients created: 2
Client 1 handled 3, client 2 handled 2
Show solution and explanation
public class Main {
    static class GatewayClient {
        static int created = 0;
        final int number;
        int handled = 0;
        GatewayClient() {
            created++;
            number = created;
        }
        void pay(int amount) {
            handled++;
            System.out.println("client " + number + " paid Rs " + amount);
        }
    }

    public static void main(String[] args) {
        GatewayClient[] pool = {new GatewayClient(), new GatewayClient()};
        int[] payments = {499, 1250, 80, 3000, 640};
        for (int i = 0; i < payments.length; i++) {
            GatewayClient c = pool[i % pool.length];   // borrow
            c.pay(payments[i]);
        }
        System.out.println("Clients created: " + GatewayClient.created);
        System.out.println("Client 1 handled " + pool[0].handled + ", client 2 handled " + pool[1].handled);
    }
}
client 1 paid Rs 499 client 2 paid Rs 1250 client 1 paid Rs 80 client 2 paid Rs 3000 client 1 paid Rs 640 Clients created: 2 Client 1 handled 3, client 2 handled 2

How it works:

  1. Only 2 objects are ever created; the pool array keeps them reachable, so they live for the whole program.
  2. Pooling makes sense for objects that are expensive to create or hold scarce resources (database connections, threads). For small ordinary objects it doesn't pay off: the JVM allocates and collects short-lived objects very cheaply.
  3. Real pools also handle borrowing by many threads at once; here we just rotate with i % pool.length.

Real-world scenario questions (asked in company interviews)

Interviewers use lifecycle questions to see whether you can tell memory problems from resource problems. Decide your answer first, then open the model answer.

Campus interview · GC eligibilityFresher

How many objects are eligible?

The interviewer writes: Order a = new Order(); Order b = new Order(); Order c = a; a = null; b = c; c = null; and asks: after the last line, how many Order objects are eligible for garbage collection, and which ones?

The interviewer is checking: can you track references step by step instead of guessing?

Show a strong answer

Call the objects X (first new) and Y (second new).

  • After c = a: X is referenced by a and c; Y by b.
  • a = null: X still has c.
  • b = c: b now points to X, so Y has no references: eligible.
  • c = null: X still has b. So exactly one object, Y, is eligible.

Avoid: counting null assignments. Count objects that have lost every path from a root.

Fintech · connection pool1–3 years

“Timeout waiting for a connection”

A loan service starts failing after a few hours with errors saying no database connection is available from the pool, yet the heap is only 30% used. A code review finds methods that get a connection, run a query, and close it at the end of the method, except when an exception is thrown. What's wrong and how do you fix it?

The interviewer is checking: do you know the GC doesn't manage resources, and do you reach for try-with-resources?

Show a strong answer
  • Each failed query skips close(), so that connection is never returned to the pool. The Connection object may even be garbage collected, but the pool still counts it as borrowed. After enough failures the pool is empty.
  • Fix: try (Connection c = ds.getConnection(); PreparedStatement ps = …). Both are closed, in reverse order, on every path.
  • Turn on the pool's leak detection (most pools can log where a connection was borrowed and not returned).

Avoid: “add a finalizer that closes it” or “increase the pool size”. Both hide the leak.

SaaS · Java upgrade3+ years

Upgrading a class that overrides finalize()

While moving a document-processing service from Java 8 to Java 21, the build shows [removal] finalize() in Object has been deprecated and marked for removal in a class that wraps a native PDF library handle. Under load, the service sometimes runs out of native memory even though the Java heap is fine. What is your plan?

The interviewer is checking: do you understand why finalization is unreliable and know the modern replacement?

Show a strong answer
  • The native handle is only freed when the finalizer runs, which depends on GC timing. A small Java wrapper object barely uses heap, so the GC has no reason to run often, while native memory fills up.
  • Make the wrapper AutoCloseable and free the handle in close(); change callers to try-with-resources so release is immediate.
  • Register a Cleaner action (holding only the handle, never this) as a safety net for callers that forget.
  • Test with java --finalization=disabled to prove nothing still depends on finalizers.

Avoid: calling System.gc() to “trigger” finalizers.

E-commerce · user sessions1–3 years

Logged-out users still in memory

A shopping site keeps a static Map<String, Session> of active sessions. Each Session holds the user's cart and recently viewed products. Memory grows every day and drops only after a restart. Logout sets the controller's session variable to null. Why doesn't that free anything?

The interviewer is checking: do you understand that any reachable path keeps an object alive?

Show a strong answer
  • Setting one local variable to null removes only that reference. The static map is a GC root, and it still maps the session ID to the Session, which reaches the cart and products. All of it stays reachable.
  • On logout, sessions.remove(id). Also expire idle sessions with a timeout, since many users never log out.
  • Better, use the web framework's session management, or a cache with a size limit and expiry, instead of a hand-made static map.

Avoid: believing that nulling a variable deletes the object.