Bug address: bugs.java.com/bugdatabase…

background

Since I was working on a problem with the ClassLoader using the CompletableFuture in the Tomcat container application, I had time to fill in some of the details of this problem!

Read the ForkJoinPool source code for this BUG

First go over the process, look at the default thread pool in Tomcat ForkJoinPool thread project is how to become a SafeForkJoinWorkerThreadFactory.

###1) First look at the source code for the place where ForkJoinPool sets ThreadFactory

private static ForkJoinPool makeCommonPool() { int parallelism = -1; ForkJoinWorkerThreadFactory factory = null; UncaughtExceptionHandler handler = null; Try {// Ignore exceptions in accessing/parsing properties 、、、、、、 ("java.util.concurrent.ForkJoinPool.common.threadFactory"); if (fp ! = null) factory = ((ForkJoinWorkerThreadFactory)ClassLoader. getSystemClassLoader().loadClass(fp).newInstance()); ,,,,,,,, omitting unimportant code} catch Exception (ignore) if (factory = = null) {} {the if (System. GetSecurityManager () = = null) factory = defaultForkJoinWorkerThreadFactory; else // use security-managed default factory = new InnocuousForkJoinWorkerThreadFactory(); } 、、、、、、 omit unimportant codeCopy the code

As you can see, if you can from java.util.concurrent.ForkJoinPool.com mon. ThreadFactory gets to the value, then use this value as the threadFactory, rather then an extension.

2) So where is this value set?

Set the place is org. Apache. Catalina. Core. JreMemoryLeakPreventionListener. This class implements LifecycleListener. There’s a line of code in there

                if (forkJoinCommonPoolProtection && !JreCompat.isJre9Available()) {
                    // Don't override any explicitly set property
                    if (System.getProperty(FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY) == null) {
                        System.setProperty(FORK_JOIN_POOL_THREAD_FACTORY_PROPERTY,
                                SafeForkJoinWorkerThreadFactory.class.getName());
                    }
                }
Copy the code

3) Why does Tomcat do this?

ForkJoinPool BUG in JDK7 and JDK8 Why JDK7 and JDK8? ForkJoinPool does not exist in previous versions of JDK7, but has been fixed since JDK9.

First of all, when using something like CompletableFuture, some of its runAsync methods, ForkJoinPool.commonPool() is used if the thread pool is not specified by default.

private static final Executor asyncPool = useCommonPool ?
        ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();

public static CompletableFuture<Void> runAsync(Runnable runnable) {
        return asyncRunStage(asyncPool, runnable);
    }

Copy the code

That is, throughout the JVM, when our code uses something like CompletableFuture or some parallelStream, a ForkJoinPool is used by default, and we share a ForkJoinPool. This might not seem to be a problem in our normal Java SE applications, and it is, but in a Java EE environment, for example, our common Java WEB applications run in Tomcat, which, in order to achieve the isolation of different applications, Create a ClassLoader for each WebApp application. Using this mechanism, different applications end up using the same ForkJoinPool to execute processing code.

Maybe a little confused at this point? Moving on to the bug, in JDK7 and JDK8, take a look at the implementation of ThreadFactory.

JDK8:

static final class DefaultForkJoinWorkerThreadFactory implements ForkJoinWorkerThreadFactory { public final ForkJoinWorkerThread newThread(ForkJoinPool pool) { return new ForkJoinWorkerThread(pool); }}Copy the code

When a factory creates a new ForkJoinWorkerThread(pool), it creates a new ForkJoinWorkerThread(pool). ForkJoinWorkerThread is created by using a ForkJoinWorkerThread.

    protected ForkJoinWorkerThread(ForkJoinPool pool) {
        // Use a placeholder until a useful name can be set in registerWorker
        super("aForkJoinWorkerThread");
        this.pool = pool;
        this.workQueue = pool.registerWorker(this);
    }
Copy the code

ForkJoinWorkerThread is an inherited Thread. The constructor calls super directly (” aJoinWorkerThread “). In this constructor, the contextClassLoader of the new thread inherits the contextClassLoader of the parent thread. This is the BUG. ContextClassLoader (contextClassLoader), contextClassLoader (contextClassLoader), contextClassLoader (contextClassLoader), contextClassLoader (contextClassLoader) However, the WebAppClassLoader is still held by a ForkJoinPool thread, so the GC cannot reclaim, and therefore cannot reclaim, some of the resources loaded in the application, causing a memory leak.

4) How are bugs fixed?

Take a look directly at the implementation in JDK9:

ForkJoinPool.java

private static final class DefaultForkJoinWorkerThreadFactory implements ForkJoinWorkerThreadFactory { private static final AccessControlContext ACC = contextWithPermissions( new RuntimePermission("getClassLoader"), new RuntimePermission("setContextClassLoader")); public final ForkJoinWorkerThread newThread(ForkJoinPool pool) { return AccessController.doPrivileged( new PrivilegedAction<>() { public ForkJoinWorkerThread run() { return new ForkJoinWorkerThread( pool, ClassLoader.getSystemClassLoader()); }}, ACC); }}Copy the code

Can see the different from 1.8 before, here in the new ForkJoinWorkerThread, directly introduced into this manually. GetSystemClassLoader () as contextClassLoader

Take a look at the version updates in Tomcat8 to see how Tomcat responds to this problem. For details, see tomcat.apache.org/tomcat-8.5-…

  • Tomcat 8.5.11 in provided in the listening class implementation Tomcat container lifecycle JreMemoryLeakPreventionListener fix the problem

  • Tomcat 8.5.30, because JDK9 fix the BUG, so increased the switch in the JreMemoryLeakPreventionListener judgment, if the current JVM support JDK9, Do not use SafeForkJoinWorkerThreadFactory.