如何识别并删除 Java 中从我们的 webapp 发起的 Threads/ThreadLocals?

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

每当我停止或重新部署 webapp 时,我都会看到很多类似于

的错误
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

我没有在我的应用程序中创建任何 ThreadLocal,而是引用了许多可能正在创建这些 ThreadLocal 的库。我们目前正在使用 Tomcat 7。我已经回答过其他类似的问题 [Memory leak when redeploying application in Tomcat or What are these warnings in catalina.out?] ,但所有这些问题都只是建议这是 Tomcat 功能,以警告您 ThreadLocals 未被删除。我没有看到任何删除 ThreadLocals 的答案。我也看到一些关于线程未停止的错误,

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.

这些在我们公司的中央日志系统中被记录为错误,从而增加了我们应用程序的错误计数。当我们检查应用程序的性能时,这肯定看起来不太好。我尝试了这两个来源的实现 [Killing threads and Sample code from this thread],但似乎不起作用。它删除了 thread/threadlocals 不是由我们的应用程序创建的。 我需要的是仅删除由我们的 webapp 启动的 threads/threadlocals。有什么办法可以在 ServletContextListener 的 contextDestroyed() 方法中删除这些? 以下是我当前的 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

}

}

更新:我仍然无法解决这个问题。有什么帮助吗?没有人 运行 进入这些?

没有一次性修复所有线程局部泄漏的解决方案。 通常使用 Threadlocal 变量的第三方库有某种清理 API 调用,可用于清除其本地线程变量。

您必须检查所有报告的线程局部泄漏,并找到在相应库中处理它们的正确方法。您可以在 CustomServletContextListener

中执行此操作

示例:

log4j (javadoc):

LogManager.shutdown()

jdbc driver: (javadoc):

DriverManager.deregisterDriver(driver);

注意:还要检查您的第 3 方库的新版本,以检查有关内存泄漏(and/or 线程本地泄漏)的 fox 修复。

解决方案取决于创建这些 Threads/ThreadLocal-s 的库。 基本上你需要从你的 CustomServletContextListener.contextDestroyed() 方法调用库的清理代码。

所以找到什么是库以及如何正确关闭它。

您可以尝试使用此代码删除所有 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);
    }
}

如果我们在停止容器时从所有线程的线程本地删除对象,那么我们只是在尝试解决容器显示错误消息的问题。相反,objective 应该是为了避免在容器未 stopped/restarted 的时间段内可能发生的内存泄漏。因此,理想情况下,由于 ThreadPool 中的线程被重新用于服务不同的请求,然后在发送响应后,不应该有任何理由通过将对象保留在本地线程中来保留内存,因为该线程可能用于服务来自客户端的下一个(完全不同的)请求.其中一项建议是通过配置在从服务器发送响应之前立即执行的过滤器来从线程本地删除任何对象。

我会尝试找出是哪个库导致了这些 ThreadLocal,可能是通过 运行 调试器中的 Web 应用程序并在 ThreadLocal 创建时停止。然后您可以查看您是否忘记清理某些库的后面,或者库是否 buggy/not 为网络应用程序使用而制作。也许 post 你的发现在这里。

在上下文监听器中清理线程时,我曾经检查过线程的contextClassLoader是否与监听器线程运行相同,以避免弄乱其他应用程序的线程。

希望对您有所帮助。

尝试

Runtime.getRuntime().addShutdownHook(webapp.new ShutdownHook());

在你的 shudownhook 中,清理对象

如果您在代码中使用 ThreadLocal,您可以将 ThreadLocal 替换为我创建的 ImrpovedThreadLocal,这样您就不会在 stop/redeploy 上发生内存泄漏。您可以以相同的方式使用该 threadLocal 而不会发生任何线程争用。