5 Code Smells That Are a Dead Ringer for a Memory Leak
Memory leaks are sneaky. They often don’t surface in testing, and may not even be obvious in performance labs. Suddenly, in production, the…
5 Code Smells That Are a Dead Ringer for a Memory Leak
Memory leaks are sneaky. They often don’t surface in testing, and may not even be obvious in performance labs. Suddenly, in production, the application has performance problems, either at peak times, or after it has been running for some time.
Monitoring and careful analysis of garbage collection trends in production can help us detect problems before they crash the system, saving time and money. It would be even better if we were able to avoid coding practices that can result in memory leaks altogether.
This article aims to help with identifying memory leak patterns, both when coding and when troubleshooting. We’ll look at the five most common ‘code smells’ that result in memory leaks.
What is a Memory Leak?
In the JVM, the garbage collector works in the background, clearing unused memory. To do this, it works from garbage roots: memory objects that are known to be currently in use. These include variables belonging to active methods, and static variables. It checks what objects in the heap these roots reference, and recursively works through the reference chain, marking objects that are still being referenced. Anything else is treated as garbage, and cleared from memory.
A memory leak occurs when objects that are no longer needed by the program are still referenced from somewhere, and can’t be garbage collected. These objects can gradually build up over time, until the application runs short of memory. For more information, see this article: Common Memory Leaks in Java and How to Fix Them.
Symptoms that indicate we may have a memory leak include:
- Intermittent sluggish response. This happens because the garbage collector (GC) is running more and more often, pausing other application threads each time;
- Heavy CPU usage. The GC is very CPU-intense;
- Deteriorating performance over time;
- OutOfMemory crashes.
Both in performance labs and in production, it’s a good idea to enable GC logging, and regularly run these logs through a monitoring tool such as GCeasy. This lets us look at micrometrics that can predict memory issues before they cause problems in the live system. We can also look at GC trends over time, seeing whether memory is being freed as it should. The image below is taken from a GCeasy report showing memory usage after each GC cycle over time. GC events are indicated by a red triangle.

