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.
Sir, I keep replacing orders with new ones. Shouldn't I be deleting the old Order objects somewhere?
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.
So I can't leak memory in Java at all?
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.
What is the object lifecycle?
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.
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
Lifecycle facts at a glance
| Creation order | Allocate + zero-fill → parent constructor → field initializers and { } blocks (in source order) → constructor body |
|---|---|
| Becomes unreachable by | Reassigning the variable, setting it to null, the method returning (its frame is popped), or the object holding it becoming unreachable |
| GC roots | Local variables and parameters of active frames, static fields of loaded classes, live threads, references held by native code |
| When collected | Not 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 with | try-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”.
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.
Set to null
order = null;
Useful for long-lived fields or arrays. Rarely needed for locals.
Scope end
A method returns and its frame is popped. Objects referenced only by its locals become unreachable.
Frame poppedReleasing 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");
}
}finalize() | Cleaner | try-with-resources | |
|---|---|---|---|
| Since | Java 1.0 | Java 9 | Java 7 |
| Status | Deprecated (9), for removal (18) | Recommended safety net | Recommended main tool |
| When it runs | Some time after unreachable, maybe never | On clean(), or some time after unreachable | Exactly at the end of the try block |
| Can bring the object back? | Yes (resurrection), a classic bug | No: the action can't see the object | Not applicable |
| Cost for the GC | Objects need extra GC cycles | Small | None |
| Use it for | Nothing in new code | Backup cleanup of native resources | Files, 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.
From new to garbage, one stage at a time
Make sure the class is ready
IfOrder o = new Order(101, 499.0);Orderhasn't been loaded and initialised yet, the JVM does that first (Lesson 8.4). Now it knows exactly how many bytes an Order needs.Allocate and zero-fill
The bytecode instruction
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.newreserves the memory on the heap.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 withthisin slot 0. Parent constructors run first, so a parent's fields are ready before the child's code uses them.Use it while it is reachable
The reference indouble total = o.amount * 1.18;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.Lose the last reference
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).o = new Order(102, 99.0); // or o = null; or the method returns
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.
new to collected 6 steps- Allocate:
newfirst reserves memory for the order with every field at its default value. - Construct: the constructor runs and sets id, amount and status.
- Use: the program reads the order through
o. (The long decimal is normaldoublerounding.) - Unreachable:
ois pointed at a new order. Order 101 still exists, but nothing can reach it. - GC runs: later, when the JVM needs space, the garbage collector starts a cycle. Your code doesn't see it.
- Collected: order 101's memory is reused for new objects. No method is called on it.
newallocates object #1 on the heap and zero-fills it.ois not assigned yet.- A constructor frame is pushed; through
thisit writes the real values into #1. Then the frame is popped and the reference is stored ino. - main's frame is a GC root and
opoints at #1, so #1 is reachable. onow points at #2. #1 is unchanged in memory, but no chain of references from any root leads to it.- When the collector runs, it marks everything reachable from the roots: #2 is marked, #1 is not.
- Unmarked objects are reclaimed; their memory is reused. Order 101 is gone without any method being called.
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.
- Start: a
staticlist of listeners. Static fields are GC roots: anything reachable from them stays alive. - Screen opened: it registers listener #2, which refers to the screen and its images.
- Another screen: listener #3 is added. Fine so far.
- Screen closed: the user leaves the first screen, but the code never removes listener #2 from the list.
- GC can't help: the collector frees only unreachable objects. #2 is still reachable from the static list, so it (and its screen) stays forever. Repeat this a few thousand times and the app runs out of memory.
- Fix: remove the listener when the screen closes. Now nothing refers to those objects, and the next GC frees them. This exact bug is common in Android apps and long-running servers.
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.
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.
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.
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.
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.
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");
}
}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 assignedidyet.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.
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.
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.
order 1: Rs 100, order 2: Rs 250, bill for order 3: Rs 500 + 18% GST
Bill: Rs 590.0
Approach
- Reassignment: pointing
currentat a new Order abandons the old one. - Scope end: the Order made inside
billWithGstis referenced only by that method's localtemp; when the frame pops, it is unreachable. - null: setting
current = nulldrops 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.
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.
order 501, rider "Kabir", onDuty = 1 slot
Kabir has order 501 Shift over
Approach
- Create the order and rider, link them both ways, and put the rider in the
onDutyarray. - Clear the local variables: the pair is still reachable through
onDuty, a path from a GC root. - 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.
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.
txn 71: Rs 450, txn 72: Rs 1200
[TXN-71 Rs 450, TXN-72 Rs 1200]
Approach
- The StringBuilder is a scratch object: only the local
sbrefers to it. - The receipt String is added to the static list
receipts. Static fields are GC roots, so whatever they reference stays alive. - 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.
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); useCleaneronly 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.
Count the objects created
Count how many Order objects have been created, using a static counter that the constructor increases.
create 3 orders, one of them inside a loop body
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);
}
}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.
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.
new Invoice()
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 + "%");
}
}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.
Parent first, then child
A SavingsAccount extends Account. Show that the parent's constructor finishes before the child's constructor body starts.
new SavingsAccount("SB-101")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");
}
}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.
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.
Resource("db"), Resource("file")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");
}
}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.
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.
export throws IllegalStateException
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());
}
}
}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.)
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.
views: "Phone", "Cover", "Charger", "Earbuds", "Stand"
[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 : ""));
}
}
}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.
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.
issue T1, T2; T3 used in try-with-resources; cancel T1
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);
}
}How it works:
- Two
staticcounters belong to the class, not to any ticket. - “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. - Setting
t1 = nullafter closing it makes the object unreachable; it is not what released it.
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.
payments: 499, 1250, 80, 3000, 640 (Rs)
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);
}
}How it works:
- Only 2 objects are ever created; the pool array keeps them reachable, so they live for the whole program.
- 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.
- 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.
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.
“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.
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
AutoCloseableand free the handle inclose(); change callers to try-with-resources so release is immediate. - Register a
Cleaneraction (holding only the handle, neverthis) as a safety net for callers that forget. - Test with
java --finalization=disabledto prove nothing still depends on finalizers.
Avoid: calling System.gc() to “trigger” finalizers.
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.