Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to identify and remove Threads/ThreadLocals initiated from our webapp in Java?

Whenever I stop or redeploy the webapp, I see lot of errors similar to,

msg=The web application [] created a ThreadLocal with key of type [] (value []) and 
a value of type [] (value []) but failed to remove it when the web application was 
stopped. Threads are going to be renewed over time to try and avoid probable memory leak

I'm not creating any ThreadLocals in my app but referencing many libraries which may be creating these ThreadLocals. We are currently using Tomcat 7. I've already gone through other similar questions [Memory leak when redeploying application in Tomcat or What are these warnings in catalina.out?] but all of them only suggest that this is Tomcat feature to warn you about ThreadLocals not being removed. I don't see any answer to remove ThreadLocals. I also see few ERRORs regarding thread not stopped as well,

msg=The web application [] appears to have started a thread named [] but has
failed to stop it. This is very likely to create a memory leak.

These are being logged in as ERRORs in our company's central logging system and thereby increasing the error count by our application. This certainly does not look good when we check the performance of our app. I tried the implementations from these two sources [Killing threads and Sample code from this thread], but doesn't seem to work. It removes thread/threadlocals not created by our app. What I need is to remove only the threads/threadlocals started by our webapp. Is there any way we can remove these in contextDestroyed() method of ServletContextListener? Following is my current ServletContextListener class,

public class CustomServletContextListener implements ServletContextListener {

private List<String> threadsAtStartup;

@Override
public void contextInitialized(ServletContextEvent sce) {
    retrieveThreadsOnStartup();
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
    // Now deregister JDBC drivers in this context's ClassLoader:
    // Get the webapp's ClassLoader
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    // Loop through all drivers
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        if (driver.getClass().getClassLoader() == cl) {
            // This driver was registered by the webapp's ClassLoader, so deregister it:
            try {
                System.out.println("Deregistering JDBC driver {}: " + driver);
                DriverManager.deregisterDriver(driver);
            } catch (SQLException ex) {
                System.out.println("Error deregistering JDBC driver {}: " + driver + "\nException: " + ex);
            }
        } else {
            // driver was not registered by the webapp's ClassLoader and may be in use elsewhere
            System.out.println("Not deregistering JDBC driver {} as it does not belong to this webapp's ClassLoader: " + driver);
        }
    }

    //Threads
    ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
    threadGroup = Thread.currentThread().getThreadGroup();
    Thread[] threads;
    try {
        threads = retrieveCurrentActiveThreads(threadGroup);
    } catch (NoSuchFieldException e) {
        System.out.println("Could not retrieve initial Threads list. The application may be unstable on shutting down " + e.getMessage());
        return;
    } catch (IllegalAccessException e) {
        System.out.println("Could not retrieve initial Threads list. The application may be unstable on shutting down " + e.getMessage());
        return;
    }

    int toBeKilledCount = 0;
    int totalThreadCount = 0;
    int killedTLCount = 0;
    int totalTLCount = 0;
    int killedITLCount = 0;
    int totalITLCount = 0;
    for (; totalThreadCount < threads.length; totalThreadCount++) {
        Thread thread = threads[totalThreadCount];
        if(thread != null) {
            String threadName = thread.getName();
            boolean shouldThisThreadBeKilled;

            shouldThisThreadBeKilled = isThisThreadToBeKilled(Thread.currentThread(), thread);
            if (shouldThisThreadBeKilled) {
                //ThreadLocal
                try {
                    removeThreadLocals("threadLocals", thread);
                    removeThreadLocals("inheritableThreadLocals", thread);
                } catch (Exception e) {
                    System.out.println("\tError accessing threadLocals field of '" + threadName + "': " + e.getMessage());
                }

                //Stop thread
                thread.interrupt();
                thread = null;
                toBeKilledCount++;
            }

        }
    }
}

private void retrieveThreadsOnStartup() {
    final Thread[] threads;
    final ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();
    try {
        threads = retrieveCurrentActiveThreads(threadGroup);
    } catch (NoSuchFieldException e) {
        System.out.println("Could not retrieve initial Threads list. The application may be unstable on shutting down " + e);
        threadsAtStartup = new ArrayList<String>();
        return;
    } catch (IllegalAccessException e) {
        System.out.println("Could not retrieve initial Threads list. The application may be unstable on shutting down " + e);
        threadsAtStartup = new ArrayList<String>();
        return;
    }

    threadsAtStartup = new ArrayList<String>(threads.length);
    for (int i = 0; i < threads.length; i++) {
        final Thread thread;
        try {
            thread = threads[i];
            if (null != thread) {
                threadsAtStartup.add(thread.getName());
            }
        } catch (RuntimeException e) {
            System.out.println("An error occured on initial Thread statement: " + e);
        }
    }
}

