10 popular memory leaks in Java
Grrr memory leaks! You see, in my journey as a software developer, I’ve always been aware that these little things aren’t a big cause for concern… until the moment the Java project underperforms in production. That’s exactly when the panic sets in, and I frantically search for a quick fix to remedy this poor performance.
I recall, in one such instance, the fastest solution was to scale the infrastructure vertically — sounds expensive, right? 🤔. Let’s explore another solution for finding and fixing all such memory leaks during the coding phase itself.
1. Static Fields and Collections
Static fields and collections cause a challenge for garbage collection, as their lifecycle matches the application’s lifecycle. They often lead to memory leaks if not managed carefully.
Example: Static HashMap
Consider the following code snippet, where an User
object is placed into a static HashMap and never removed.
public class User {
private String userName;
// Static HashMap storing User objects
private static Map<String, User> users = new HashMap<>();
// Constructor
public User(String userName) {
this.userName = userName;
users.put(userName, this);
}
// other methods
}
The User
objects placed into the static HashMap users
will never be garbage collected unless explicitly removed from the HashMap.
Solution
To prevent such memory leaks, make sure that objects are removed from static fields or collections when they are no longer in use. One solution is to use a WeakHashMap
, which automatically removes entries when the keys are no longer needed.
private static Map<String, User> users = new WeakHashMap<>();
2. Unclosed Resources
Not closing resources, such as streams or connections, can also lead to memory leaks. In this case, the leak occurs outside the Java heap, in native memory or off-heap.
Example: FileInputStream
Consider the following code snippet, which reads data from a file:
public void readDataFromFile(String filePath) {
try {
FileInputStream fis = new FileInputStream(filePath);
// Reading data from the file
} catch (IOException e) {
e.printStackTrace();
}
}
In this example, the FileInputStream is not closed after usage, leading to memory leaks.
Solution
Always close the resources after they are no longer required. Java 7 introduced try-with-resources, which automatically closes the resources when the block ends.
public void readDataFromFile(String filePath) {
try (FileInputStream fis = new FileInputStream(filePath)) {
// Reading data from the file
} catch (IOException e) {
e.printStackTrace();
}
}
3. ThreadLocal Variables
ThreadLocal variables allow multiple threads to have their own instance of a shared object, preventing interference among them. However, misuse of ThreadLocal variables can lead to memory leaks, as objects may persist long after the thread has completed execution.
Example: Custom ThreadLocal
public class CustomThreadLocal {
public static final ThreadLocal<SimpleDateFormat> dateFormatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public String formatDate(Date date) {
return dateFormatter.get().format(date);
}
}
In this example, we store a SimpleDateFormat object in a ThreadLocal variable. However, if the thread is not managed, the SimpleDateFormat object may never be garbage collected.
Solution
To avoid memory leaks in this case, clean up the variables once the thread has completed its task. The following code snippet demonstrates the use of a remove
method to accomplish this:
public void cleanup() {
dateFormatter.remove();
}
4. Caching without Limits
Cache stores previously calculated values for faster retrieval. However, an unbounded or improperly managed cache may cause memory leaks.
Example: HashMap-based Cache
public class SimpleCache {
private final Map<String, BigDecimal> cache = new HashMap<>();
public BigDecimal getValue(String key) {
BigDecimal value = cache.get(key);
if (value == null) {
value = calculateValue(key);
cache.put(key, value);
}
return value;
}
private BigDecimal calculateValue(String key) {
// A time-consuming operation to calculate the value
return new BigDecimal("123.45");
}
}
In this example, we use a simple HashMap-based cache to store results. However, this cache is unbounded, leading to memory leaks when there are too many entries.
Solution:
Limit the size of the cache and use a suitable eviction policy. Libraries like Google Guava provide configurable caching solutions. Here’s an example using Guava’s CacheBuilder:
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public class LimitedCache {
private final Cache<String, BigDecimal> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build();
public BigDecimal getValue(String key) {
BigDecimal value = cache.getIfPresent(key);
if (value == null) {
value = calculateValue(key);
cache.put(key, value);
}
return value;
}
private BigDecimal calculateValue(String key) {
// A time-consuming operation to calculate the value
return new BigDecimal("123.45");
}
}
In this solution, the cache size is limited to 1,000 entries, and the eviction policy is set to remove the oldest entries when the cache reaches its maximum size.
5. Improper use of Event Listeners
Adding listeners to different events is a popular pattern in Java programming. However, not removing listeners when they are no longer needed can lead to memory leaks.
Example: Anonymous Event Listener
class MyButton {
private List<ActionListener> listeners = new ArrayList<>();
public void addActionListener(ActionListener listener) {
listeners.add(listener);
}
public void doAction() {
for (ActionListener listener : listeners) {
listener.actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Click"));
}
}
}
public class Main {
public static void main(String[] args) {
MyButton button = new MyButton();
for (int i = 0; i < 10; i++) {
// Adding new anonymous listeners
button.addActionListener(e -> System.out.println("Button clicked"));
}
}
}
Here, we create and add anonymous action listeners to a custom button. However, the listeners are never removed, causing memory leaks.
Solution
To prevent memory leaks caused by event listeners, ensure that listeners are removed when they are no longer required. One solution is to use a WeakReference to hold the event listener. Another approach is to ensure that any long-lived or large objects holding the listeners are removed as soon as they are finished with it:
button.removeActionListener(listener);
6. Uncollected Garbage Collection Roots
Garbage Collection (GC) roots are objects that are always reachable and, therefore, are never garbage collected. Some frequent GC roots include static variables, threads, and local variables in the main thread.
If GC roots hold references to unneeded objects, they prevent these objects from being garbage collected.
Example: Object Not Garbage Collected
public static List<BigDecimal> numbers = new ArrayList<>();
public void getData() {
while (dataAvailable()) {
BigDecimal number = getNextNumber();
numbers.add(number);
}
processData(numbers);
}
In this example, the static numbers
variable holds references to objects. As long as the numbers
list is not cleared, it will cause a memory leak in the application.
Solution
Ensure that such GC roots release objects when they are no longer needed. In this example, we can clear the numbers
list when the processing is finished:
public void getData() {
while (dataAvailable()) {
BigDecimal number = getNextNumber();
numbers.add(number);
}
processData(numbers);
numbers.clear(); // Free up memory
}
7. Mismanaged Thread Pools
Improper management of thread pools can lead to memory leaks, mainly when Java applications use thread pools with an unbounded number of threads or do not release resources.
Example: Unclosed Executors
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> {
// Some actions
});
}
}
}
ExecutorService
. However, we do not shut down ExecutorService
properly, leading to memory leaks.
Solution
To fix memory leaks related to thread pools, ensure that resources are released and threads are terminated in a controlled manner. Shut down the ExecutorService
properly once all tasks have been executed:
public class Main {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
executorService.submit(() -> {
// Some actions
});
}
// Shutdown the ExecutorService
executorService.shutdown();
}
}
8. Singleton Pattern Misuse
Singleton objects are designed to have only one instance during the lifetime of an application. However, improper usage of the Singleton pattern can lead to memory leaks.
Example: Singleton Object
public class Singleton {
private static final Singleton instance = new Singleton();
private List<BigDecimal> data = new ArrayList<>();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
public void addData(BigDecimal value) {
data.add(value);
}
// Other methods
}
In this example, the Singleton
object holds references to BigDecimal
objects through its data
list. The data
list can grow indefinitely, leading to memory leaks.
Solution
To avoid memory leaks related to Singleton objects, be cautious when using the pattern and ensure that you release or limit the resources consumed by the Singleton instance:
public class Singleton {
private static final Singleton instance = new Singleton();
private List<BigDecimal> data = new ArrayList<>();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
public void addData(BigDecimal value) {
data.add(value);
}
public void clearData() {
data.clear();
}
// Other methods
}
In the improved solution, a clearData
method is added to release the resources held by the Singleton
instance.
9. Deep and Complex Object Graphs
Applications with complicated object graphs can become hard to manage, making it difficult to determine when objects can be garbage collected. Memory leaks may arise when unreachable objects remain attached to the object graph.
Example: Customer and Orders
public class Customer {
private List<Order> orders = new ArrayList<>();
public void addOrder(Order order) {
orders.add(order);
}
// Getter and setters
}
public class Order {
private List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
// Getter and setters
}
public class Item {
private String name;
private BigDecimal price;
// Getter and setters
}
In this example, Customer
objects have references to Order
objects, and Order
objects have references to Item
objects. If a Customer
object is no longer needed but is not properly detached from the object graph, memory leaks can occur.
Solution
Ensure that you properly manage object references and relationships. Use techniques like the Observer pattern, or weak references, to avoid such memory leaks:
import java.lang.ref.WeakReference;
public class Customer {
private List<WeakReference<Order>> orders = new ArrayList<>();
public void addOrder(Order order) {
orders.add(new WeakReference<>(order));
}
// Getter and setters
}
10. Third-Party Libraries
Memory leaks can also be introduced by third-party libraries if they have bugs or are misconfigured.
Example: XML Parsing
Some XML parsers, like Xerces, may cause memory leaks when custom EntityResolvers are used.
Solution
To avoid memory leaks due to third-party libraries:
- Keep libraries updated to the latest stable version.
- Understand how the library works and any potential memory issues.
- Configure the library according to best practices and recommendations.
In the case of the XML parser, customizing the EntityResolver or switching to a different XML parser implementation can help avoid memory leaks.
// Customize EntityResolver
public class CustomEntityResolver implements EntityResolver {
// Implementation to resolve entities
}
I hope this was informative and productive for your next JIRA dev task.
Hope you like it
Peace!