首页 > 其他分享 >Tomcat 应用中并行流带来的类加载问题

Tomcat 应用中并行流带来的类加载问题

时间:2023-04-04 13:42:29浏览次数:40  
标签:Tomcat ForkJoinPool 并行 loader 线程 contextClassLoader 加载


vivo互联网技术 微信公众号 
作者:肖铭轩、王道环

随着 Java8 的不断流行,越来越多的开发人员使用并行流(parallel)这一特性提升代码执行效率。但是,作者发现在 Tomcat 容器中使用并行流会出现动态加载类失败的情况,通过对比 Tomcat 多个版本的源码,结合并行流和 JVM 类加载机制的原理,成功定位到问题来源。本文对这个问题展开分析,并给出解决方案。

一、问题场景

在某应用中,服务启动时会通过并行流调用 Dubbo,调用代码如下:

Lists.partition(ids, BATCH_QUERY_LIMIT).stream()
     .parallel()
     .map(Req::new)
     .map(client::batchQuery)
     .collect(Collectors.toList());

调用日志中发现大量的 WARN 日志com.alibaba.com.caucho.hessian.io.SerializerFactory.getDeserializer Hessian/Burlap:‘XXXXXXX’ is an unknown class in null:java.lang.ClassNotFoundException: XXXXXXX,在使用接口返回结果的时候抛出错误 java.lang.ClassCastException: java.util.HashMap cannot be cast to XXXXXXX。

二、原因分析

1、初步定位

首先根据错误日志可以看到,由于依赖的 Dubbo 服务返回参数的实体类没有找到,导致 Dubbo 返回的数据报文在反序列化时无法转换成对应的实体,类型强制转化中报了java.lang.ClassCastException。通过对线程堆栈和WARN日志定位到出现问题的类为com.alibaba.com.caucho.hessian.io.SerializerFactory,由于 _loader 为 null 所以无法对类进行加载,相关代码如下:

try {
       Class cl = Class.forName(type, false, _loader);
       deserializer = getDeserializer(cl);
   } catch (Exception e) {
       log.warning("Hessian/Burlap: '" + type + "' is an unknown class in " + _loader + ":\n" + e);
    log.log(Level.FINER, e.toString(), e);
   }

接下来继续向上定位为什么 _loader 会为 nullSerializerFactory 构造方法中对 _loader 进行了初始化,初始化代码如下,可以看出 _loader 使用的是当前线程的 contextClassLoader。

public SerializerFactory() {
    this(Thread.currentThread().getContextClassLoader());
}
 
public SerializerFactory(ClassLoader loader) {
    _loader = loader;
}

根据堆栈看到当前线程为ForkJoinWorkerThread,ForkJoinWorkerThread是Fork/Join框架内的工作线程(Java8 并行流使用的就是Fork/Join)。JDK文档指出:

The context ClassLoader is provided by the creator of the thread for use by code running in this thread when loading classes and resources. If not set, the default is the ClassLoader context of the parent Thread.

因此当前的线程contextClassLoader应该和创建此线程的父线程保持一致才对,不应该是null啊?

继续看ForkJoinWorkerThread创建的源码,首先使用ForkJoinWorkerThreadFactory创建一个线程,然后将创建的线程注册到ForkJoinPool中,线程初始化的逻辑和普通线程并无差别,发现单独从JDK自身难以发现问题,因此将分析转移到Tomcat中。

2、Tomcat升级带来的问题

取 Tomcat7.0.x 的一些版本做了实验和对比,发现7.0.74之前的版本无此问题,但7.0.74之后的版本出现了类似问题,实验结果如下表。

Tomcat 应用中并行流带来的类加载问题_类加载

Tomcat 应用中并行流带来的类加载问题_Tomcat_02

至此已经将问题定位到了是Tomcat的版本所致,通过源代码比对,发现7.0.74版本之后的Tomcat中多了这样的代码:

