How to Identify and Fix Class Loader Leaks in Java Web Applications: A Step-by-Step Guide
The Scenario
In modern Java web applications, dynamic class loading using custom class loaders is a common practice, especially when dealing with plugins or modular components. However, improper handling of these class loaders can lead to memory leaks, causing severe performance issues over time. In this article, we will walk through a practical approach to identify and resolve class loader leaks, using real-world examples and diagrams to illustrate each step.
Step-by-Step Analysis
1. Generate a Heap Dump
Triggering a Heap Dump: When your application is experiencing high memory usage or you suspect a memory leak, the first step is to generate a heap dump. This can be done using tools like jmap or jconsole.
jmap -dump:live,format=b,file=heapdump.hprof <pid>
2. Load the Heap Dump
Opening the Heap Dump: Use tools like Eclipse MAT (Memory Analyzer Tool) to load the heap dump. Open Eclipse MAT, and load the .hprof file you generated.
3. Initial Analysis
Overview and Histogram: Start with the Overview page to get a snapshot of memory usage. Then, navigate to the Histogram view to identify the classes and objects consuming the most memory.
In MAT's overview, we notice a large number of class loaders:
Class Name | Instances | Shallow Heap | Retained Heap -----------------------------|-----------|--------------|--------------- com.example.PluginClassLoader| 1,500 | 120,000 B | 500 MB
4. Analyze Class Loaders
Histogram Filter: Filter for java.lang.ClassLoader in the Histogram view to see class loader instances and their retained memory.
Regex: .*ClassLoader
We see multiple instances of our custom class loader:
Class Name | Instances | Shallow Heap | Retained Heap -----------------------------|-----------|--------------|--------------- com.example.PluginClassLoader| 1,500 | 120,000 B | 500 MB
Diagram Tip: Create a pie chart of class loader instances and their retained heap sizes.
5. Examine Dominator Tree
In the dominator tree, we find:
Class Name | Retained Heap -----------------------------|--------------- com.example.PluginClassLoader| 500 MB |- com.example.PluginA | 100 MB |- com.example.PluginB | 150 MB |- ...
Diagram Tip: Create a tree diagram showing the dominator hierarchy.
6. Inspect Class Loader Retention
领英推荐
Analyzing the path to GC roots for a PluginClassLoader instance:
PluginClassLoader@0x12345678
<- static field PluginRegistry.loadedPlugins
<- PluginRegistry class
Diagram Tip: Create a chain diagram showing the reference path from GC root to the class loader.
7. Identify Unnecessary References
We discover thatPluginRegistryholds static references to all loaded plugins:
public class PluginRegistry {
private static final Map<String, Plugin> loadedPlugins = new HashMap<>();
public static void registerPlugin(String name, Plugin plugin)
{ loadedPlugins.put(name, plugin); }
// Missing: Method to unregister plugins }
8. Analyze and Fix the Leak
The issue: Plugins (and their class loaders) are never unregistered, causing a memory leak.
public static void unregisterPlugin(String name) {
loadedPlugins.remove(name);
}
Verification
After implementing the fix:
Class Name | Instances | Shallow Heap | Retained Heap -----------------------------|-----------|--------------|--------------- com.example.PluginClassLoader| 10 | 800 B | 5 MB
Diagram Tip: Create a before/after bar chart comparing class loader instances and retained heap.
References