Skip to content

Static ExecutorService in Async.java causes ClassLoader leaks in Web Containers #2244

@QiuYucheng2003

Description

@QiuYucheng2003

Bug: Memory Leak in Web Containers due to Static ExecutorService

The org.web3j.utils.Async class utilizes a static instance of ExecutorService which relies solely on a JVM Shutdown Hook for termination. This design prevents the threads from stopping when a Web Application is undeployed, causing ClassLoader leaks.

Steps To Reproduce

  1. Create a Java Web Application (WAR) that depends on the Web3j library.
  2. Deploy the application to a Servlet Container (e.g., Apache Tomcat, Jetty).
  3. Execute code that triggers the initialization of org.web3j.utils.Async (this starts the static cached thread pool).
  4. Reload or Undeploy the web application via the container's management interface (without restarting the JVM).
  5. Take a Heap Dump and analyze it (e.g., using Eclipse MAT).

Expected behavior

The ExecutorService threads should terminate (or provide a mechanism to be terminated) when the application context is destroyed, releasing references to the WebAppClassLoader and allowing it to be garbage collected.

Actual behavior

The threads created by the static executor remain alive because the JVM Shutdown Hook (Runtime.getRuntime().addShutdownHook) is not triggered during application redeployment (only during JVM termination).

These "Not Terminated" threads hold strong references to the WebAppClassLoader, preventing it from being garbage collected. Repeated redeployments lead to java.lang.OutOfMemoryError: Metaspace (or PermGen).

Environment

Describe the environment in which the issue occurs

  • Web3j version: [填入你的 Web3j 版本,例如 4.8.7]
  • Java or Android version: [填入你的 Java 版本,例如 Java 8 / 11]
  • Operating System: [填入你的操作系统,例如 Windows / Linux]
  • Container: [填入你的容器,例如 Apache Tomcat 9.0]

Additional context

Root Cause Analysis:
The issue lies in org.web3j.utils.Async:

private static final ExecutorService executor = Executors.newCachedThreadPool();

static {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown(executor)));
}

Since the executor is private and static, there is no way for a developer to manually shut it down using a ServletContextListener.

Suggested Fix: 
Please provide a public static method (e.g., Async.shutdown()) to allow manual resource cleanup, or avoid using a static thread pool that assumes it owns the JVM lifecycle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugA bug in behaviour or functionality

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions