美文网首页
震惊!JDK7和JDK8中关于ForkJoinPool的内存泄漏

震惊!JDK7和JDK8中关于ForkJoinPool的内存泄漏

作者: 我有一只喵喵 | 来源:发表于2021-03-06 15:33 被阅读0次

    Bug地址:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8172726

    背景

    由于当时在解决了一个问题:Tomcat容器应用中使用CompletableFuture时,关于ClassLoader引起的问题,之后,后来有时间对此此问题中的一些细节进行一个补充!

    透过ForkJoinPool源码了解这个BUG

    首先走一遍过程,来看下在Tomcat中ForkJoinPool的默认线程池的线程工程是怎么变为SafeForkJoinWorkerThreadFactory的。

    1)首先查看ForkJoinPool设置ThreadFactory的地方源码

    private static ForkJoinPool makeCommonPool() {
            int parallelism = -1;
            ForkJoinWorkerThreadFactory factory = null;
            UncaughtExceptionHandler handler = null;
            try {  // ignore exceptions in accessing/parsing properties
       、、、、、、省略不重要代码
                String fp = System.getProperty
                    ("java.util.concurrent.ForkJoinPool.common.threadFactory");
                if (fp != null)
                    factory = ((ForkJoinWorkerThreadFactory)ClassLoader.
                               getSystemClassLoader().loadClass(fp).newInstance());
       、、、、、、省略不重要代码
            } catch (Exception ignore) {
            }
            if (factory == null) {
                if (System.getSecurityManager() == null)
                    factory = defaultForkJoinWorkerThreadFactory;
                else // use security-managed default
                    factory = new InnocuousForkJoinWorkerThreadFactory();
            }
              、、、、、、省略不重要代码
    

    可以看到,如果可以从java.util.concurrent.ForkJoinPool.common.threadFactory中获取到值,那么就使用这个值作为ThreadFactory,相当于是一个扩展。

    2)那么这个值是在哪里设置进去的呢?

    设置的地方就是org.apache.catalina.core.JreMemoryLeakPreventionListener。这个类实现了LifecycleListener。其中有一行代码

                    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());
                        }
                    }
    

    3)为什么Tomcat要做这种操作?

    到这里就引出了文章标题中所说的JDK7和JDK8中关于ForkJoinPool的BUG!为啥是JDK7和JDK8?因为ForkJoinPool是JDK7开始存在,那么之前的版本自然没有,而JDK9之后针对此BUG进行了修复。

    先说下这个BUG:首先当我们使用诸如CompletableFuture时,使用它的一些runAsync之类方式时,如果我们不默认指定线程池,则会使用ForkJoinPool.commonPool()。

    private static final Executor asyncPool = useCommonPool ?
            ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
    
    public static CompletableFuture<Void> runAsync(Runnable runnable) {
            return asyncRunStage(asyncPool, runnable);
        }
    
    

    也就是说在整个JVM中,当我们的代码使用了像CompletableFuture或者一些parallelStream等时,会默认使用ForkJoinPool,且共用一个ForkJoinPool,这看起来在我们普通的Java SE程序中好像不会有什么问题,也确实不会有问题,但是在JAVA EE环境下就不同了,比如我们常见的JAVA WEB程序会放在Tomcat中运行,而Tomcat为了达到不同应用的隔离,其实是会为WebApp下每一个应用创建一个专属ClassLoader加载执行。那么基于上述机制,不同的应用中最终会使用同一个ForkJoinPool去执行处理代码。

    可能此时还有点懵逼?接着往下看bug所在,在JDK7和JDK8中,看下其ThreadFactory的实现。

    JDK8中:

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

    可以看到是搞了一个静态内部类,这里工厂产生线程时,直接new ForkJoinWorkerThread(pool);再来看下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);
        }
    

    ForkJoinWorkerThread是继承了Thread,这构造方法中直接调用 super("aForkJoinWorkerThread");而这个构造方法中,新线程的contextClassLoader会继承父线程的contextClassLoader,这里就是BUG所在,为什么这么说,在Tomcat应用中,父线程的contextClassLoader自然就是WebAppClassLoader,WebApp下每个应用都有一个,那么如果在产生的新线程中使用contextClassLoader去加载一些类使用,后来这个应用可能要卸载,但是其拽你书WebAppClassLoader依然被ForkJoinPool中的线程所持有,所以GC无法回收,进而也无法回收这个应用中加载的一些资源,从而造成内存泄漏。

    4)BUG是如何修复的?

    直接看下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);
            }
        }
    

    可以看到与1.8之前不同,这里在新建ForkJoinWorkerThread时,直接手动传入了ClassLoader.getSystemClassLoader()作为contextClassLoader

    再来从Tomcat8中的版本更新中看下Tomcat针对此问题的应对措施,具体见http://tomcat.apache.org/tomcat-8.5-doc/changelog.html

    • Tomcat 8.5.11 在中提供Tomcat容器生命周期监听类的实现JreMemoryLeakPreventionListener修复此问题

    • Tomcat 8.5.30 中,由于JDK9中修复了此BUG,所以在JreMemoryLeakPreventionListener中增加了开关判断,如果当前JVM支持JDK9,则不使用SafeForkJoinWorkerThreadFactory。

    相关文章

      网友评论

          本文标题:震惊!JDK7和JDK8中关于ForkJoinPool的内存泄漏

          本文链接:https://www.haomeiwen.com/subject/kvajqltx.html