Skip to content

Commit 42beaa5

Browse files
Flossyclaude
andcommitted
Add ClassLoaderStatistics and ClassLoaderCleanupUtil utilities
Add utilities to jclassloader.util package: - ClassLoaderStatistics: track class loading stats (classes loaded, bytes, cache hits) - ClassLoaderCleanupUtil: comprehensive ClassLoader cleanup to prevent memory leaks - ThreadLocal cleanup - JDBC driver deregistration - JMX MBean cleanup - Shutdown hook removal - ResourceBundle cache clearing - Leak detection with diagnostic logging Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 22ad653 commit 42beaa5

3 files changed

Lines changed: 628 additions & 0 deletions

File tree

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
package org.flossware.jclassloader.util;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
import javax.management.MBeanServer;
7+
import javax.management.ObjectName;
8+
import java.lang.management.ManagementFactory;
9+
import java.lang.ref.WeakReference;
10+
import java.lang.reflect.Field;
11+
import java.sql.Driver;
12+
import java.sql.DriverManager;
13+
import java.util.ArrayList;
14+
import java.util.Enumeration;
15+
import java.util.List;
16+
import java.util.Map;
17+
import java.util.Set;
18+
19+
/**
20+
* Utility class for cleaning up ClassLoader resources to prevent memory leaks.
21+
*
22+
* <p>ClassLoader leaks are a common problem in Java platforms that load and unload
23+
* applications dynamically. This utility provides comprehensive cleanup for common
24+
* leak sources:</p>
25+
* <ul>
26+
* <li>ThreadLocal variables</li>
27+
* <li>JDBC drivers registered with DriverManager</li>
28+
* <li>JMX MBeans</li>
29+
* <li>Shutdown hooks</li>
30+
* <li>Resource bundle caches</li>
31+
* </ul>
32+
*
33+
* <p>Usage:</p>
34+
* <pre>
35+
* ClassLoaderCleanupUtil cleanup = new ClassLoaderCleanupUtil("my-app", classLoader);
36+
* cleanup.cleanupAll();
37+
* cleanup.detectLeaks(); // Verify cleanup succeeded
38+
* </pre>
39+
*
40+
* @since 2.0
41+
*/
42+
public class ClassLoaderCleanupUtil {
43+
44+
private static final Logger logger = LoggerFactory.getLogger(ClassLoaderCleanupUtil.class);
45+
46+
private final String applicationId;
47+
private final ClassLoader classLoader;
48+
private final WeakReference<ClassLoader> leakDetector;
49+
50+
/**
51+
* Creates a new cleanup utility for the specified ClassLoader.
52+
*
53+
* @param applicationId the application identifier for logging
54+
* @param classLoader the ClassLoader to clean up
55+
*/
56+
public ClassLoaderCleanupUtil(String applicationId, ClassLoader classLoader) {
57+
this.applicationId = applicationId;
58+
this.classLoader = classLoader;
59+
this.leakDetector = new WeakReference<>(classLoader);
60+
}
61+
62+
/**
63+
* Performs all cleanup operations.
64+
*
65+
* <p>This is the main entry point for ClassLoader cleanup. It calls all
66+
* individual cleanup methods in the correct order.</p>
67+
*/
68+
public void cleanupAll() {
69+
logger.info("[{}] Starting ClassLoader cleanup", applicationId);
70+
71+
cleanupThreadLocals();
72+
cleanupJdbcDrivers();
73+
cleanupMBeans();
74+
cleanupShutdownHooks();
75+
cleanupResourceBundles();
76+
77+
logger.info("[{}] ClassLoader cleanup completed", applicationId);
78+
}
79+
80+
/**
81+
* Removes ThreadLocal variables from application threads.
82+
*
83+
* <p>ThreadLocals are a common source of ClassLoader leaks. This method
84+
* clears ThreadLocals from all threads whose names contain the application ID.</p>
85+
*/
86+
public void cleanupThreadLocals() {
87+
try {
88+
int cleaned = 0;
89+
Map<Thread, StackTraceElement[]> threads = Thread.getAllStackTraces();
90+
91+
for (Thread thread : threads.keySet()) {
92+
if (thread.getName().contains(applicationId)) {
93+
if (clearThreadLocals(thread)) {
94+
cleaned++;
95+
}
96+
}
97+
}
98+
99+
logger.info("[{}] Cleaned ThreadLocals from {} threads", applicationId, cleaned);
100+
} catch (Exception e) {
101+
logger.warn("[{}] Failed to clean ThreadLocals: {}", applicationId, e.getMessage());
102+
}
103+
}
104+
105+
/**
106+
* Clears ThreadLocals from a specific thread using reflection.
107+
*
108+
* @param thread the thread to clean
109+
* @return true if cleanup succeeded
110+
*/
111+
private boolean clearThreadLocals(Thread thread) {
112+
try {
113+
// Access Thread.threadLocals field
114+
Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
115+
threadLocalsField.setAccessible(true);
116+
Object threadLocalMap = threadLocalsField.get(thread);
117+
118+
if (threadLocalMap != null) {
119+
// Access ThreadLocalMap.table field
120+
Class<?> threadLocalMapClass = threadLocalMap.getClass();
121+
Field tableField = threadLocalMapClass.getDeclaredField("table");
122+
tableField.setAccessible(true);
123+
Object[] table = (Object[]) tableField.get(threadLocalMap);
124+
125+
if (table != null) {
126+
// Clear entries loaded by our ClassLoader
127+
for (int i = 0; i < table.length; i++) {
128+
Object entry = table[i];
129+
if (entry != null) {
130+
// Check if entry's value was loaded by our ClassLoader
131+
Field valueField = entry.getClass().getDeclaredField("value");
132+
valueField.setAccessible(true);
133+
Object value = valueField.get(entry);
134+
135+
if (value != null && value.getClass().getClassLoader() == classLoader) {
136+
table[i] = null; // Clear the entry
137+
}
138+
}
139+
}
140+
}
141+
}
142+
143+
// Also clean inheritableThreadLocals
144+
Field inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals");
145+
inheritableThreadLocalsField.setAccessible(true);
146+
inheritableThreadLocalsField.set(thread, null);
147+
148+
return true;
149+
} catch (Exception e) {
150+
logger.debug("[{}] Could not clear ThreadLocals for thread {}: {}",
151+
applicationId, thread.getName(), e.getMessage());
152+
return false;
153+
}
154+
}
155+
156+
/**
157+
* Deregisters JDBC drivers loaded by this ClassLoader.
158+
*
159+
* <p>JDBC drivers registered with DriverManager create a permanent reference
160+
* to the ClassLoader that loaded them. This method deregisters all drivers
161+
* loaded by our ClassLoader.</p>
162+
*/
163+
public void cleanupJdbcDrivers() {
164+
try {
165+
List<Driver> driversToDeregister = new ArrayList<>();
166+
Enumeration<Driver> drivers = DriverManager.getDrivers();
167+
168+
while (drivers.hasMoreElements()) {
169+
Driver driver = drivers.nextElement();
170+
if (driver.getClass().getClassLoader() == classLoader) {
171+
driversToDeregister.add(driver);
172+
}
173+
}
174+
175+
for (Driver driver : driversToDeregister) {
176+
DriverManager.deregisterDriver(driver);
177+
logger.info("[{}] Deregistered JDBC driver: {}", applicationId, driver.getClass().getName());
178+
}
179+
180+
if (!driversToDeregister.isEmpty()) {
181+
logger.info("[{}] Deregistered {} JDBC drivers", applicationId, driversToDeregister.size());
182+
}
183+
} catch (Exception e) {
184+
logger.warn("[{}] Failed to cleanup JDBC drivers: {}", applicationId, e.getMessage());
185+
}
186+
}
187+
188+
/**
189+
* Unregisters JMX MBeans registered by this ClassLoader.
190+
*
191+
* <p>MBeans registered by applications can hold references to the ClassLoader.
192+
* This method unregisters all MBeans whose ObjectName contains the application ID.</p>
193+
*/
194+
public void cleanupMBeans() {
195+
try {
196+
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
197+
Set<ObjectName> allMBeans = mbs.queryNames(null, null);
198+
int unregistered = 0;
199+
200+
for (ObjectName name : allMBeans) {
201+
// Unregister MBeans that match our application ID
202+
if (name.toString().contains(applicationId)) {
203+
try {
204+
mbs.unregisterMBean(name);
205+
unregistered++;
206+
logger.debug("[{}] Unregistered MBean: {}", applicationId, name);
207+
} catch (Exception e) {
208+
logger.warn("[{}] Failed to unregister MBean {}: {}",
209+
applicationId, name, e.getMessage());
210+
}
211+
}
212+
}
213+
214+
if (unregistered > 0) {
215+
logger.info("[{}] Unregistered {} MBeans", applicationId, unregistered);
216+
}
217+
} catch (Exception e) {
218+
logger.warn("[{}] Failed to cleanup MBeans: {}", applicationId, e.getMessage());
219+
}
220+
}
221+
222+
/**
223+
* Removes shutdown hooks registered by application threads.
224+
*
225+
* <p>Applications may register shutdown hooks that hold references to the
226+
* ClassLoader. This method attempts to remove hooks for threads whose names
227+
* contain the application ID.</p>
228+
*/
229+
public void cleanupShutdownHooks() {
230+
try {
231+
// Access Runtime.shutdownHooks field
232+
Field hooksField = Runtime.class.getDeclaredField("shutdownHooks");
233+
hooksField.setAccessible(true);
234+
Map<Thread, Thread> hooks = (Map<Thread, Thread>) hooksField.get(Runtime.getRuntime());
235+
236+
List<Thread> toRemove = new ArrayList<>();
237+
for (Thread hook : hooks.keySet()) {
238+
if (hook.getName().contains(applicationId)) {
239+
toRemove.add(hook);
240+
}
241+
}
242+
243+
for (Thread hook : toRemove) {
244+
Runtime.getRuntime().removeShutdownHook(hook);
245+
logger.debug("[{}] Removed shutdown hook: {}", applicationId, hook.getName());
246+
}
247+
248+
if (!toRemove.isEmpty()) {
249+
logger.info("[{}] Removed {} shutdown hooks", applicationId, toRemove.size());
250+
}
251+
} catch (Exception e) {
252+
logger.warn("[{}] Failed to cleanup shutdown hooks: {}", applicationId, e.getMessage());
253+
}
254+
}
255+
256+
/**
257+
* Clears ResourceBundle caches that may hold ClassLoader references.
258+
*
259+
* <p>ResourceBundles are cached by ClassLoader, which can prevent garbage
260+
* collection. This method clears the cache using reflection.</p>
261+
*/
262+
public void cleanupResourceBundles() {
263+
try {
264+
Class<?> bundleClass = Class.forName("java.util.ResourceBundle");
265+
Field cacheListField = bundleClass.getDeclaredField("cacheList");
266+
cacheListField.setAccessible(true);
267+
Object cacheList = cacheListField.get(null);
268+
269+
if (cacheList != null) {
270+
synchronized (cacheList) {
271+
// Clear the cache (implementation varies by JDK version)
272+
if (cacheList instanceof Map) {
273+
((Map<?, ?>) cacheList).clear();
274+
logger.info("[{}] Cleared ResourceBundle cache", applicationId);
275+
}
276+
}
277+
}
278+
} catch (Exception e) {
279+
// ResourceBundle implementation details vary by JDK version
280+
logger.debug("[{}] Could not clear ResourceBundle cache: {}", applicationId, e.getMessage());
281+
}
282+
}
283+
284+
/**
285+
* Detects if the ClassLoader has been garbage collected.
286+
*
287+
* <p>This method should be called after {@link #cleanupAll()} to verify
288+
* that the ClassLoader is eligible for garbage collection. It triggers
289+
* a GC and checks if the WeakReference has been cleared.</p>
290+
*
291+
* @return true if the ClassLoader has been garbage collected, false if a leak is detected
292+
*/
293+
public boolean detectLeaks() {
294+
logger.info("[{}] Running leak detection...", applicationId);
295+
296+
// Suggest garbage collection
297+
System.gc();
298+
System.runFinalization();
299+
System.gc();
300+
301+
// Small delay to allow GC to run
302+
try {
303+
Thread.sleep(100);
304+
} catch (InterruptedException e) {
305+
Thread.currentThread().interrupt();
306+
}
307+
308+
// Check if ClassLoader was garbage collected
309+
if (leakDetector.get() != null) {
310+
logger.warn("[{}] CLASSLOADER LEAK DETECTED - ClassLoader was not garbage collected", applicationId);
311+
logLeakDiagnostics();
312+
return false;
313+
} else {
314+
logger.info("[{}] No ClassLoader leak detected", applicationId);
315+
return true;
316+
}
317+
}
318+
319+
/**
320+
* Logs diagnostic information to help identify leak sources.
321+
*/
322+
private void logLeakDiagnostics() {
323+
logger.warn("[{}] Leak diagnostics:", applicationId);
324+
logger.warn("[{}] - Check for static fields holding application objects", applicationId);
325+
logger.warn("[{}] - Check for threads still running with application classes", applicationId);
326+
logger.warn("[{}] - Check for external caches holding application objects", applicationId);
327+
logger.warn("[{}] - Use a heap profiler (VisualVM, JProfiler) to find reference chains", applicationId);
328+
329+
// Log threads that might hold references
330+
Map<Thread, StackTraceElement[]> threads = Thread.getAllStackTraces();
331+
for (Map.Entry<Thread, StackTraceElement[]> entry : threads.entrySet()) {
332+
Thread thread = entry.getKey();
333+
if (thread.getName().contains(applicationId) && thread.isAlive()) {
334+
logger.warn("[{}] - Active thread: {} (state: {})", applicationId, thread.getName(), thread.getState());
335+
}
336+
}
337+
}
338+
339+
/**
340+
* Returns the application ID.
341+
*
342+
* @return the application ID
343+
*/
344+
public String getApplicationId() {
345+
return applicationId;
346+
}
347+
}

0 commit comments

Comments
 (0)