ClassLoader leaks are one of the hardest Java memory problems to diagnose. Unlike typical heap leaks where objects pile up, ClassLoader leaks occur in the Metaspace (or PermGen in older JVMs), making them invisible to standard heap analysis until the application crashes with java.lang.OutOfMemoryError: Metaspace. A single unreleased reference to any object from a dynamically loaded class keeps the entire ClassLoader alive, which in turn keeps every class it loaded in memory.
This guide walks through how to monitor Java class loading, detect ClassLoader leaks using heap dumps and runtime profiling, and apply fixes that prevent redeployment crashes and production OutOfMemoryErrors.
Prerequisites
Before starting, ensure you have:
- Java application running on JDK 8 or later
- Access to the JVM process ID and ability to generate heap dumps
- Eclipse MAT (Memory Analyzer Tool) installed for heap dump analysis
- Basic understanding of Java class loading hierarchy (bootstrap, extension, application, and custom class loaders)
- Production access or staging environment that reproduces the leak
If your application runs in Kubernetes or containerized environments, verify you can exec into containers and have sufficient disk space to store heap dumps (typically 1–2x heap size).
Step 1: Enable Class Loading Metrics Collection
Start by enabling JVM flags that expose class loading metrics. Add the following flags to your application startup script:
-XX:+TraceClassLoading
-XX:+TraceClassUnloading
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/gc.log
For containerized applications using OpenTelemetry, configure the JMX metrics receiver to export ClassLoader metrics:
receivers:
jmx:
jar_path: /opt/opentelemetry-jmx-metrics.jar
endpoint: service:jmx:rmi:///jndi/rmi://localhost:9010/jmxrmi
target_system: jvm
collection_interval: 10s
resource_attributes:
service.name: your-app-name
exporters:
otlp:
endpoint: http://your-observability-platform:4317
service:
pipelines:
metrics:
receivers: [jmx]
exporters: [otlp]
This configuration sends ClassLoader count, loaded class count, and unloaded class count to your observability platform every 10 seconds. Monitor the jvm.classes.loaded and jvm.classes.unloaded metrics. If loaded classes increase steadily while unloaded classes remain near zero, you have a ClassLoader leak.
Step 2: Reproduce the Leak in a Controlled Environment
ClassLoader leaks typically surface during application redeployment. Deploy your application, let it run under normal load, then redeploy without restarting the JVM. Repeat this process 3–5 times while monitoring Metaspace usage.
Use jstat to track Metaspace growth in real time:
jstat -gc <pid> 1000
Watch the MU (Metaspace Used) column. If it increases after each redeployment and never drops even after full GC events, you have confirmed a ClassLoader leak.
For web applications running in Tomcat or similar containers, the leak often appears as repeated OutOfMemoryError: Metaspace crashes after 2–3 hot redeploys. Application servers like JBoss 4.x were infamous for this exact problem, requiring full restarts between every deployment.
Step 3: Generate and Analyze a Heap Dump
Once you have reproduced the leak, undeploy the application and trigger a full garbage collection followed by a heap dump:
jcmd <pid> GC.run
jcmd <pid> GC.heap_dump /tmp/heapdump.hprof
For older JVMs, use jmap:
jmap -dump:live,format=b,file=/tmp/heapdump.hprof <pid>
Transfer the heap dump to your analysis machine and open it in Eclipse MAT. The file size will be 1–2x your heap size, so ensure adequate disk space.
In MAT, navigate to the Histogram view and filter for java.lang.ClassLoader:
.*ClassLoader
Look for custom ClassLoader instances. If you see hundreds or thousands of instances of the same ClassLoader type (e.g., org.apache.catalina.loader.WebappClassLoader), each representing a previous deployment, you have confirmed the leak.
Right-click on the ClassLoader instance and select “Path to GC Roots” → “exclude weak references”. This shows what is preventing the ClassLoader from being garbage collected. Common culprits include:
- Static references in surviving classes
- ThreadLocal variables holding references to old application objects
- Cached objects in third-party libraries (JDBC drivers, logging frameworks)
- Background threads started by the application but never stopped
Step 4: Identify the Root Reference Preventing ClassLoader GC
Trace the reference chain from the ClassLoader back to the GC root. A typical leak path looks like this:
GC Root (System ClassLoader)
↓
Static field in PluginRegistry class
↓
HashMap<String, Plugin> loadedPlugins
↓
Plugin instance (loaded by custom ClassLoader)
↓
Custom ClassLoader instance
↓
All classes loaded by that ClassLoader
In this example, a static HashMap in PluginRegistry holds references to plugin instances. Because plugins are loaded by a custom ClassLoader, and objects hold references to their classes, and classes hold references to their ClassLoader, the entire ClassLoader remains in memory.
The fix is to add an unregister method:
public class PluginRegistry {
private static final Map<String, Plugin> loadedPlugins = new ConcurrentHashMap<>();
public static void registerPlugin(String name, Plugin plugin) {
loadedPlugins.put(name, plugin);
}
public static void unregisterPlugin(String name) {
loadedPlugins.remove(name);
}
public static void unregisterAll() {
loadedPlugins.clear();
}
}
Call PluginRegistry.unregisterAll() in the application shutdown hook or servlet context listener:
@WebListener
public class AppContextListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent sce) {
PluginRegistry.unregisterAll();
}
}
Step 5: Fix ThreadLocal and Background Thread Leaks
ThreadLocal variables are a common source of ClassLoader leaks. If a ThreadLocal holds a reference to an application object, and the thread outlives the application deployment, the ClassLoader stays in memory.
Identify ThreadLocal usage in your codebase and ensure cleanup in a shutdown hook:
public class RequestContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
public static void setUser(User user) {
currentUser.set(user);
}
public static User getUser() {
return currentUser.get();
}
public static void clear() {
currentUser.remove();
}
}
Register a context listener to clear ThreadLocals on shutdown:
@Override
public void contextDestroyed(ServletContextEvent sce) {
RequestContext.clear();
}
Background threads are another common leak source. If your application starts threads that are never stopped, they hold references to the application ClassLoader. Always shut down thread pools and background tasks during application undeployment:
private final ExecutorService executor = Executors.newFixedThreadPool(10);
@Override
public void contextDestroyed(ServletContextEvent sce) {
executor.shutdown();
try {
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
Step 6: Monitor Class Loading with APM Tools
Manual heap dump analysis is effective but reactive. For continuous monitoring, integrate Java class loading metrics into your observability platform. Tools that support infrastructure monitoring can track JVM metrics in real time and alert when Metaspace usage crosses thresholds.
If you are using CubeAPM, configure the JMX receiver as shown in Step 1. CubeAPM natively supports JVM runtime metrics and correlates class loading behavior with application traces and logs, making it easier to pinpoint which deployment or code path triggered a ClassLoader leak.
Create an alert rule for Metaspace usage:
alerts:
- name: metaspace-leak-detection
condition: jvm.memory.used{area="metaspace"} > 0.8 * jvm.memory.max{area="metaspace"}
for: 5m
annotations:
summary: "Metaspace usage above 80% for 5 minutes"
description: "Possible ClassLoader leak detected"
This alert fires when Metaspace usage stays above 80% of max capacity for 5 minutes, giving you early warning before an OutOfMemoryError crashes the application.
Step 7: Verify the Fix with Load Testing
After applying fixes, redeploy the application multiple times in a staging environment while monitoring Metaspace and loaded class count. Run a load test between each deployment to simulate production traffic.
Track the following metrics:
jvm.classes.loaded— should increase during startup and stabilizejvm.classes.unloaded— should increase after each undeploymentjvm.memory.used{area="metaspace"}— should drop after full GC following undeployment
If Metaspace usage remains flat or decreases after each redeployment, the leak is fixed. If it continues climbing, return to Step 3 and generate another heap dump to identify remaining references.
Troubleshooting Common Issues
Issue: Metaspace still growing after ThreadLocal and static reference cleanup
Third-party libraries often register shutdown hooks or cache ClassLoader references. Common culprits include JDBC drivers, logging frameworks like Log4j, and dependency injection containers. Check the MAT dominator tree for references from java.sql.DriverManager, org.apache.logging, or framework-specific classes. Explicitly deregister JDBC drivers in your shutdown hook:
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
DriverManager.deregisterDriver(drivers.nextElement());
}
Issue: Classes are unloaded but Metaspace usage does not drop
The JVM does not always release Metaspace memory back to the OS immediately. Metaspace is allocated in chunks, and even after classes are unloaded, the JVM may retain those chunks for future use. Monitor jvm.memory.committed{area="metaspace"} instead of jvm.memory.used. If committed memory stabilizes or decreases, the leak is resolved even if used memory shows slower decline.
Issue: ClassLoader leak only happens in production, not in staging
Production environments often have longer-running threads, higher concurrency, or different library versions. Enable -XX:+TraceClassLoading and -XX:+TraceClassUnloading in production temporarily to capture which classes are loaded but never unloaded. Compare the output with staging logs to identify environment-specific differences.
Issue: Heap dump analysis shows no obvious GC root path to ClassLoader
Some leaks are caused by weak references that are not excluded by default in MAT’s GC root analysis. Try analyzing without excluding weak references, or search the heap dump for known leak-prone classes like ThreadLocal, ThreadPoolExecutor, or ConcurrentHashMap and inspect their contents manually.
Monitoring Java Class Loading with CubeAPM
CubeAPM provides runtime JVM monitoring with automatic correlation between class loading metrics, application traces, and infrastructure health. Unlike generic monitoring tools, CubeAPM runs on-prem inside your VPC, ensuring JVM telemetry never leaves your infrastructure.
To monitor Java class loading with CubeAPM:
- Deploy the OpenTelemetry Java agent with CubeAPM’s OTLP endpoint configured
- Enable JMX metrics collection targeting
java.lang:type=ClassLoadingMBeans - Create a dashboard tracking
jvm.classes.loaded,jvm.classes.unloaded, and Metaspace usage over time - Set threshold alerts for Metaspace growth or stagnant unloaded class counts
CubeAPM correlates class loading spikes with deployment events and application traces, making it faster to identify which code path or library triggered the leak. For teams running microservices or frequent deployments, this visibility reduces mean time to detection from hours to minutes.
CubeAPM’s flat $0.2/GB pricing includes unlimited retention, so you can track class loading trends over months to detect gradual Metaspace creep before it becomes a production incident.
Conclusion
ClassLoader leaks are invisible to standard heap monitoring but cause production OutOfMemoryErrors that crash applications during redeployment. The fix requires identifying root references from static fields, ThreadLocals, or background threads that prevent ClassLoaders from being garbage collected.
Enable class loading metrics, generate heap dumps after redeployment, and use Eclipse MAT to trace references back to GC roots. Clean up static caches, deregister JDBC drivers, and shut down thread pools in application shutdown hooks. Verify fixes with load testing and monitor Metaspace usage continuously in production.
Disclaimer: The information in this article reflects the latest details available at the time of publication and may change as technologies and products evolve. Features, pricing, and plan limits can change over time. Always verify the latest information directly with the vendor before making purchasing or deployment decisions.
Frequently Asked Questions
What is a ClassLoader leak in Java?
A ClassLoader leak occurs when a ClassLoader cannot be garbage collected because live references exist to objects or classes it loaded. This keeps all classes loaded by that ClassLoader in memory, filling Metaspace until the JVM crashes.
How do I detect a ClassLoader leak?
Monitor Metaspace usage and loaded class count during application redeployments. If Metaspace grows and never decreases after undeployment, generate a heap dump and search for duplicate ClassLoader instances in Eclipse MAT.
What causes ClassLoader leaks?
Common causes include static references to application objects, ThreadLocal variables holding old objects, background threads not stopped on shutdown, and third-party libraries that cache ClassLoader references.
How do I fix a ClassLoader leak?
Identify the GC root preventing ClassLoader garbage collection using heap dump analysis. Clear static caches, remove ThreadLocal values, shut down background threads, and deregister JDBC drivers in application shutdown hooks.
Can ClassLoader leaks happen in production without redeployment?
Yes, if the application uses dynamic class generation (e.g., CGLIB, ASM, or scripting engines like Groovy), new classes can be created at runtime and leaked if not properly released.
What tools help monitor Java class loading?
Use `jstat` for real time Metaspace tracking, Eclipse MAT for heap dump analysis, and APM tools that support JMX metrics collection like CubeAPM for continuous monitoring and alerting.
How much memory does a leaked ClassLoader consume?
Each leaked ClassLoader retains all classes it loaded, plus any static fields and referenced objects. In large applications, a single leaked ClassLoader can hold 50–200 MB of Metaspace and heap memory.