private Thread[] retrieveCurrentActiveThreads(ThreadGroup threadGroup) throws NoSuchFieldException, IllegalAccessException {
    final Thread[] threads;
    final Field privateThreadsField;
    privateThreadsField = ThreadGroup.class.getDeclaredField("childrenThreads");
    privateThreadsField.setAccessible(true);

    threads = (Thread[]) privateThreadsField.get(threadGroup);
    return threads;
}

private void removeThreadLocals(String fieldName, Thread thread) {
    Field threadLocalsField = Thread.class.getDeclaredField(fieldName);
    threadLocalsField.setAccessible(true);
    Object threadLocalMap = threadLocalsField.get(thread);
    Field tableField = threadLocalMap.getClass().getDeclaredField("table");
    tableField.setAccessible(true);
    Object table = tableField.get(threadLocalMap);
    int count = 0;
    for (int i = 0, length = Array.getLength(table); i < length; ++i) {
        Object entry = Array.get(table, i);
        if (entry != null) {
            totalTLCount++;
            Object threadLocal = ((WeakReference)entry).get();
            if (threadLocal != null) {
                Array.set(table, i, null);
                killedTLCount++;
            }
        } 
    }   
}

private Boolean isThisThreadToBeKilled(Thread currentThread, Thread testThread) {
    boolean toBeKilled;
    String currentThreadName = currentThread.getName();
    String testThreadName = testThread.getName();
    System.out.println("currentThreadName: " + currentThreadName + ", testThreadName: " + testThreadName);
    return !threadsAtStartup.contains(testThreadName)               // this thread was not already running at startup
            && !testThreadName.equalsIgnoreCase(currentThreadName); // this is not the currently running thread

}

}

Update: I'm still not able to resolve this. Any help? Nobody ever ran into these?

like image 367
AndyT Avatar asked Mar 26 '15 00:03

AndyT


3 Answers

There is no solution to fix all threadlocal leaks in one go. Normally third party libraries using Threadlocal variables have some kind of cleanup API call which can be used to clear their local thread variables.

You have to check for all reported threadlocal leaks and find the correct way of disposing them in the corresponding library. You can do this in your CustomServletContextListener

examples:

log4j (javadoc):

LogManager.shutdown()

jdbc driver: (javadoc):

DriverManager.deregisterDriver(driver);

note: Also check for new versions of your 3rd party libs to check fox fixes concerning memory leaks (and/or thread local leaks).

like image 172
R. Oosterholt Avatar answered Oct 29 '22 20:10

R. Oosterholt


Solution depends on the library that created these Threads/ThreadLocal-s. Basically you need to call library's cleanup code from your CustomServletContextListener.contextDestroyed() method.

So find what is the library and how to shut it down properly.

like image 30
Alexey Shurygin Avatar answered Oct 29 '22 18:10

Alexey Shurygin


You can try this code to remove all ThreadLocal

private void cleanThreadLocals() {
    try {
        // Get a reference to the thread locals table of the current thread
        Thread thread = Thread.currentThread();
        Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
        threadLocalsField.setAccessible(true);
        Object threadLocalTable = threadLocalsField.get(thread);

        // Get a reference to the array holding the thread local variables inside the
        // ThreadLocalMap of the current thread
        Class threadLocalMapClass = Class.forName("java.lang.ThreadLocal$ThreadLocalMap");
        Field tableField = threadLocalMapClass.getDeclaredField("table");
        tableField.setAccessible(true);
        Object table = tableField.get(threadLocalTable);

        // The key to the ThreadLocalMap is a WeakReference object. The referent field of this object
        // is a reference to the actual ThreadLocal variable
        Field referentField = Reference.class.getDeclaredField("referent");
        referentField.setAccessible(true);

        for (int i=0; i < Array.getLength(table); i++) {
            // Each entry in the table array of ThreadLocalMap is an Entry object
            // representing the thread local reference and its value
            Object entry = Array.get(table, i);
            if (entry != null) {
                // Get a reference to the thread local object and remove it from the table
                ThreadLocal threadLocal = (ThreadLocal)referentField.get(entry);
                threadLocal.remove();
            }
        }
    } catch(Exception e) {
        // We will tolerate an exception here and just log it
        throw new IllegalStateException(e);
    }
}
like image 37
namthienmenh Avatar answered Oct 29 '22 18:10

namthienmenh