Parallel Stream Class Loading Failure Analysis in Tomcat Containers
The article explains how Java 8 parallel streams in Tomcat containers trigger class‑loading failures because ForkJoinWorkerThreads inherit a null context class loader after Tomcat 7.0.74 introduced SafeForkJoinWorkerThreadFactory, leading to deserialization errors and memory‑leak risks, and recommends avoiding dynamic class loading or using custom thread pools.
With the increasing popularity of Java 8, more developers are using parallel streams to improve code execution efficiency. However, the author discovered that using parallel streams in Tomcat containers leads to dynamic class loading failures. By comparing the source code of multiple Tomcat versions and combining the principles of parallel streams and JVM class loading mechanisms, the problem source was successfully identified. This article analyzes the issue and provides solutions.
Problem Scenario: In a certain application, when the service starts, it uses parallel streams to call Dubbo. The code uses Lists.partition(ids, BATCH_QUERY_LIMIT).stream().parallel().map(Req::new).map(client::batchQuery).collect(Collectors.toList()); . The logs show numerous WARN messages: com.alibaba.com.caucho.hessian.io.SerializerFactory.getDeserializer Hessian/Burlap: 'XXXXXXX' is an unknown class in null and java.lang.ClassNotFoundException: XXXXXXX . When using the interface return value, an error is thrown: java.lang.ClassCastException: java.util.HashMap cannot be cast to XXXXXXX .
Root Cause Analysis: The Dubbo service return parameter entity class was not found, causing the Dubbo return data message to fail to convert to the corresponding entity during deserialization, resulting in a ClassCastException. By analyzing the thread stack and WARN logs, the problematic class was identified as com.alibaba.com.caucho.hessian.io.SerializerFactory . Since _loader is null, the class cannot be loaded.
The SerializerFactory constructor initializes _loader using the current thread's contextClassLoader: public SerializerFactory() { this(Thread.currentThread().getContextClassLoader()); } . The current thread is ForkJoinWorkerThread , which is the work thread in the Fork/Join framework (used by Java 8 parallel streams).
Tomcat Version Issue: Testing different Tomcat 7.0.x versions revealed that versions before 7.0.74 had no this issue, but versions after 7.0.74 had similar problems. Through source code comparison, it was found that Tomcat after version 7.0.74 added code that sets SafeForkJoinWorkerThreadFactory as the ForkJoinWorkerThread factory in Java 8 environment, and sets the thread's contextClassLoader to ForkJoinPool.class.getClassLoader() . Since ForkJoinPool belongs to the rt.jar package loaded by Bootstrap ClassLoader, the corresponding class loader is null.
Memory Leak Analysis: Tomcat fixed a memory leak issue caused by ForkJoinPool in this version (Bug 60620). If a thread holds a reference to ClassLoaderA, and after the application needs to unload ClassLoaderA and its loaded classes, thread A still holds a reference to ClassLoaderA, this causes a memory leak due to bidirectional references between the class loader and its loaded classes. Tomcat uses SafeForkJoinWorkerThreadFactory to avoid class loader memory leaks caused by parallel streams in Tomcat applications.
Summary: During development, if parallel streams are used in compute-intensive tasks, avoid dynamically loading classes in subtasks. For other business scenarios, use thread pools instead of parallel streams. In summary, we need to avoid dynamic loading of custom classes or third-party classes through parallel streams in Tomcat applications.
vivo Internet Technology
Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.