if (forkJoinCommonPoolProtection && IS_JAVA_8_OR_LATER) {
    // 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,
                "org.apache.catalina.startup.SafeForkJoinWorkerThreadFactory");
    }
}
private static class SafeForkJoinWorkerThread extends ForkJoinWorkerThread {
 
   protected SafeForkJoinWorkerThread(ForkJoinPool pool) {
       super(pool);
       setContextClassLoader(ForkJoinPool.class.getClassLoader());
   }
}

 

在 Java8 环境下,7.0.74 版本之后的 Tomcat 会默认将 SafeForkJoinWorkerThreadFactory 作为 ForkJoinWorkerThread 的创建工厂,同时将该线程的 contextClassLoader 设置为ForkJoinPool.class.getClassLoader(),ForkJoinPool 是属于rt.jar包的类,由BootStrap ClassLoader加载,所以对应的类加载器为null。至此,_loader为空的问题已经清楚,但是Tomcat为什么要多此一举,将null作为这个 ForkJoinWorkerThread的contextClassLoader呢?

继续对比Tomcat的changeLog http://tomcat.apache.org/tomcat-7.0-doc/changelog.html 发现Tomcat在此版本修复了由ForkJoinPool引发的内存泄露问题 Bug 60620 - [JRE] Memory leak found in java.util.concurrent.ForkJoinPool,为什么线程的contextClassLoader会引起内存泄露呢?

3、contextClassLoader内存泄露之谜

在JDK1.2以后,类加载器的双亲委派模型被广泛引入。它的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把整个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载,流程如下图。

Tomcat 应用中并行流带来的类加载问题_类加载_03

Tomcat 应用中并行流带来的类加载问题_类加载_04

 

然而双亲委派的模型并不能保证应用程序加载类的过程,一个典型的例子就是JNDI服务,这些接口定义在rt.jar并由第三方提供实现,Bootstrap ClassLoader显然不认识这些代码。为了解决这个问题,JDK1.2同时引入了线程上下文类加载器(Thread Context ClassLoader)进行类加载,作为双亲委派模型的补充。

回到内存泄漏的问题上,设想一个场景,如果某个线程持有了ClassLoaderA(由ClassLoaderA加载了若干类),当应用程序需要对ClassLoaderA以及由ClassLoaderA加载出来的类卸载完成后,线程A仍然持有了ClassLoaderA的引用,然而业务方以为这些类以及加载器已经卸载干净,由于类加载器和其加载出的类双向引用,这就造成了类加载器和其加载出来的类无法垃圾回收,造成内存泄露。在并行流中,ForkJoinPool和ForkJoinWorkerThreadFactory默认是静态且共享的(JDK官方推荐,创建线程本身是相对重的操作,尽量避免重复创建ForkJoinWorkerThread 造成资源浪费),下图描绘了发生内存泄露的场景:

Tomcat 应用中并行流带来的类加载问题_parallel_05

因此 Tomcat 默认使用SafeForkJoinWorkerThreadFactory作为ForkJoinWorkerThreadFactory,并将该工厂创建的ForkJoinWorkerThread的contextClassLoader都指定为ForkJoinPool.class.getClassLoader(),而不是JDK默认的继承父线程的contextClassLoader,进而避免了Tomcat应用中由并行流带来的类加载器内存泄露。

三、总结

在开发过程中,如果在计算密集型任务中使用了并行流,请避免在子任务中动态加载类;其他业务场景请尽量使用线程池,而非并行流。总之,我们需要避免在Tomcat应用中通过并行流进行自定义类或者第三方类的动态加载。

 vivo 互联网技术 微信公众号

Tomcat 应用中并行流带来的类加载问题_加载_06


标签:Tomcat,ForkJoinPool,并行,loader,线程,contextClassLoader,加载
From: https://blog.51cto.com/u_14291117/6168586

