CodeGym /Courses /JAVA 25 SELF /Local variables, memory leaks, weak references

Local variables, memory leaks, weak references

JAVA 25 SELF
Level 64 , Lesson 2
Available

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
Strong
Only when there are no strong references Regular variables, collections
Soft
When memory is low A cache you’d like to keep longer
Weak
On the next GC pass if there are no strong references Caches, WeakHashMap, listeners
Phantom
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.

1
Task
JAVA 25 SELF, level 64, lesson 2
Locked
Using a Weak Reference — Message in Vanishing Ink 👻
Using a Weak Reference — Message in Vanishing Ink 👻
1
Task
JAVA 25 SELF, level 64, lesson 2
Locked
Cache using WeakHashMap – A Smart Library That Reclaims Forgotten Books 📚
Cache using WeakHashMap – A Smart Library That Reclaims Forgotten Books 📚
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION