Keywords: Tomcat8 | Memory Leak | ThreadLocal | JDBC Driver | Thread Management
Abstract: This paper provides a comprehensive analysis of memory leak warnings encountered when stopping Tomcat8 in Java 8 environments, focusing on issues caused by MySQL JDBC driver threads and custom ThreadLocalProperties classes. It explains the working principles of Tomcat's detection mechanisms, analyzes the root causes of improperly closed threads and uncleaned ThreadLocal variables, and offers practical solutions including moving JDBC drivers to Tomcat's lib directory, implementing graceful thread pool shutdowns, and optimizing ThreadLocal management. Through code examples and principle analysis, it helps developers understand and avoid common memory leak pitfalls in web applications.
Problem Background and Phenomenon Analysis
When stopping Tomcat8 running in Java 8 environments, memory leak warnings frequently appear, primarily involving two types of issues: unstopped threads and uncleaned ThreadLocal variables. From the provided error logs, two critical problems are evident: first, the MySQL JDBC driver's "Abandoned connection cleanup thread" fails to stop properly, and second, ThreadLocal variables created by the custom ThreadLocalProperties class are not removed when the application stops.
Tomcat Memory Leak Detection Mechanism
Tomcat incorporates a sophisticated memory leak detection mechanism that automatically checks for two common issues when web applications stop: threads started but not stopped by the application, and ThreadLocal variables created but not properly cleaned. These detections are based on Java class loader lifecycle management principles. When a web application stops, Tomcat attempts to unload the corresponding WebappClassLoader, but active threads or ThreadLocal references prevent normal class loader recycling, leading to memory leaks.
MySQL JDBC Driver Thread Analysis
The MySQL JDBC driver creates an "Abandoned connection cleanup thread" during initialization, which runs as a daemon thread responsible for cleaning up abandoned database connections. The issue is that this thread exists as a JVM-level singleton. When the JDBC driver is placed in the WEB-INF/lib directory, each web application loads its own driver instance, but the cleanup thread is shared at the JVM level. This causes the thread to remain active when Tomcat stops, preventing class loader unloading.
// Example: Core logic of MySQL driver thread creation
public class AbandonedConnectionCleanupThread extends Thread {
private static volatile AbandonedConnectionCleanupThread instance = null;
public static synchronized void initialize() {
if (instance == null) {
instance = new AbandonedConnectionCleanupThread();
instance.setDaemon(true);
instance.start();
}
}
@Override
public void run() {
while (!shutdown) {
try {
Reference<?> ref = referenceQueue.remove(5000);
// Clean abandoned connections
} catch (InterruptedException e) {
// Handle interruption
}
}
}
}
ThreadLocalProperties Implementation Issues
The provided ThreadLocalProperties class attempts to call remove() in contextDestroyed but has design flaws. The ThreadLocal.remove() method only removes the variable copy in the current thread and cannot clean copies that may exist in other threads. More importantly, when the ThreadLocalProperties instance is set as a system property, its lifecycle extends to the JVM level rather than remaining at the web application level.
// Optimized ThreadLocalProperties implementation
public class ImprovedThreadLocalProperties extends Properties {
private final ThreadLocal<Properties> threadLocal = new ThreadLocal<>() {
@Override
protected Properties initialValue() {
return new Properties();
}
};
// Add method to clean all thread copies
public void cleanupAllThreads() {
// In practical applications, maintain references to all threads using this ThreadLocal
// and call threadLocal.remove() at appropriate times
}
@Override
public String getProperty(String key) {
Properties localProps = threadLocal.get();
String localValue = localProps.getProperty(key);
return (localValue != null) ? localValue : super.getProperty(key);
}
}
Solutions and Best Practices
For the MySQL driver thread issue, the most effective solution is moving the MySQL JDBC driver from WEB-INF/lib to Tomcat's lib directory. This approach ensures the driver is loaded only once, making the cleanup thread a true JVM-level singleton that doesn't prevent web application class loader unloading. If moving the driver file is impossible, consider explicitly closing the driver when the application stops:
@WebListener
public class DriverCleanupListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent sce) {
// Attempt to unload MySQL driver
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
if (driver.getClass().getName().contains("mysql")) {
try {
DriverManager.deregisterDriver(driver);
} catch (SQLException e) {
// Log the exception
}
}
}
}
}
For ThreadLocal management, the following patterns are recommended:
- Avoid using long-lived ThreadLocal variables in web applications
- If necessary, ensure all thread copies of ThreadLocal are cleaned in the ServletContextListener's contextDestroyed method
- Consider using InheritableThreadLocal subclasses for managing variable transfer between threads
- Exercise caution when modifying system properties to avoid elevating web application-level objects to JVM level
Graceful Thread Pool Shutdown
Beyond the specific issues mentioned, common thread management problems in web applications include improperly closed thread pools. Best practice involves explicitly closing all thread pools when the application stops:
@WebListener
public class ExecutorServiceManager implements ServletContextListener {
private ScheduledExecutorService scheduler;
@Override
public void contextInitialized(ServletContextEvent sce) {
scheduler = Executors.newScheduledThreadPool(5);
// Initialize other resources
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (scheduler != null) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(60, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
Conclusion and Recommendations
While Tomcat's memory leak warnings may sometimes appear alarming, they actually provide valuable diagnostic information. Understanding the principles behind these warnings is crucial for developing stable web applications. Key takeaways include: properly managing third-party library loading locations, implementing resource lifecycle management, using ThreadLocal variables cautiously, and ensuring all threads and thread pools shut down gracefully. By following these best practices, memory leak risks can be significantly reduced, improving application stability and maintainability.