相关文章

  • Tomcat 优雅关闭之路
    vivo互联网技术微信公众号 作者:马运杰本文通过阅读Tomcat启动和关闭流程的源码,深入分析不同的Tomcat关闭方式背后的原理,让开发人员能够了解在使用不同的关闭方式时需要注意的点,避免因JVM进程异常退出导致的各种非预见性错误。一、Tomcat的启动过程要了解Tomcat关闭的原理,首先......
  • Tomcat 9.0.26 高并发场景下DeadLock问题排查与修复
    vivo互联网技术微信公众号 作者:黄卫兵、陈锦霞一、Tomcat容器9.0.26版本Deadlock问题1.1问题现象1.1.1 发生Deadlock的背景某接口/get.do压测,3分钟后,成功事务数TPS由1W骤降至0。1.1.2 Tomcat服务器出现大量的CLOSE_WAIT被压测服务器,出现TCPCLOSE_WAIT状态个数在200~......
  • Java使用IntelliJ IDEA创建JavaWeb应用程序并配置Tomcat
    1、创建JavaWeb程序创建一个JavaEE项目,选择Web应用程序。创建好后结果如下2、添加Tomcat服务器首先在官网下载Tomcat对应的操作系统版本。https://tomcat.apache.org/右上角添加Tomcat服务器在服务器里选择本地的Tomcat地址,其他的默认。在部署里点击加号,选择Artifact工件,增加web......
  • 延迟加载(Lazy Loading)在C#中的应用
    延迟加载是一种常见的编程技术,它可以在需要时才加载数据或资源,而不是在程序启动时就加载。这种技术可以提高程序的性能和响应速度,因为它可以避免不必要的资源浪费。在C#中,延迟加载可以通过使用Lazy<T>类来实现。这个类是在.NETFramework4中引入的,它提供了一种简单的方法来实现......
  • css学习:加载页面出现选择哪个文件观看或下载
         观察发现是文件写错了,html前没有“.” ......
  • Tomcat 与 Nginx,Apache的区别
       Apache指的应该是Apache软件基金会下的一个项目——ApacheHTTPServerProject   Nginx同样也是一款开源的HTTP服务器软件(当然它也可以作为邮件代理服务器、通用的TCP代理服务器)。   Tomcat是Apache基金会下的另外一个项目,与ApacheHTTPServer相比,Tomcat能够......
  • PA 懒加载(循环引用,N+1,使用关联对象)(二)
    这次具体讲述一下,对于懒加载遇到(循环引用,N+1,使用关联对象)的解决方案。为了方便大家模拟操作,我会完整说一下不想看过程的,直接看总结。 一建表创建School和UserSchoolSETNAMESutf8mb4;SETFOREIGN_KEY_CHECKS=0;--------------------------------Tablestructu......
  • 加载更多 - 监听div的滚动scroll
    前言:某些情况下,在展示列表数据时,为了实现性能优化及用户更好的体验,可以先展示十几条数据,然后边滑动边加载更多,可以减少服务器压力及页面渲染时间。varpageNum=1;//页数vardomHeight=$(".listBox").height()*4;vardom=document.getElementById('list');dom.addEventList......
  • 加载spring配置的两个方法AnnotationConfigApplicationContext()和getRootConfigClass
    在Spring中,AnnotationConfigApplicationContext类和AbstractAnnotationConfigDispatcherServletInitializer类中的getRootConfigClasses()方法都是用来加载Spring配置类,并创建Spring容器的。因此,它们的作用是相似的,都是用来配置Spring容器的。但是,它们的使用场景和......
  • vue中使用nprogress优化导航条和请求数据的时候进行加载中显示
    以下内容仅供学习使用先进行安装npminstall--savenprogress或者yarnaddnprogress在你封装的request.js中使用importNProgressfrom'nprogress'import'nprogress/nprogress.css'在axios.interceptors.request.use(config=>{},请求开始的时候使用NProgres......