Memory Management & Garbage Collection
Stack memory stores method calls and local variables — fast, and automatically cleaned up once the method ends.
Heap memory stores objects (created with "new") — they stick around as long as some reference is using them.
The Garbage Collector (GC) automatically finds and removes objects with no remaining references — you never need to manually free memory in Java.
void createObjects() {
Student s = new Student(); // heap par banta hai
} // method khatam -> s ab unreachable -> GC eligible- Stack = method calls, fast, auto-cleaned
- Heap = objects, this is where GC works
- GC = automatic memory cleanup
Stack memory stores method calls and local variables — fast (it's just a pointer moving for push/pop), and it cleans up automatically the moment a method finishes (no GC needed). Every thread has its own separate Stack.
Heap memory stores objects (created with "new") — these stick around for a long time, until nothing references them anymore. The Heap is shared across all threads (which is why access to heap objects needs to be synchronized in multithreading).
| Stack | Heap | |
|---|---|---|
| Stores | Method calls, local variables, references | Objects, instance fields |
| Speed | Very fast | A bit slower |
| Size | Small, fixed (can get StackOverflowError) | Large, configurable (-Xmx flag) |
| Thread-specific? | Yes, each thread has its own | No, shared across all threads |
| Cleanup | Automatic (when a method returns) | Garbage Collector |
The JVM splits the Heap into generations because experience shows — "most objects die young" (most objects become unused pretty quickly, like a temporary object created inside a loop). The Young Generation is for new objects — GC runs here very often, and it's fast (since it's scanning a small area).
Objects that survive a long time (surviving multiple GC cycles) get promoted to the Old Generation (Tenured) — GC runs less often here, and it's a bit slower (a bigger area, but it runs rarely).
Serial GC: single-threaded, for small applications/single-core machines — simple but pause times can be large.
Parallel GC: uses multiple threads for GC, throughput-focused (finishing the total work fast, not too worried about pause times).
G1 (Garbage First): the modern default (Java 9+), splits the Heap into small regions and gives predictable, small pause times — best for large applications, especially where response time matters.
Even with automatic GC, memory leaks can still happen: (1) adding objects to a static collection without ever removing them (like a static List that never gets emptied) — static references stay "reachable" for the JVM's entire lifetime.
(2) unclosed resources (files, database connections) that GC doesn't clean up (GC only manages memory, not external resources) — this is exactly why try-with-resources matters. (3) listeners/callbacks that are registered but never unregistered — in all these cases the objects stay "reachable", so GC can't remove them, even though they're practically "unused".
Older Java had a finalize() method that ran before an object was garbage collected, for cleanup — but it was unpredictable (no idea when it would run, might never run, GC was in control), so it was deprecated in Java 9+ and is being prepared for complete removal in Java 18.
Modern approach: use the AutoCloseable interface + try-with-resources, which gives deterministic (guaranteed, immediate, predictable) cleanup — this is the right way to do things like closing files/connections.