How to create a memory leak
This is going to be a rather evil post – something you will be googling when you really wish to make someone’s life a misery. In the world of Java development memory leaks are just the type of bugs you would introduce in this case. Days or even weeks of sleepless nights in the office are guaranteed for your victim.
We will describe two leaks in this post. Both of them are easy to understand and reproduce. The leaks originate from the real world case studies, but for the sake of clarity we have extracted the demo cases to be shorter and simpler for you to grasp. But rest assured – after we have both seen and fixed hundreds of leaks – cases similar to those demoed this are more common than you might have expected.
First contestant to step into the ring – the infamous HashSet/HashMap solutions where the key used either does not have or has an incorrect equals()/hashCode() solutions.
class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map<key, string=""> m = new HashMap<key, string="">(); while (true) for (int i = 0; i < 10000; i++) if (!m.containsKey(i)) m.put(new Key(i), "Number:" + i); } }
When you execute the code above you would expect it to run forever without any problems – after all, the naive caching solution built should only expand to 10,000 elements and then the growth would stop, as all the keys are already present in the HashMap. However, this is not the case – the elements keep being added as the Key class does not contain a proper equals() implementation next to its hashCode(). The solution would be easy – add the implementation for equals() method similar to the following sample and you’re good to go. But before you manage to find the cause, you have definitely spent lost some precious brain cells.
@Override public boolean equals(Object o) { boolean response = false; if (o instanceof Key) { response = (((Key)o).id).equals(this.id); } return response; }
Second problem to keep your friend awake – String handling in some operations. Works out like a charm, especially when combined with JVM version differences. The way String internals work were changed in JDK 7u6, so if you manage to find environments where production and staging differ only by minor versions, then you are all set. Throw in the code similar to the following for your friend to debug and wonder why the problem does not surface anywhere else but in production.
class Stringer { static final int MB = 1024*512; static String createLongString(int length){ StringBuilder sb = new StringBuilder(length); for(int i=0; i < length; i++) sb.append('a'); sb.append(System.nanoTime()); return sb.toString(); } public static void main(String[] args){ List<string> substrings = new ArrayList<string>(); for(int i=0; i< 100; i++){ String longStr = createLongString(MB); String subStr = longStr.substring(1,10); substrings.add(subStr); } } }
What is happening in the code above – when it is being ran on a pre JDK 7u6, the returned substring keeps a reference to the ~1MB large String underneath. So when the sample is ran with -Xmx100m you would experience an unexpected OutOfMemoryException. Combine this with platform differences and have a different JDK version in the environment you are experimenting and the first grey hairs are about to flourish. Now if you wish to cover up your tracks, we have some more advanced concepts to add into the portfolio, such as
- Load the broken code in a different classloader and keep a reference to the class loaded after the original classloader has been discarded mimicking a classloader leak.
- Hide the offending code into the finalize() methods making the symptoms truly unpredictable
- Toss in a tricky combination of long-running Threads storing something in ThreadLocals being accessed by ThreadPool – governed application Threads
I hope we gave you some food for thought and some tricks to pull next time when you are mad at someone. Endless hours of hardcore debugging guaranteed. Unless your friend is using Plumbr of course which finds the leaks for him. But blatant marketing asides, I hope we were able to demonstrate in two simple cases how easy it is to create a memory leak in Java. And most of you have experienced how hard it would be to trace down a bug like this. So if you enjoyed the post, subscribe to our Twitter feed be alerted about our future content about JVM performance tuning.
Love this post. It’s great help to test out my memory. Also great to give to someone who I dont like lol. JK
Anyways, if having resources such as scanners and such, which you never close, but are still running in the ‘background,’ do they cause the memory to leak? Or does the JVM simply close these resources itself?
If your Scanners and other instances are left running in the background then yes they will leak memory. But if your instances are not used anymore, they will get garbage collected and the memory will be freed.