1. Where local variables are stored
Let’s start with the basics: local variables. These are variables declared inside a method that exist only while that method is executing. Their lives are short and dramatic: as soon as the method finishes, all its local variables disappear without a trace.
In Java, local variables are stored on the stack. Each thread has its own stack. If a method calls another method, a new “frame” (stack frame) with local variables and the return address is pushed onto the stack. When the method completes, its frame is popped from the stack.
Example: the life of a local variable
public class LocalVariableDemo {
public static void main(String[] args) {
int a = 42; // local variable a lives only in main
printSquare(a);
// There's no variable b here anymore!
}
public static void printSquare(int b) {
int square = b * b; // local variable square
System.out.println("Square: " + square);
// After exiting printSquare all local variables disappear
}
}
Important: if a local variable is a reference to an object (for example, a String, a Scanner, or an array), then the reference itself lives on the stack, but the object lives on the heap! When the reference disappears and there are no other references to the object, the garbage collector can remove the object.
Illustration
Stack (for main):
| int a = 42 |
| args |
-------------------
Heap:
| [objects created via new] |
2. Memory leaks in Java: myth or reality?
Many beginners think: “Java has a garbage collector! That means there can’t be memory leaks!” Alas, that’s a myth that evaporates on the very first large project.
The garbage collector removes only those objects that have no live references at all. If there’s a reference left somewhere (even in the most unexpected place), the object will hang around in memory indefinitely. Or until an OutOfMemoryError.
Example 1: Static collection trap
import java.util.ArrayList;
import java.util.List;
public class MemoryLeakDemo {
// Oh, that static collection!
private static final List<String> BIG_LIST = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
BIG_LIST.add("String number " + i);
}
System.out.println("Added one million strings");
// Even if main finishes, BIG_LIST will remain in memory as long as the JVM lives
}
}
What’s happening?
- The static variable BIG_LIST lives as long as the class is loaded (usually until the JVM shuts down).
- All strings added to the list cannot be collected — there’s always a reference to them via BIG_LIST.
- If you forget to clear such collections — you’ll get a memory leak.
Example 2: Unremoved listeners
import java.util.ArrayList;
import java.util.List;
class EventSource {
private final List<Runnable> listeners = new ArrayList<>();
public void addListener(Runnable listener) {
listeners.add(listener);
}
// ... other methods ...
}
public class ListenerLeakDemo {
public static void main(String[] args) {
EventSource source = new EventSource();
Runnable listener = () -> System.out.println("Event!");
source.addListener(listener);
// If you forget to call source.removeListener(listener), the listener will stay in memory forever!
}
}
Problem: if the listener is no longer needed but isn’t removed from the list, it — and all objects it references — will remain in memory.
Example 3: A cache that never evicts
import java.util.HashMap;
import java.util.Map;
public class CacheLeakDemo {
private static final Map<String, byte[]> CACHE = new HashMap<>();
public static void main(String[] args) {
for (int i = 0; i < 1_000_000; i++) {
// Each time we create a 1 KB array
CACHE.put("key" + i, new byte[1024]);
}
System.out.println("Added one million entries to the cache");
// The cache grows, memory runs out, OutOfMemoryError!
}
}
Conclusion: even with a GC you can easily get a memory leak if you don’t manage object lifecycles carefully!
3. Weak references and their friends
Sometimes we need caches or collections where objects can be removed by the GC if nobody else references them. For this, weak references (WeakReference) were invented.
Regular (strong) references
String s = new String("hello"); // strong reference
The object referenced by s will stay in memory as long as there is at least one strong reference to it.
Weak reference (WeakReference)
import java.lang.ref.WeakReference;
public class WeakRefDemo {
public static void main(String[] args) {
String strong = new String("Hello, world!");
WeakReference<String> weak = new WeakReference<>(strong);
System.out.println("Before cleanup: " + weak.get()); // there is a reference
strong = null; // remove the strong reference
System.gc(); // ask the GC to clean memory (not guaranteed!)
// After some time weak.get() may become null
System.out.println("After GC: " + weak.get());
}
}
How does it work?
- As long as there is at least one strong reference to an object, the GC won’t remove it.
- If only weak references remain, the object can be collected during the next GC cycle.
- The weak.get() method returns the object if it’s still alive, or null if it has been collected.
Where are weak references used?
The main use is caches where it’s not critical if an object gets removed from memory. For example, if you cache images but don’t want the cache to occupy all memory.
Example: WeakHashMap
WeakHashMap is a collection that stores keys via weak references. If nobody else references the key, the corresponding entry is removed from the map.
import java.util.Map;
import java.util.WeakHashMap;
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Object, String> map = new WeakHashMap<>();
Object key = new Object();
map.put(key, "Value");
System.out.println("Before cleanup: " + map);
key = null; // remove the strong reference to the key
System.gc(); // ask the GC to run
// After some time the map will become empty!
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("After GC: " + map);
}
}
Attention: WeakHashMap works only for keys — values are stored with regular strong references.
Soft, Weak, Phantom: the whole reference family
Java has four types of references (sorted by “strength”):
| Reference type | When does the GC remove the object? | Typical use |
|---|---|---|
|
Only when there are no strong references | Regular variables, collections |
|
When memory is low | A cache you’d like to keep longer |
|
On the next GC pass if there are no strong references | Caches, WeakHashMap, listeners |
|
After finalization, to track object reclamation | Special tasks, off-heap cleanup |
- SoftReference — the object is removed when memory is low (well-suited for image caches, etc.).
- WeakReference — collected at the first GC if there are no other references.
- PhantomReference — the most “ghostly” type, needed for complex scenarios, rarely used by beginners.
4. Practice: a memory leak and how to fix it
Leak example: a static list
import java.util.ArrayList;
import java.util.List;
public class LeakExample {
private static final List<byte[]> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new byte[1024 * 1024]); // 1 MB
if (i % 10 == 0) System.out.println("Added " + i + " MB");
}
}
}
What will happen?
The program will quickly consume all available memory and crash with an OutOfMemoryError because the static list holds references to all created arrays.
Fix: use weak references
If it’s not critical that all objects are always available, you can store them via weak references:
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class LeakFixed {
private static final List<WeakReference<byte[]>> list = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 100_000; i++) {
list.add(new WeakReference<>(new byte[1024 * 1024]));
if (i % 10 == 0) System.out.println("Added " + i + " MB");
System.gc(); // Hint to the GC (does not guarantee immediate cleanup!)
}
}
}
Now the arrays can be collected by the GC if there are no other references to them — the list stores only weak references.
5. Typical memory leak scenarios
- Event listeners: forgot to remove a listener — the object lives forever.
- Static collections: non-evicting caches, global lists — all of these can lead to leaks.
- Inner classes and lambdas: if an inner class or lambda captures a reference to the outer object, it won’t be collected as long as the outer object lives.
Example with an inner class
public class Outer {
private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 MB
public Runnable createTask() {
// Anonymous inner class captures a reference to Outer!
return new Runnable() {
@Override
public void run() {
System.out.println("Task is running");
}
};
}
public static void main(String[] args) {
Outer outer = new Outer();
Runnable task = outer.createTask();
// Even if outer = null, task still holds a reference to bigArray!
}
}
Solution: use static inner classes or move the logic to separate classes to avoid holding unnecessary references.
6. Practice: using weak references in a cache
Let’s add a simple cache using WeakHashMap to the training app.
import java.util.Map;
import java.util.WeakHashMap;
public class ImageCache {
private final Map<String, byte[]> cache = new WeakHashMap<>();
public void put(String name, byte[] data) {
cache.put(name, data);
}
public byte[] get(String name) {
return cache.get(name);
}
public static void main(String[] args) {
ImageCache cache = new ImageCache();
cache.put("cat", new byte[1024 * 1024]); // 1 MB
System.out.println("Cat added to cache");
// If there are no more references to the key "cat", the entry may be removed by the GC
}
}
In real applications (for example, image libraries) weak references help avoid running out of memory by automatically removing rarely used data.
7. Strong vs weak: when to use which?
- Strong references — the default: use them for everything that must reliably live.
- Weak references — for caches, listeners, when it’s not critical if an object gets collected.
- Soft references — for caches you’d like to keep longer but can be collected when memory is low.
- Phantom references — for advanced scenarios (for example, finalization outside the heap).
8. Common mistakes when working with memory and references
Error #1: “The GC will clean up everything for me!” The garbage collector removes only those objects that have no live strong references. If you “forgot” a reference somewhere (for example, in a static collection), the object will live forever.
Error #2: Forgotten listeners. You added a listener to an object but didn’t remove it when the object was destroyed? The listener — and everything it captured — will remain in memory.
Error #3: A cache without weak references. Using a regular HashMap for a cache that should auto-clean? Better use WeakHashMap or SoftReference.
Error #4: Inner classes and lambdas capture the outer object. Inner classes (and lambda expressions) implicitly store a reference to the outer object. If you keep instances of such classes longer than the outer object itself, you’ll get a leak.
Error #5: Expecting the GC to run immediately. Calling System.gc() does not guarantee that garbage collection will happen right away. It’s just a “request” to the JVM, not a command.
GO TO FULL VERSION