Hidden Java Memory Leaks: 8 Common Pitfalls and How to Fix Them
This article explains eight typical Java memory‑leak scenarios—including unclosed resources, missing equals/hashCode, non‑static inner classes, overridden finalize, String.intern misuse, ThreadLocal traps, and static variables in web containers—provides code examples for each, and offers practical mitigation strategies.
1. Definition of Memory Leak
If the garbage collector cannot reclaim an object that is no longer used, it is considered a memory leak.
2. Unclosed Resources
Opening streams or network connections allocates memory; if an exception prevents closing, the resources remain allocated. The following pattern should be used to ensure proper cleanup.
public void handleResource() {
try {
// open connection
// handle business
} catch (Throwable t) {
// log stack
} finally {
// close connection
}
}3. Missing equals() and hashCode()
When a class does not override equals and hashCode, collections such as HashMap use object identity, leading to unexpected growth. Example of a class without overrides:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
}A unit test that inserts many identical Person objects into a map will show a size of 100 instead of 1. Overriding the methods fixes the issue:
public class Person {
public String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person person = (Person) o;
return person.name.equals(name);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + name.hashCode();
return result;
}
}4. Non‑Static Inner Classes
Non‑static inner classes hold an implicit reference to their outer class, which can prevent the outer class from being garbage‑collected, especially in Android where long‑running tasks keep the outer activity alive.
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
new MyAsyncTask().execute();
}
private class MyAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
return doSomeStuff();
}
private Object doSomeStuff() {
// do something to get result
return new MyObject();
}
}
}5. Overriding finalize()
Classes that override finalize() are handled by a special java.lang.ref.Finalizer object. If many instances are created, they are placed in a ReferenceQueue processed by a low‑priority background thread, which can cause OutOfMemoryError.
public class Finalizer {
@Override
protected void finalize() throws Throwable {
while (true) {
Thread.yield();
}
}
public static void main(String[] str) {
while (true) {
for (int i = 0; i < 100000; i++) {
Finalizer force = new Finalizer();
}
}
}
}6. Using String.intern() on Large Strings
In Java 1.6 and earlier, the string constant pool resides in PermGen, which is not garbage‑collected, so interning large strings can exhaust memory. From Java 1.7 onward the pool is on the heap, eliminating this issue.
@Test
public void givenLengthString_whenIntern_thenOutOfMemory() throws IOException, InterruptedException {
String str = new Scanner(new File("src/test/resources/large.txt"), "UTF-8")
.useDelimiter("\\A").next();
str.intern();
System.gc();
Thread.sleep(15000);
}7. Misusing ThreadLocal
Improper handling of ThreadLocal can retain large objects for the lifetime of a thread pool, causing memory leaks. The following example demonstrates how resetting the ThreadLocal reference leaves the previously stored value unreachable for GC.
@Test
public void testThreadLocalMemoryLeaks() {
ThreadLocal<List<Integer>> localCache = new ThreadLocal<>();
List<Integer> cacheInstance = new ArrayList<>(10000);
localCache.set(cacheInstance);
localCache = new ThreadLocal<>(); // old ThreadLocal still holds cacheInstance
}8. Static Variables in Web Applications
Static fields that hold ThreadLocal instances can cause class‑loader leaks in containers like Tomcat. When a web application is undeployed but threads remain alive, the static ThreadLocal retains a reference to the webapp classloader, preventing it from being garbage‑collected.
public class MyCounter {
private int count = 0;
public void increment() { count++; }
public int getCount() { return count; }
}
public class MyThreadLocal extends ThreadLocal<MyCounter> {}
public class LeakingServlet extends HttpServlet {
private static MyThreadLocal myThreadLocal = new MyThreadLocal();
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
MyCounter counter = myThreadLocal.get();
if (counter == null) {
counter = new MyCounter();
myThreadLocal.set(counter);
}
response.getWriter().println("The current thread served this servlet " + counter.getCount() + " times");
counter.increment();
}
}Key points: the static MyThreadLocal must be loaded by the webapp classloader, and the classloader itself can be leaked through the retained reference.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