Fig: Comparing Healthy GC Pattern with Memory Leak Pattern
The upper chart shows the memory usage pattern of a healthy application. Memory builds up, but is cleared back down to a similar level when GC runs. The lower chart shows the pattern of an application with a memory leak. The GC is still clearing memory, but never back down to the same level. This indicates that memory is building up over time, which is typical of a memory leak.
This may not necessarily be caused by your own code: if you use third party libraries, they may also be the culprit.
We need to also understand that even a tiny object that’s not released when it’s no longer needed can cause memory problems. This is because of the concept of shallow heap and retained heap.
- Shallow Heap is the amount of memory that’s occupied by the object itself.
- Retained heap is the amount of memory occupied by the object, all objects it refers to, and so on down the parent/child relationship chain.
Identifying Memory Leak Patterns
Let’s look at the five most common coding patterns that are likely to cause memory leaks. There may be others, but avoiding these five traps goes a long way towards writing and maintaining healthy applications.
1. Variables Declared in the Wrong Scope
Understanding the concept of scope is very important if we are to use memory efficiently. Variables are visible anywhere within the scope where they’re declared. They will only be eligible for garbage collection when that scope is no longer valid, unless they are explicitly set to null. Let’s look at the three most frequently-used scopes in Java:
- Static variables, also known as class variables. These are defined in the outer block of the class, in other words they are not declared within any method, and are declared with the keyword static. These variables are visible anywhere within the class, and they will not be garbage collected until the class is unloaded. The JVM holds one copy of the variable per class, which is shared between all objects created from this class.
- Instance variables, also known as member variables. These are declared in the outer block of the class, but don’t have the keyword static. They are visible anywhere within the class, and each object created from the class keeps its own copy of these variables. They remain in memory until the object created from the class becomes eligible for garbage collection.
- Local variables. Typically, these are defined within a method, and may in fact be defined in an inner block of the method, such as a for or while loop. These variables are visible only within the block where they were declared, and become eligible for garbage collection when the block goes out of scope: for example, when the method or loop completes.
If other objects outside the scope hold pointers to them, variables will be retained after their scope becomes invalid. Let’s take an example. Object A is declared as a local variable, and is registered as a listener for Object B, which is an instance variable. Object A cannot be garbage collected when it goes out of scope, since Object B holds a reference to it. It will be retained until either Object B is eligible for garbage collection, or it’s deregistered as a listener.
When we declare a variable, we always need to ask ourselves how long it should be retained, and declare it in the correct scope. Instance variables should only be used for items that need to be retained as long as the objects exist.
Giving an object too wide a scope causes it to be retained even when it’s no longer needed. Data required only for the duration of a single transaction should be declared within the block that deals with the transaction.
On the other hand, giving an object too narrow a scope may result in duplicate data being stored. For example, if product details are cached so that frequently-used products need not be repeatedly read from storage, the cache needs to be in an outer scope shared between all transactions.
The program below illustrates a memory leak caused by an instance variable being made to refer to a local variable. The local variable cannot be garbage collected, even though the method completes, and even though the variable is explicitly set to null.
import java.nio.ByteBuffer;
import java.util.ArrayList;
// This program demonstrates a memory leak due to an instance variable holding a reference
// to a local variable
// ***************************************************************************************
public class BuggyProg14 {
// Class variable holds reference to local variable
ArrayList arr = new ArrayList();
public static void main(String[] args) {
// Create an object from the class
BuggyProg14 obj = new BuggyProg14();
}
// Permanent loop, which will eventually end in an OutOfMemoryError
public BuggyProg14() {
while(true)
method1();
}
// Method 1 creates a large local variable
public void method1() {
ByteBuffer buffer1= ByteBuffer.allocate(32768);
// The instance variable holds a reference to it
arr.add(buffer1);
// Even though it is set to null, it can't be garbage collected
buffer1=null;
// Delay to give time to take a heap dump
try{Thread.sleep(20);} catch(Exception e){}
}
}
To visualize what’s happening in memory, we can run it with GC logging enabled, and take a heap dump while it’s in progress. We can then submit the GC logs to a GC log analyzer such as GCeasy, and analyze the heap dump with a tool such as HeapHero.
The GC log analysis shows that memory is continually increasing, in spite of the garbage collector running regularly. Since it’s a simple program, which has no other variables that can be garbage collected, memory is increasing in almost a straight line, indicating a memory leak.

Fig: GCeasy Shows Memory is Not Being Released
Analyzing the heap dump shows that we had a very large number of ByteBuffer objects at the time the dump was taken.

Fig: HeapHero Histogram Shows a Large Number of ByteBuffer Objects
Scope is very important when coding a program. In particular, we need to be careful of class and instance variables that hold references to local variables.
2. Improper Use of Caches and Other Collections
Retaining data in a cache or collection can considerably speed up applications, since there is no need to repetitively read the same data from slow storage devices. These need to be managed with care, however, since they are a well-known source of memory leaks when not used correctly.
Things to beware of when storing large amounts of data in memory include:
- Caches must always have a working eviction policy to keep them from growing indefinitely. Rather than ‘reinventing the wheel’, it’s a good idea to use a reputable open-source library such as Guava caching when developing high-performance systems.
- Make sure duplicates are not stored.
- When using keyed collections such as a HashMap, make sure the keys are immutable, otherwise there’s no guarantee the collection will not contain duplicates. For more information, see Not So Common Memory Leaks.
- Classes defining objects that are stored in a cache must correctly implement the hashCode() and equals() methods to enable duplicates to be identified.
Incidentally, if a large number of objects are removed from a collection, the underlying array will not shrink back to its original size unless we call its trim() method. This can result in a large amount of wasted memory.
3. Failing to Close Resources
Objects created from classes such as streams, database connections, buffers and network connections may retain resources that need to be released once their task is finished. They often have a large retained heap size. These classes generally include a close() method that releases resources. It’s important to use this method when we no longer need the object. If we don’t, it can result in a memory leak.
Failing to close objects can simply be laziness on the part of a developer, or it may be that the close() method is enclosed in a try … catch block, and in the event of an exception, the close() statement is never reached.
This is the case in the sample program below.
import java.io.FileInputStream;
import java.io.IOException;
// This program has a memory leak due to failing to close
// file input streams when an exception is thrown
// *******************************************************
public class BuggyProg15 {
private static String fileName="";
public static void main(String[] args) throws Exception {
fileName=args[0];
for (int i = 0; i < 50000; i++) {
try{Thread.sleep(20);} catch(Exception e){}
readFile();
}
System.out.println("Finished reading files");
}
// This leaks memory since the stream is never closed
private static void readFile() throws IOException {
// Create a new input stream
FileInputStream stream1 = new FileInputStream(fileName);
try {
// Read a few bytes (simulate use)
byte[] buffer = new byte[1024];
stream1.read(buffer);
// The next statement throws an exception, which then skips the close
Integer.parseInt("A");
stream1.close();
}
catch(Exception e)
{}
}
}
It opens and reads a block from a file many times, but since an exception is thrown within the method, the program never gets as far as the close() statement.
If we run the program and obtain GC logs and a heap dump, we can see the effect of this.
Submitting the logs to GCeasy shows the following memory usage pattern:

