1. Typical mistakes when working with memory
It’s time to look at the flip side of the magic of automatic memory management. Even if you don’t write in C, where you personally track every byte, in Java you can mess things up so badly that the app will gobble memory like a hungry cat devours sausage. Let’s go through the most common mistakes and how to avoid them.
Forgotten listeners
Java often uses the “listener” pattern — an object that subscribes to another object’s events. For example, you created a button and added a click handler:
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// handle click
}
});
The problem: if you forgot to remove this listener (removeActionListener) when the button or window is no longer needed, the listener stays hanging around in memory. Even if you closed the window and nulled out all references to it, the listener object still holds a reference to the window (or vice versa), preventing the garbage collector from freeing the memory.
Analogy: Imagine you moved but forgot to unsubscribe from a pizzeria’s mailing list — they keep sending ads to your old address.
Static collections that never get cleared
Static fields live as long as the class (and sometimes until the end of the app’s life). If you have a static collection:
public class Cache {
public static final List<String> globalList = new ArrayList<>();
}
and you add objects there but never remove them, they will stay in memory forever. Even if there are no other references to those objects, the reference from the static collection won’t let the GC reclaim them.
Real example: A photo cache in a desktop app that never gets cleaned. After a couple of hours — OutOfMemoryError.
Not releasing resources (files, streams, connections)
Although Java frees memory, it doesn’t automatically close file descriptors, network connections, and other external resources. If you forget to close a file or stream, the resource will linger, and at some point the system will say: “That’s it, no more files for you!” (IOException: Too many open files).
Tip: Always use try-with-resources:
try (FileInputStream in = new FileInputStream("data.txt")) {
// read the file
} // in.close() will be called automatically!
Large objects lingering in memory
Sometimes you create a large array or collection, use it, and then “forget to let go.” For example:
List<byte[]> bigList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
bigList.add(new byte[1024 * 1024]); // 1 MB each
}
// ... forgot to clear bigList
If this collection lives in a static field or in an object that isn’t removed for a long time, all that memory will remain occupied.
Inner and anonymous classes: capturing outer references
Anonymous (and inner) classes in Java keep an implicit reference to the outer object:
public class Outer {
void doSomething() {
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello from inner!");
}
};
// r is stored somewhere
}
}
If the object r ends up in a static collection or cache, it will “hold” a reference to the Outer instance even if it’s no longer needed. The result is a memory leak. With lambda expressions the situation is slightly better, but if a lambda uses fields of the outer class, the reference is still kept.
2. Mistakes when working with the garbage collector
Forcing System.gc()
Many beginners think: “Memory is running out — I’ll call System.gc() and everything will be solved!” In reality it’s only a request to the JVM, not a guarantee of immediate collection. Frequent use can dramatically hurt performance, causing long pauses and freezes. In real applications it’s better to trust the JVM — it will decide when to collect. By the way, some JVMs can ignore explicit GC calls (for example with -XX:+DisableExplicitGC).
Ignoring GC logs
GC logs show when collections happen, how long they take, and how much memory is freed. If you don’t look at these logs, you can miss warning signs: long pauses, frequent Full GC, memory leaks.
How to enable GC logs:
java -Xlog:gc* -jar MyApp.jar
or for older JVMs:
java -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar MyApp.jar
Choosing the wrong GC for the job
The choice of collector affects latency and stability. For low latency (exchanges, online games) a parallel stop-the-world Parallel GC is a bad idea: it can “freeze” all threads during collection. Consider G1 GC, ZGC, or Shenandoah.
3. Mistakes with collections
Using HashMap instead of WeakHashMap for caches.
If you’re building a cache where objects should be automatically removed when there are no more “live” references to them, use WeakHashMap:
Map<Key, Value> cache = new WeakHashMap<>();
With a regular HashMap, objects will live until the cache is cleared manually, which leads to a memory leak.
Forgotten remove() for elements.
If you add objects to collections (for example, listener lists) but don’t remove them when they’re no longer needed, these objects will live forever, especially in long-lived collections (for example, static ones).
4. Best practices: how to avoid problems
Always remove listeners.
If an object subscribed to events, make sure to unsubscribe it when it’s no longer needed. It’s convenient to do this in the dispose() method or when closing a window/screen.
button.removeActionListener(myListener);
Use weak references for caches.
If a cache can do without a guarantee to retain an object, use WeakReference or collections built on them (WeakHashMap). Then the GC can free memory when needed.
Monitor memory in production.
Use jvisualvm, jconsole, or APM systems. This helps catch leaks before users complain.
Analyze a heap dump if you suspect a leak
If the application starts consuming more memory than usual, take a heap dump (for example via jmap or jvisualvm) and see which objects take up the most space. Often the culprit is found in a couple of minutes.
Tune JVM parameters.
- -Xmx — maximum heap size
- -Xms — initial heap size
Sensible limits help avoid OutOfMemoryError and speed up diagnosis.
5. Practice: a code example with a memory leak and its fix
Example 1: Leak via a static collection
public class MemoryLeakDemo {
// Static collection — lives forever
private static final List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
leakyList.add(new byte[1024 * 1024]); // 1 MB each time
System.out.println("Added " + (i + 1) + " MB");
}
// OutOfMemoryError!
}
}
Fix: Use a local variable or clear the collection when it’s no longer needed.
public class MemoryLeakFixed {
public static void main(String[] args) {
List<byte[]> tempList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
tempList.add(new byte[1024 * 1024]);
System.out.println("Added " + (i + 1) + " MB");
}
// tempList = null; // You can explicitly null it out
// Now the objects are eligible for GC after the method returns
}
}
Example 2: Leak via a listener
public class Window {
private final List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener l) {
listeners.add(l);
}
// No removeListener method!
}
Fix: Add a method to remove a listener and call it when closing the window.
public void removeListener(EventListener l) {
listeners.remove(l);
}
Example 3: Cache with HashMap instead of WeakHashMap
Map<Object, Object> cache = new HashMap<>();
// ... add objects
Fix: Switch to WeakHashMap:
Map<Object, Object> cache = new WeakHashMap<>();
Tips for configuring the JVM to monitor memory
- Enable GC logs: -Xlog:gc* or -XX:+PrintGCDetails
- Limit the maximum heap size: -Xmx512m
- If you use caches, watch their size and use weak references where appropriate
- Experiment with GC: -XX:+UseG1GC, -XX:+UseZGC, -XX:+UseShenandoahGC
7. Typical mistakes when working with memory
Mistake No. 1: Forgotten listeners and subscriptions. If you added a listener to an object but forgot to remove it, the listener object (and everything it references) will stay in memory. A classic in GUIs and event-driven systems. Use removeListener/removeActionListener.
Mistake No. 2: Static collections without cleanup. Static fields live the longest. If you put objects there and don’t clean the collection, those objects will remain in memory forever. Especially insidious for unbounded caches.
Mistake No. 3: Not releasing external resources. Left a stream, file, or connection open? You waste memory and hit OS limits. Use try-with-resources and close resources.
Mistake No. 4: Forcing System.gc(). This is not a silver bullet, just a request to the JVM. It often leads to pauses and performance degradation.
Mistake No. 5: Using regular collections for caches. If objects in a cache should be removed automatically, use weak/soft references (WeakHashMap, SoftReference). Otherwise you’ll get a leak.
Mistake No. 6: Inner and anonymous classes capturing outer references. Inner classes and lambdas can implicitly hold a reference to the outer object. If they’re saved in a long-lived collection, that’s a leak.
Mistake No. 7: Ignoring GC logs. If you don’t look at GC logs, you won’t know about long pauses or frequent Full GCs — but users will, through lags and freezes. Enable -Xlog:gc* or -XX:+PrintGCDetails.
GO TO FULL VERSION