From ea197f483ede1a9216e93a500c0ef01aef0d91f3 Mon Sep 17 00:00:00 2001 From: Chris Dennis Date: Mon, 14 Jul 2025 11:06:09 -0400 Subject: [PATCH] 8362123: ClassLoader Leak via Executors.newSingleThreadExecutor(...) Executors shutdown via `shutdownNow()` should have their cleanables cleaned to prevent a classloader leak. This can happen if a classloader exists that both references the wrapped executor and is referenced by the delegate executor. --- .../java/util/concurrent/Executors.java | 7 +++ .../concurrent/Executors/AutoShutdown.java | 48 ++++++++++++++++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/java/util/concurrent/Executors.java b/src/java.base/share/classes/java/util/concurrent/Executors.java index ef3d634801090..8b339cf83ef3d 100644 --- a/src/java.base/share/classes/java/util/concurrent/Executors.java +++ b/src/java.base/share/classes/java/util/concurrent/Executors.java @@ -755,6 +755,13 @@ public void shutdown() { super.shutdown(); cleanable.clean(); // unregisters the cleanable } + + @Override + public List shutdownNow() { + List unexecuted = super.shutdownNow(); + cleanable.clean(); // unregisters the cleanable + return unexecuted; + } } /** diff --git a/test/jdk/java/util/concurrent/Executors/AutoShutdown.java b/test/jdk/java/util/concurrent/Executors/AutoShutdown.java index 2b7fa7b883fa5..9ef73128a2c92 100644 --- a/test/jdk/java/util/concurrent/Executors/AutoShutdown.java +++ b/test/jdk/java/util/concurrent/Executors/AutoShutdown.java @@ -23,23 +23,32 @@ /* * @test - * @bug 6399443 8302899 + * @bug 6399443 8302899 8362123 * @summary Test that Executors.newSingleThreadExecutor wraps an ExecutorService that * automatically shuts down and terminates when the wrapper is GC'ed + * @library /test/lib/ * @modules java.base/java.util.concurrent:+open * @run junit AutoShutdown */ +import java.lang.ref.PhantomReference; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; import java.lang.reflect.Field; import java.time.Duration; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.IntStream; +import jdk.test.lib.Utils; +import jdk.test.lib.util.ForceGC; + import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -61,6 +70,10 @@ private static Stream executorAndQueuedTaskCounts() { .mapToObj(i -> Arguments.of(s, i))); } + private static Stream shutdownMethods() { + return Stream.>of(e -> e.shutdown(), e -> e.shutdownNow()).map(Arguments::of); + } + /** * SingleThreadExecutor with no worker threads. */ @@ -110,6 +123,21 @@ void testActiveWorker(Supplier supplier,int queuedTaskCount) th assertEquals(ntasks, completedTaskCount.get()); } + @ParameterizedTest + @MethodSource("shutdownMethods") + void testShutdownUnlinksCleaner(Consumer shutdown) throws Exception { + ClassLoader classLoader = Utils.getTestClassPathURLClassLoader(ClassLoader.getPlatformClassLoader()); + + ReferenceQueue queue = new ReferenceQueue<>(); + Reference reference = new PhantomReference(classLoader, queue); + + classLoader.loadClass("AutoShutdown$IsolatedClass").getDeclaredMethod("shutdown", Consumer.class).invoke(null, shutdown); + classLoader = null; + + assertTrue(ForceGC.wait(() -> queue.poll() != null)); + Reference.reachabilityFence(reference); + } + /** * Returns the delegate for the given ExecutorService. The given ExecutorService * must be a Executors$DelegatedExecutorService. @@ -132,5 +160,21 @@ private void gcAndAwaitTermination(ExecutorService executor) throws Exception { terminated = executor.awaitTermination(100, TimeUnit.MILLISECONDS); } } -} + public static class IsolatedClass { + + private static final ExecutorService executor = Executors.newSingleThreadExecutor(new IsolatedThreadFactory()); + + public static void shutdown(Consumer shutdown) { + shutdown.accept(executor); + } + } + + public static class IsolatedThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(Runnable r) { + return new Thread(r); + } + } +}