Introduction: "Breaking is Easier Than Building"
\nEvery Java developer has cursed a memory leak in their production application at least once. The application crashes with OutOfMemoryError, the server reboots, and users write angry emails. But there is another side to the coin: how to create a memory leak in Java intentionally? It sounds crazy, but this very question (original from Stack Overflow) has gathered thousands of views.
Why should you know this? To understand the mechanics of the garbage collector (GC) and the antipatterns that turn your code into a sieve. In 2025, when Java 21 with virtual threads and Loom is already the standard, old leaks haven't gone away — they just disguise themselves as new features.
\nIn this article, we will not only show how to "break" a Java application, but also provide best practices for detecting leaks in 2025. Ready? Let's go!
\n\nA Bit of Theory: Why Java Isn't "Immortal"
\nMany beginners firmly believe that the garbage collector (GC) automatically removes everything that is not needed. Reality is harsh: a memory leak in Java is possible if an object remains reachable from root references (GC roots) but is no longer needed by the application.
\nIn 2025, with the advent of Value Objects (Project Valhalla) and improved ZGC (a collector with pause time < 1 ms), the performance situation has improved, but the logic of leaks remains the same. The GC is not a telepath — it doesn't know that you forgot to nullify a reference in a HashMap.
Method #1: The Classic — "Forgetful" HashMap
\nThe most popular antipattern, which migrates from article to article. But we can't do without it.
\nHow the Leak Works
\nYou put objects into a HashMap (or HashSet) as keys, but forget to remove them. If the keys are mutable objects that change their hash code after insertion, you are guaranteed to get a Java memory leak.
import java.util.HashMap;\nimport java.util.Map;\n\npublic class LeakyMapExample {\n public static void main(String[] args) {\n Map<StringBuilder, String> map = new HashMap<>();\n\n for (int i = 0; i < 100_000; i++) {\n StringBuilder key = new StringBuilder("key_" + i);\n map.put(key, "value_" + i);\n // "Forgot" to remove — keys hang forever\n }\n System.out.println("Map size: " + map.size());\n // Leak: 100_000 StringBuilder + String objects in the heap\n }\n}\n\n\nWhy is this a leak? As long as the reference to map is alive, all keys and values remain in the heap. Even if the application logic no longer uses this data, the GC will not touch it.
Best Practice 2025
\n- \n
- Use
WeakHashMapif the keys are temporary objects. \n - For caching — use
CaffeineorGuava Cachewith size/time limits. \n - Never use
StringBuilderas a key — only immutable types. \n
Method #2: Static Collections — The Silent Killer
\nStatic fields live until the JVM shuts down. If you put data into a static List or static Map and don't clean it up, you get a Java memory leak at the class level.
import java.util.ArrayList;\nimport java.util.List;\n\npublic class StaticLeak {\n private static final List<byte[]> CACHE = new ArrayList<>();\n\n public static void addData() {\n // Each call adds 10 MB to the static list\n CACHE.add(new byte[10 * 1024 * 1024]);\n }\n\n public static void main(String[] args) throws Exception {\n for (int i = 0; i < 1_000; i++) {\n addData();\n Thread.sleep(100);\n }\n // OutOfMemoryError is inevitable\n }\n}\n\n\n<