Fig: Heap Usage Continually Increases
This issue can easily be avoided by always including a finally block with the try…catch construct, which is used to close resources and do any other necessary cleanup operations. We can recode the readFile() method as shown below.
private static void readFile() throws IOException {
// Create a new input stream
FileInputStream stream1 = new FileInputStream(fileName);
try {
// Read a few bytes (simulate use)
byte[] buffer = new byte[1024];
stream1.read(buffer);
// The next statement throws an exception
Integer.parseInt("A");
}
catch(Exception e) {
}
finally {
stream1.close();
}
}
}
In this version, the stream will always be closed. In newer versions of Java, we can achieve the same thing by using the Try With Resources construct.
4. Rogue Loops
Loops are an essential part of programming, but we need to take care that they always have termination conditions that will always become true, no matter what happens. If, in unusual circumstances, the termination condition never becomes true, the loop never ends, and may continue to create more objects, or add entries to collections, until the JVM runs out of memory. Consider this code snippet, which loops through a file using a buffered reader. The readLine() method of a buffered reader returns null if there is no more input.
boolean moreLines=true;
while(moreLines) {
try {
String inputLine=bufferedReader.readLine();
if(inputLine==null)
moreLines=false;
else
processLine(inputLine);
}
catch(Exception e) {
logError(e);
}
}
This code may work correctly for years — until the readLine() method throws an exception. When it does, the condition moreLines will always remain true, and the program will loop indefinitely. If the method was expanded to create objects or add to a collection, these variables would build up in memory resulting in a memory leak.
As another example, let’s take the following code snippet:
float firstNumber = getFirstValue();
float secondNumber = 0;
while(firstNumber!=secondNumber)
firstNumber=doCalculations(firstNumber);
This obtains a number and repeatedly does some calculations on it, which are designed to eventually bring this number back to zero. In theory, this shouldn’t give us an endless loop, but it’s dangerous when using float values, which are subject to tiny rounding differences. The result of the calculations could end up being something like 0.0000000001 because of roundings, and it may never equal exactly zero.
We should always think through the terminating conditions of loops carefully, and be sure that they will always be met at the right time under all possible circumstances.
These are just two ways a loop may not end predictably. The number of possible coding glitches is infinite.
5. Failing to Clean Up Threads
Multithreading is prone to bugs in many ways if not carefully managed. Memory management is no exception. We always need to be aware of how or when a thread will terminate, and what memory it may be holding.
Threads may not terminate for many reasons, including:
- Intentional program design: a listener thread, for example, often loops indefinitely, waiting for and then dealing with incoming connections.
- They may hang waiting for locks or external resources.
- They may be part of a thread pool, and will be returned to the pool rather than terminating when their task is done.
- A program bug can prevent them from terminating.
ThreadLocal variables are a useful feature of Java, creating variables whose scope is the entire thread. This is great, but we need to remember that unless explicitly removed using the remove() method, those variables will be retained if the thread doesn’t terminate for any reason. If these variables hold pointers referencing a large retained heap, they can cause memory leaks. Great care also needs to be taken if they’re used in a thread that may form part of a thread pool, since they can contain values that relate to the thread’s previous task.
Good practice is to:
- Ensure during testing and performance labs that all threads are terminated when they should. The fastThread utility is an excellent way of monitoring thread usage.
- Including a cleanup method in any thread that may be used in a thread pool, and using it to clear or dereference any variables that may otherwise be left behind. Make sure this method is always called by the controlling logic when a thread has completed its task.
The program below simulates a situation where a thread hangs, waiting to receive a message via the network.
The class Thread1 creates a large ThreadLocal variable, then connects to a network socket and waits until it gets some response from it.
The main program spawns 1000 of these threads, then sets up a server socket to receive connections from the thread.
import java.io.InputStreamReader;
import java.io.BufferedReader;
import java.net.ServerSocket;
import java.net.Socket;
public class BuggyProg16 {
// Leak due to uncleared ThreadLocal
public static void main(String[] args) {
// Create an object from the class
BuggyProg16 obj = new BuggyProg16();
}
public BuggyProg16() {
try {
int qlen = 1000;
int port = 4444;
Socket sock;
ServerSocket servsock = new ServerSocket(port, qlen);
for(int i=0;i<1000;i++)
new Thread1("Thread"+i).start();
while (true) {
//
// Listens for and accepts a client connection
// **************************************************
sock=servsock.accept();
}
}
catch (Exception e) {System.out.println("Main thread:"+e.toString());}
}
}
class Thread1 extends Thread {
ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public Thread1(String s) {
super(s);
}
@Override
public void run() {
// Set thread local to a large array
// *********************************
try{
byte[] data = new byte[10000];
threadLocal.set(data);
// Pause to give server time to get started
try{Thread.sleep(1000);} catch(Exception e){}
Socket clientSocket = new Socket("127.0.0.1", 4444);
BufferedReader reader = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
// This call will hang because the server never sends anything
String line=null;
while(line==null) {
try{Thread.sleep(500);} catch(Exception e){};
line = reader.readLine();
}
}
catch (Exception e) {System.out.println(getName()+":"+e.toString());}
}
}
If we run the program with GC logging and take a heap dump, we see that once again we have a memory leak. GCeasy produces the following chart from the logs.

Fig: Memory Usage Shown in GCeasy Chart
This shows the memory rising until all threads have been created, then remaining at the same level. The garbage collector is unable to free any memory since the references are still valid.
Analyzing a heap dump with HeapHero produces the histogram shown below.

Fig: HeapHero Histogram Showing Large Retained Heap for Thread
As highlighted, Thread1 has many instances each with a large retained heap.
Conclusion
Memory leaks can cause havoc in production, degrading performance and often resulting in system crashes.
If we can identify memory leak patterns, we can code more robust software, and troubleshoot memory leaks faster. It’s worth constantly monitoring critical systems to identify possible memory leaks before they cause a problem.
Careful coding can save excessive production costs later, so it’s always worth taking the time to double-check for potential issues at an early stage.
메타데이터
- post_id
- fc76c3e3eae8
- slug
- 5-code-smells-that-are-a-dead-ringer-for-a-memory-leak-fc76c3e3eae8
- url
- https://medium.com/@jill.thornhill/5-code-smells-that-are-a-dead-ringer-for-a-memory-leak-fc76c3e3eae8
- canonical_url
- https://medium.com/@jill.thornhill/5-code-smells-that-are-a-dead-ringer-for-a-memory-leak-fc76c3e3eae8
- author_url
- https://medium.com/@jill.thornhill
- status
- ok
- fetched_at
- 2026-06-13 07:35:29