You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2023/12/18 20:07:54 UTC

(logging-log4j2) branch main updated: Move JCTools-based queues to a new `log4j-jctools` module (#2104)

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/main by this push:
     new 2e4e5e5102 Move JCTools-based queues to a new `log4j-jctools` module (#2104)
2e4e5e5102 is described below

commit 2e4e5e510243cab0d5ae56a26916df8000a2f575
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Mon Dec 18 21:07:42 2023 +0100

    Move JCTools-based queues to a new `log4j-jctools` module (#2104)
---
 .../log4j/internal/RecyclerFactoriesTest.java      | 102 ------
 .../log4j/internal/StringParameterParserTest.java  | 373 ---------------------
 .../log4j/internal/recycler/ArrayQueueTest.java    | 101 ++++++
 .../recycler/RecyclerFactoryRegistryTest.java      |  85 +++++
 .../internal/recycler/RecyclerFactoryTestUtil.java |  46 +++
 .../ThreadLocalRecyclerFactoryProviderTest.java}   |  46 ++-
 log4j-api/pom.xml                                  |  11 +-
 .../logging/log4j/internal/DefaultLogBuilder.java  |   4 +-
 .../logging/log4j/internal/QueueFactories.java     | 160 ---------
 .../log4j/internal/QueueingRecyclerFactory.java    |  92 -----
 .../logging/log4j/internal/RecyclerFactories.java  | 173 ----------
 .../log4j/internal/StringParameterParser.java      | 304 -----------------
 .../log4j/internal/ThreadLocalRecyclerFactory.java | 100 ------
 .../log4j/internal/recycler/ArrayQueue.java        | 100 ++++++
 .../recycler/DummyRecyclerFactoryProvider.java     |  81 +++++
 .../recycler/QueueingRecyclerFactoryProvider.java  | 106 ++++++
 .../ThreadLocalRecyclerFactoryProvider.java        | 125 +++++++
 .../logging/log4j/message/MessageFactory.java      |   2 +-
 .../log4j/message/ParameterizedMessage.java        |   2 +-
 .../log4j/message/ReusableMessageFactory.java      |   4 +-
 .../message/ReusableParameterizedMessage.java      |   4 +-
 .../apache/logging/log4j/spi/AbstractLogger.java   |   2 +
 .../logging/log4j/spi/DummyRecyclerFactory.java    |  59 ----
 .../apache/logging/log4j/spi/LoggingSystem.java    |   6 +-
 .../logging/log4j/spi/LoggingSystemProperty.java   |  13 +-
 .../logging/log4j/spi/PropertyComponent.java       |   2 +
 .../org/apache/logging/log4j/spi/QueueFactory.java |  32 --
 .../apache/logging/log4j/spi/RecyclerFactory.java  |  58 ----
 .../log4j/spi/{ => recycler}/AbstractRecycler.java |  10 +-
 .../logging/log4j/spi/{ => recycler}/Recycler.java |  10 +-
 .../log4j/spi/{ => recycler}/RecyclerAware.java    |   3 +-
 .../log4j/spi/recycler/RecyclerFactory.java        |  58 ++++
 .../spi/recycler/RecyclerFactoryProvider.java      |  59 ++++
 .../spi/recycler/RecyclerFactoryRegistry.java      | 119 +++++++
 .../logging/log4j/spi/recycler/package-info.java   |  26 ++
 .../apache/logging/log4j/status/StatusLogger.java  |   5 +-
 .../logging/log4j/util/ServiceLoaderUtil.java      |  11 +
 log4j-core-its/pom.xml                             |   6 -
 log4j-core-test/pom.xml                            |   6 -
 .../log4j/core/appender/AsyncAppenderTest.java     |   8 -
 log4j-core/pom.xml                                 |   7 -
 .../logging/log4j/core/config/Configuration.java   |   2 +-
 .../log4j/core/filter/StructuredDataFilter.java    |   4 +-
 .../logging/log4j/core/impl/DefaultBundle.java     |   1 +
 .../logging/log4j/core/impl/MutableLogEvent.java   |   2 +-
 .../log4j/core/impl/ReusableLogEventFactory.java   |   4 +-
 .../log4j/core/layout/AbstractStringLayout.java    |   4 +-
 .../logging/log4j/core/layout/PatternLayout.java   |   2 +-
 .../log4j/core/layout/StringBuilderEncoder.java    |   2 +-
 .../apache/logging/log4j/core/util/JsonUtils.java  |   2 +-
 log4j-gctests/pom.xml                              |   6 -
 {log4j-api => log4j-jctools}/pom.xml               |  58 ++--
 .../jctools}/JCToolsBlockingQueueFactory.java      |   9 +-
 .../jctools/JCToolsRecyclerFactoryProvider.java    | 104 ++++++
 .../jctools/JCToolsBlockingQueueFactoryTest.java   |  76 +++++
 .../JCToolsRecyclerFactoryProviderTest.java        |  24 +-
 .../resources/JCToolsBlockingQueueFactoryTest.xml  |   2 +-
 ...ging.log4j.spi.recycler.RecyclerFactoryProvider |   6 +
 log4j-layout-template-json-test/pom.xml            |   6 -
 ...rFactoryCustomizedJsonTemplateLayoutLogging.xml |  30 --
 log4j-layout-template-json/pom.xml                 |   5 -
 .../layout/template/json/JsonTemplateLayout.java   |   2 +-
 .../template/json/resolver/CounterResolver.java    |   2 +-
 .../json/resolver/MessageParameterResolver.java    |   2 +-
 .../json/resolver/ReadOnlyStringMapResolver.java   |   4 +-
 .../json/resolver/StackTraceStringResolver.java    |   4 +-
 .../plugins/convert/TypeConverterFactory.java      |   3 -
 .../java/org/apache/logging/slf4j/SLF4JLogger.java |   2 +-
 pom.xml                                            |   7 +
 src/site/asciidoc/manual/appenders.adoc            |   1 +
 src/site/asciidoc/runtime-dependencies.adoc        |   4 -
 71 files changed, 1270 insertions(+), 1631 deletions(-)

diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/RecyclerFactoriesTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/RecyclerFactoriesTest.java
deleted file mode 100644
index 9673abe14f..0000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/RecyclerFactoriesTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.util.ArrayDeque;
-import java.util.concurrent.ArrayBlockingQueue;
-import org.apache.logging.log4j.spi.DummyRecyclerFactory;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-import org.assertj.core.api.Assertions;
-import org.assertj.core.api.InstanceOfAssertFactories;
-import org.junit.jupiter.api.Test;
-
-public class RecyclerFactoriesTest {
-
-    @Test
-    void DummyRecyclerFactory_should_work() {
-        final RecyclerFactory actualDummyRecyclerFactory = RecyclerFactories.ofSpec("dummy");
-        Assertions.assertThat(actualDummyRecyclerFactory).isSameAs(DummyRecyclerFactory.getInstance());
-    }
-
-    @Test
-    void ThreadLocalRecyclerFactory_should_work() {
-        final RecyclerFactory actualThreadLocalRecyclerFactory = RecyclerFactories.ofSpec("threadLocal");
-        Assertions.assertThat(actualThreadLocalRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
-                .extracting(ThreadLocalRecyclerFactory::getCapacity)
-                .isEqualTo(RecyclerFactories.CAPACITY);
-    }
-
-    @Test
-    void ThreadLocalRecyclerFactory_should_work_with_capacity() {
-        final int capacity = 13;
-        final RecyclerFactory actualThreadLocalRecyclerFactory =
-                RecyclerFactories.ofSpec("threadLocal:capacity=" + capacity);
-        Assertions.assertThat(actualThreadLocalRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
-                .extracting(ThreadLocalRecyclerFactory::getCapacity)
-                .isEqualTo(capacity);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work() {
-        final RecyclerFactory actualQueueingRecyclerFactory = RecyclerFactories.ofSpec("queue");
-        Assertions.assertThat(actualQueueingRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
-                .extracting(QueueingRecyclerFactory::getCapacity)
-                .isEqualTo(RecyclerFactories.CAPACITY);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work_with_supplier() {
-        final RecyclerFactory recyclerFactory = RecyclerFactories.ofSpec("queue:supplier=java.util.ArrayDeque.new");
-        Assertions.assertThat(recyclerFactory).isInstanceOf(QueueingRecyclerFactory.class);
-        final QueueingRecyclerFactory queueingRecyclerFactory = (QueueingRecyclerFactory) recyclerFactory;
-        final Recycler<Object> recycler = queueingRecyclerFactory.create(Object::new);
-        Assertions.assertThat(recycler).isInstanceOf(QueueingRecyclerFactory.QueueingRecycler.class);
-        final QueueingRecyclerFactory.QueueingRecycler<Object> queueingRecycler =
-                (QueueingRecyclerFactory.QueueingRecycler<Object>) recycler;
-        Assertions.assertThat(queueingRecycler.getQueue()).isInstanceOf(ArrayDeque.class);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work_with_capacity() {
-        final int capacity = 100;
-        final RecyclerFactory actualQueueingRecyclerFactory = RecyclerFactories.ofSpec("queue:capacity=" + capacity);
-        Assertions.assertThat(actualQueueingRecyclerFactory)
-                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
-                .extracting(QueueingRecyclerFactory::getCapacity)
-                .isEqualTo(capacity);
-    }
-
-    @Test
-    void QueueingRecyclerFactory_should_work_with_supplier_and_capacity() {
-        final int capacity = 100;
-        final RecyclerFactory recyclerFactory = RecyclerFactories.ofSpec(
-                "queue:supplier=java.util.concurrent.ArrayBlockingQueue.new,capacity=" + capacity);
-        Assertions.assertThat(recyclerFactory).isInstanceOf(QueueingRecyclerFactory.class);
-        final QueueingRecyclerFactory queueingRecyclerFactory = (QueueingRecyclerFactory) recyclerFactory;
-        final Recycler<Object> recycler = queueingRecyclerFactory.create(Object::new);
-        Assertions.assertThat(recycler).isInstanceOf(QueueingRecyclerFactory.QueueingRecycler.class);
-        final QueueingRecyclerFactory.QueueingRecycler<Object> queueingRecycler =
-                (QueueingRecyclerFactory.QueueingRecycler<Object>) recycler;
-        Assertions.assertThat(queueingRecycler.getQueue()).isInstanceOf(ArrayBlockingQueue.class);
-        final ArrayBlockingQueue<Object> queue = (ArrayBlockingQueue<Object>) queueingRecycler.getQueue();
-        Assertions.assertThat(queue.remainingCapacity()).isEqualTo(capacity);
-    }
-}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/StringParameterParserTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/StringParameterParserTest.java
deleted file mode 100644
index 512cc708c6..0000000000
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/StringParameterParserTest.java
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Set;
-import org.apache.logging.log4j.internal.StringParameterParser.DoubleQuotedStringValue;
-import org.apache.logging.log4j.internal.StringParameterParser.NullValue;
-import org.apache.logging.log4j.internal.StringParameterParser.StringValue;
-import org.apache.logging.log4j.internal.StringParameterParser.Value;
-import org.apache.logging.log4j.internal.StringParameterParser.Values;
-import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-@SuppressWarnings("DoubleBraceInitialization")
-class StringParameterParserTest {
-
-    @Test
-    void test_empty_string() {
-        testSuccess("", Collections.emptyMap());
-    }
-
-    @Test
-    void test_blank_string() {
-        testSuccess("\t", Collections.emptyMap());
-    }
-
-    @Test
-    void test_simple_pair() {
-        testSuccess("a=b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_1() {
-        testSuccess(" a=b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_2() {
-        testSuccess(" a =b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_3() {
-        testSuccess(" a = b", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_simple_pair_with_whitespace_4() {
-        testSuccess(" a = b ", Collections.singletonMap("a", Values.stringValue("b")));
-    }
-
-    @Test
-    void test_null_value_1() {
-        testSuccess("a", Collections.singletonMap("a", Values.nullValue()));
-    }
-
-    @Test
-    void test_null_value_2() {
-        testSuccess("a,b=c,d=", new LinkedHashMap<>() {
-            {
-                put("a", Values.nullValue());
-                put("b", Values.stringValue("c"));
-                put("d", Values.nullValue());
-            }
-        });
-    }
-
-    @Test
-    void test_null_value_3() {
-        testSuccess("a,b=c,d", new LinkedHashMap<>() {
-            {
-                put("a", Values.nullValue());
-                put("b", Values.stringValue("c"));
-                put("d", Values.nullValue());
-            }
-        });
-    }
-
-    @Test
-    void test_null_value_4() {
-        testSuccess("a,b=\"c,=\\\"\",d=,e=f", new LinkedHashMap<>() {
-            {
-                put("a", Values.nullValue());
-                put("b", Values.doubleQuotedStringValue("c,=\""));
-                put("d", Values.nullValue());
-                put("e", Values.stringValue("f"));
-            }
-        });
-    }
-
-    @Test
-    void test_two_pairs() {
-        testSuccess("a=b,c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.stringValue("b"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_01() {
-        testSuccess("a=\"b\"", Collections.singletonMap("a", Values.doubleQuotedStringValue("b")));
-    }
-
-    @Test
-    void test_quoted_string_02() {
-        testSuccess("a=\"b\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("b"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_03() {
-        testSuccess("a=b,c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.stringValue("b"));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_04() {
-        testSuccess("a=\"b\",c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("b"));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_05() {
-        testSuccess("a=\"\\\"b\"", Collections.singletonMap("a", Values.doubleQuotedStringValue("\"b")));
-    }
-
-    @Test
-    void test_quoted_string_06() {
-        testSuccess("a=\"\\\"b\\\"\"", Collections.singletonMap("a", Values.doubleQuotedStringValue("\"b\"")));
-    }
-
-    @Test
-    void test_quoted_string_07() {
-        testSuccess("a=\"\\\"b\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_08() {
-        testSuccess("a=\"\\\"b\\\"\",c=d", new LinkedHashMap<String, Value>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\""));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_09() {
-        testSuccess("a=\"\\\"b,\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b,"));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_10() {
-        testSuccess("a=\"\\\"b\\\",\",c=d", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\","));
-                put("c", Values.stringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_11() {
-        testSuccess("a=\"\\\"b\",c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b"));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_12() {
-        testSuccess("a=\"\\\"b\\\"\",c=\"d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\""));
-                put("c", Values.doubleQuotedStringValue("d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_13() {
-        testSuccess("a=\"\\\"b,\",c=\"\\\"d\"", new LinkedHashMap<String, Value>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b,"));
-                put("c", Values.doubleQuotedStringValue("\"d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_14() {
-        testSuccess("a=\"\\\"b\\\",\",c=\"\\\"d\\\"\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\","));
-                put("c", Values.doubleQuotedStringValue("\"d\""));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_15() {
-        testSuccess("a=\"\\\"b\",c=\",d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b"));
-                put("c", Values.doubleQuotedStringValue(",d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_16() {
-        testSuccess("a=\"\\\"b\\\"\",c=\",d\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\""));
-                put("c", Values.doubleQuotedStringValue(",d"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_17() {
-        testSuccess("a=\"\\\"b,\",c=\"\\\"d,\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b,"));
-                put("c", Values.doubleQuotedStringValue("\"d,"));
-            }
-        });
-    }
-
-    @Test
-    void test_quoted_string_18() {
-        testSuccess("a=\"\\\"b\\\",\",c=\"\\\"d\\\",\"", new LinkedHashMap<>() {
-            {
-                put("a", Values.doubleQuotedStringValue("\"b\","));
-                put("c", Values.doubleQuotedStringValue("\"d\","));
-            }
-        });
-    }
-
-    private static void testSuccess(final String input, final Map<String, Value> expectedMap) {
-        final Map<String, Value> actualMap = StringParameterParser.parse(input);
-        Assertions.assertThat(actualMap).as("input: %s", input).isEqualTo(expectedMap);
-    }
-
-    @Test
-    void test_missing_key() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = ",a=b";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate key at index 0");
-    }
-
-    @Test
-    void test_conflicting_key() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,a";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("conflicting key at index 2");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_01() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\"";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_02() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\"c";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_03() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\",c";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("failed to locate the end of double-quoted content starting at index 4");
-    }
-
-    @Test
-    void test_prematurely_ending_quoted_string_04() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b=\",c\" x";
-                    StringParameterParser.parse(input);
-                })
-                .hasMessageStartingWith("was expecting comma at index 9");
-    }
-
-    @Test
-    void test_NullValue_toString() {
-        final Map<String, Value> map = StringParameterParser.parse("a");
-        final NullValue value = (NullValue) map.get("a");
-        Assertions.assertThat(value.toString()).isEqualTo("null");
-    }
-
-    @Test
-    void test_StringValue_toString() {
-        final Map<String, Value> map = StringParameterParser.parse("a=b");
-        final StringValue value = (StringValue) map.get("a");
-        Assertions.assertThat(value.toString()).isEqualTo("b");
-    }
-
-    @Test
-    void test_DoubleQuotedStringValue_toString() {
-        final Map<String, Value> map = StringParameterParser.parse("a=\"\\\"b\"");
-        final DoubleQuotedStringValue value = (DoubleQuotedStringValue) map.get("a");
-        Assertions.assertThat(value.toString()).isEqualTo("\"b");
-    }
-
-    @Test
-    void test_allowedKeys() {
-        Assertions.assertThatThrownBy(() -> {
-                    final String input = "a,b";
-                    final Set<String> allowedKeys = new LinkedHashSet<>(Collections.singletonList("a"));
-                    StringParameterParser.parse(input, allowedKeys);
-                })
-                .hasMessageStartingWith("unknown key \"b\" is found in input: a,b");
-    }
-}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ArrayQueueTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ArrayQueueTest.java
new file mode 100644
index 0000000000..2ded8ff7a8
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ArrayQueueTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.ArrayBlockingQueue;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class ArrayQueueTest {
+
+    @ParameterizedTest
+    @ValueSource(ints = {-1, 0})
+    void invalid_capacity_should_not_be_allowed(final int invalidCapacity) {
+        assertThatThrownBy(() -> new ArrayQueue<>(invalidCapacity))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("invalid capacity: " + invalidCapacity);
+    }
+
+    @Test
+    void should_work_with_capacity_1() {
+
+        // Verify initials
+        final Queue<String> queue = new ArrayQueue<>(1);
+        assertThat(queue.size()).isEqualTo(0);
+        assertThat(queue.peek()).isNull();
+        assertThat(queue.poll()).isNull();
+        assertThat(queue).isEmpty();
+
+        // Verify enqueue & deque
+        assertThat(queue.offer("foo")).isTrue();
+        assertThat(queue.offer("bar")).isFalse();
+        assertThat(queue.size()).isEqualTo(1);
+        assertThat(queue).containsOnly("foo");
+        assertThat(queue.peek()).isEqualTo("foo");
+        assertThat(queue.poll()).isEqualTo("foo");
+
+        // Verify final state
+        assertThat(queue.size()).isEqualTo(0);
+        assertThat(queue.peek()).isNull();
+        assertThat(queue.poll()).isNull();
+        assertThat(queue).isEmpty();
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+        "1,0.3", "1,0.5", "1,0.8", "2,0.3", "2,0.5", "2,0.8", "3,0.3", "3,0.5", "3,0.8", "4,0.3", "4,0.5", "4,0.8"
+    })
+    void ops_should_match_with_std_lib(final int capacity, final double pollRatio) {
+
+        // Set the stage
+        final Random random = new Random(0);
+        final int opCount = random.nextInt(100);
+        final Queue<String> queueRef = new ArrayBlockingQueue<>(capacity);
+        final Queue<String> queueTarget = new ArrayQueue<>(capacity);
+
+        for (int opIndex = 0; opIndex < opCount; opIndex++) {
+
+            // Verify entry
+            assertThat(queueTarget.size()).isEqualTo(queueRef.size());
+            assertThat(queueTarget.peek()).isEqualTo(queueRef.peek());
+            assertThat(queueTarget).containsExactlyElementsOf(queueRef);
+
+            // Is this a `poll()`?
+            if (pollRatio >= random.nextDouble()) {
+                assertThat(queueTarget.poll()).isEqualTo(queueRef.poll());
+            }
+
+            // Then this is an `offer()`
+            else {
+                final String item = "op@" + opIndex;
+                assertThat(queueTarget.offer(item)).isEqualTo(queueRef.offer(item));
+            }
+
+            // Verify exit
+            assertThat(queueTarget.size()).isEqualTo(queueRef.size());
+            assertThat(queueTarget.peek()).isEqualTo(queueRef.peek());
+            assertThat(queueTarget).containsExactlyElementsOf(queueRef);
+        }
+    }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
new file mode 100644
index 0000000000..f6684f63cc
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryRegistryTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static org.apache.logging.log4j.internal.recycler.RecyclerFactoryTestUtil.createForEnvironment;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.apache.logging.log4j.internal.recycler.DummyRecyclerFactoryProvider.DummyRecyclerFactory;
+import org.apache.logging.log4j.internal.recycler.QueueingRecyclerFactoryProvider.QueueingRecyclerFactory;
+import org.apache.logging.log4j.internal.recycler.ThreadLocalRecyclerFactoryProvider.ThreadLocalRecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+public class RecyclerFactoryRegistryTest {
+
+    @Test
+    void DummyRecyclerFactory_should_work() {
+        final RecyclerFactory factory = createForEnvironment(null, "dummy", null);
+        assertThat(factory).isInstanceOf(DummyRecyclerFactory.class);
+    }
+
+    @Test
+    void ThreadLocalRecyclerFactory_should_work() {
+        final RecyclerFactory factory = createForEnvironment(null, "threadLocal", null);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(DEFAULT_CAPACITY);
+    }
+
+    @Test
+    void ThreadLocalRecyclerFactory_should_work_with_capacity() {
+        final int capacity = 13;
+        final RecyclerFactory factory = createForEnvironment(null, "threadLocal", capacity);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(ThreadLocalRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(capacity);
+    }
+
+    @Test
+    void QueueingRecyclerFactory_should_work() {
+        final RecyclerFactory factory = createForEnvironment(null, "queue", null);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(DEFAULT_CAPACITY);
+    }
+
+    @Test
+    void QueueingRecyclerFactory_should_work_with_capacity() {
+        final int capacity = 100;
+        final RecyclerFactory factory = createForEnvironment(null, "queue", capacity);
+        assertThat(factory)
+                .asInstanceOf(InstanceOfAssertFactories.type(QueueingRecyclerFactory.class))
+                .extracting(factory_ -> factory_.capacity)
+                .isEqualTo(capacity);
+    }
+
+    @Test
+    void verify_order() {
+        final RecyclerFactoryProvider dummyProvider = new DummyRecyclerFactoryProvider();
+        final RecyclerFactoryProvider threadLocalProvider = new ThreadLocalRecyclerFactoryProvider();
+        final RecyclerFactoryProvider queueProvider = new QueueingRecyclerFactoryProvider();
+        assertThat(dummyProvider.getOrder()).isGreaterThan(queueProvider.getOrder());
+        assertThat(queueProvider.getOrder()).isGreaterThan(threadLocalProvider.getOrder());
+    }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryTestUtil.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryTestUtil.java
new file mode 100644
index 0000000000..a6c8f99a2c
--- /dev/null
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/RecyclerFactoryTestUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Properties;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+final class RecyclerFactoryTestUtil {
+
+    private RecyclerFactoryTestUtil() {}
+
+    @Nullable
+    static RecyclerFactory createForEnvironment(
+            @Nullable Boolean threadLocalsEnabled, @Nullable final String factory, @Nullable final Integer capacity) {
+        final Properties properties = new Properties();
+        if (threadLocalsEnabled != null) {
+            properties.setProperty("log4j2.*.ThreadLocals.enable", "" + threadLocalsEnabled);
+        }
+        if (factory != null) {
+            properties.setProperty("log4j2.*.Recycler.factory", factory);
+        }
+        if (capacity != null) {
+            properties.setProperty("log4j2.*.Recycler.capacity", "" + capacity);
+        }
+        final PropertyEnvironment env = new PropertiesUtil(properties);
+        return RecyclerFactoryRegistry.findRecyclerFactory(env);
+    }
+}
diff --git a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactoryTest.java b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
similarity index 60%
rename from log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactoryTest.java
rename to log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
index c822e2218f..147a84e52c 100644
--- a/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactoryTest.java
+++ b/log4j-api-test/src/test/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProviderTest.java
@@ -14,34 +14,48 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.internal;
+package org.apache.logging.log4j.internal.recycler;
 
+import static org.apache.logging.log4j.internal.recycler.RecyclerFactoryTestUtil.createForEnvironment;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.util.List;
 import java.util.Queue;
 import java.util.stream.Collectors;
 import java.util.stream.IntStream;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.internal.recycler.ThreadLocalRecyclerFactoryProvider.ThreadLocalRecyclerFactory;
+import org.apache.logging.log4j.internal.recycler.ThreadLocalRecyclerFactoryProvider.ThreadLocalRecyclerFactory.ThreadLocalRecycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junitpioneer.jupiter.params.IntRangeSource;
 
-class ThreadLocalRecyclerFactoryTest {
+class ThreadLocalRecyclerFactoryProviderTest {
 
-    private static final int CAPACITY = 8;
+    private static final int CAPACITY = 13;
 
     private static class RecyclableObject {}
 
-    private Recycler<RecyclableObject> recycler;
+    private ThreadLocalRecycler<RecyclableObject> recycler;
 
     private Queue<RecyclableObject> recyclerQueue;
 
     @BeforeEach
     void setUp() {
-        recycler = new ThreadLocalRecyclerFactory(CAPACITY).create(RecyclableObject::new);
-        recyclerQueue = ((ThreadLocalRecyclerFactory.ThreadLocalRecycler<RecyclableObject>) recycler).getQueue();
+        final RecyclerFactory recyclerFactory = createForEnvironment(null, "threadLocal", CAPACITY);
+        assertThat(recyclerFactory).isInstanceOf(ThreadLocalRecyclerFactory.class);
+        assert recyclerFactory != null;
+        recycler = (ThreadLocalRecycler<RecyclableObject>) recyclerFactory.create(RecyclableObject::new);
+        recyclerQueue = recycler.queueRef.get();
+    }
+
+    @Test
+    void should_not_be_configured_when_TLs_are_disabled() {
+        assertThatThrownBy(() -> createForEnvironment(false, "threadLocal", null))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageStartingWith("failed to configure recycler");
     }
 
     @ParameterizedTest
@@ -79,18 +93,22 @@ class ThreadLocalRecyclerFactoryTest {
 
         assertThat(recyclerQueue).isEmpty();
 
-        // simulate a massively callstack with tons of logging
-        final List<RecyclableObject> acquiredObjects =
-                IntStream.range(0, 1024).mapToObj(i -> recycler.acquire()).collect(Collectors.toList());
+        // Simulate a callstack with excessive logging
+        final int acquisitionCount = Math.addExact(CAPACITY, 1024);
+        final List<RecyclableObject> acquiredObjects = IntStream.range(0, acquisitionCount)
+                .mapToObj(i -> recycler.acquire())
+                .toList();
 
-        // still nothing returned to pool
+        // Verify collected instances are all new
+        assertThat(acquiredObjects).doesNotHaveDuplicates();
+
+        // Verify the pool is still empty
         assertThat(recyclerQueue).isEmpty();
 
-        // don't want any duplicate instances
-        assertThat(acquiredObjects).containsOnlyOnceElementsOf(acquiredObjects);
+        // Release all acquired instances
         acquiredObjects.forEach(recycler::release);
 
-        // upon return, we should only have `CAPACITY` retained for future use
+        // Verify the queue size is capped
         assertThat(recyclerQueue).hasSize(CAPACITY);
     }
 }
diff --git a/log4j-api/pom.xml b/log4j-api/pom.xml
index 2ea9e66998..bb22c40e74 100644
--- a/log4j-api/pom.xml
+++ b/log4j-api/pom.xml
@@ -36,9 +36,7 @@
     <bnd-module-name>org.apache.logging.log4j</bnd-module-name>
     <bnd-extra-package-options>
       <!-- No JSR 305 at runtime -->
-      !javax.annotation.*,
-      <!-- Optional dependencies -->
-      org.jctools.*;resolution:=optional
+      !javax.annotation.*
     </bnd-extra-package-options>
     <bnd-extra-module-options>
       <!-- No JSR 305 at runtime -->
@@ -48,11 +46,4 @@
     </bnd-extra-module-options>
 
   </properties>
-  <dependencies>
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
-  </dependencies>
 </project>
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java
index e53fd1f589..cffd2ce56f 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/DefaultLogBuilder.java
@@ -24,8 +24,8 @@ import org.apache.logging.log4j.Marker;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.spi.ExtendedLogger;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerAware;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerAware;
 import org.apache.logging.log4j.util.*;
 
 /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
deleted file mode 100644
index 3db9e5d6e9..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueFactories.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.util.Queue;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.apache.logging.log4j.util.Cast;
-import org.apache.logging.log4j.util.InternalApi;
-import org.apache.logging.log4j.util.LoaderUtil;
-import org.jctools.queues.MpmcArrayQueue;
-import org.jctools.queues.SpscArrayQueue;
-
-/**
- * Provides {@link QueueFactory} instances for different use cases.
- * <p>
- * Implementations provided by <a href="https://jctools.github.io/JCTools/">JCTools</a> will be preferred, if available at runtime.
- * Otherwise, {@link ArrayBlockingQueue} will be used.
- * </p>
- *
- * @since 3.0.0
- */
-@InternalApi
-public enum QueueFactories implements QueueFactory {
-
-    /**
-     * Provides a bounded queue for single-producer/single-consumer usage.
-     */
-    SPSC(() -> SpscArrayQueue::new),
-
-    /**
-     * Provides a bounded queue for multi-producer/multi-consumer usage.
-     */
-    MPMC(() -> MpmcArrayQueue::new);
-
-    private final QueueFactory queueFactory;
-
-    QueueFactories(final Supplier<QueueFactory> queueFactoryProvider) {
-        this.queueFactory = getOrReplaceQueueFactory(queueFactoryProvider);
-    }
-
-    private static QueueFactory getOrReplaceQueueFactory(final Supplier<QueueFactory> queueFactoryProvider) {
-        try {
-            final QueueFactory queueFactory = queueFactoryProvider.get();
-            // Test with a large enough capacity to avoid any `IllegalArgumentExceptions` from trivial queues
-            queueFactory.create(16);
-            return queueFactory;
-        } catch (final LinkageError ignored) {
-            return ArrayBlockingQueueFactory.INSTANCE;
-        }
-    }
-
-    @Override
-    public <E> Queue<E> create(final int capacity) {
-        return queueFactory.create(capacity);
-    }
-
-    private static final class ArrayBlockingQueueFactory implements QueueFactory {
-
-        private static final ArrayBlockingQueueFactory INSTANCE = new ArrayBlockingQueueFactory();
-
-        private ArrayBlockingQueueFactory() {}
-
-        @Override
-        public <E> Queue<E> create(final int capacity) {
-            return new ArrayBlockingQueue<>(capacity);
-        }
-    }
-
-    /**
-     * Creates a {@link QueueFactory} using the provided supplier.
-     * <p>
-     * A supplier path must be formatted as follows:
-     * <ul>
-     * <li>{@code <fully-qualified-class-name>.new} – the class constructor accepting a single {@code int} argument (denoting the capacity) will be used (e.g., {@code org.jctools.queues.MpmcArrayQueue.new})</li>
-     * <li>{@code <fully-qualified-class-name>.<static-factory-method>} – the static factory method accepting a single {@code int} argument (denoting the capacity) will be used (e.g., {@code com.acme.Queues.createBoundedQueue})</li>
-     * </ul>
-     * </p>
-     *
-     * @param supplierPath a queue supplier path (e.g., {@code org.jctools.queues.MpmcArrayQueue.new}, {@code com.acme.Queues.createBoundedQueue})
-     * @return a new {@link QueueFactory} instance
-     */
-    public static QueueFactory ofSupplier(final String supplierPath) {
-        final int supplierPathSplitterIndex = supplierPath.lastIndexOf('.');
-        if (supplierPathSplitterIndex < 0) {
-            final String message = String.format("invalid queue factory supplier path: `%s`", supplierPath);
-            throw new IllegalArgumentException(message);
-        }
-        final String supplierClassName = supplierPath.substring(0, supplierPathSplitterIndex);
-        final String supplierMethodName = supplierPath.substring(supplierPathSplitterIndex + 1);
-        try {
-            final Class<?> supplierClass = LoaderUtil.loadClass(supplierClassName);
-            if ("new".equals(supplierMethodName)) {
-                final Constructor<?> supplierCtor = supplierClass.getDeclaredConstructor(int.class);
-                return new ConstructorProvidedQueueFactory(supplierCtor);
-            } else {
-                final Method supplierMethod = supplierClass.getMethod(supplierMethodName, int.class);
-                return new StaticMethodProvidedQueueFactory(supplierMethod);
-            }
-        } catch (final ReflectiveOperationException | LinkageError | SecurityException error) {
-            final String message =
-                    String.format("failed to create the queue factory using the supplier path `%s`", supplierPath);
-            throw new RuntimeException(message, error);
-        }
-    }
-
-    private static final class ConstructorProvidedQueueFactory implements QueueFactory {
-
-        private final Constructor<?> constructor;
-
-        private ConstructorProvidedQueueFactory(final Constructor<?> constructor) {
-            this.constructor = constructor;
-        }
-
-        @Override
-        public <E> Queue<E> create(final int capacity) {
-            final Constructor<Queue<E>> typedConstructor = Cast.cast(constructor);
-            try {
-                return typedConstructor.newInstance(capacity);
-            } catch (final ReflectiveOperationException error) {
-                throw new RuntimeException("queue construction failure", error);
-            }
-        }
-    }
-
-    private static final class StaticMethodProvidedQueueFactory implements QueueFactory {
-
-        private final Method method;
-
-        private StaticMethodProvidedQueueFactory(final Method method) {
-            this.method = method;
-        }
-
-        @Override
-        public <E> Queue<E> create(final int capacity) {
-            try {
-                return Cast.cast(method.invoke(null, capacity));
-            } catch (final ReflectiveOperationException error) {
-                throw new RuntimeException("queue construction failure", error);
-            }
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingRecyclerFactory.java
deleted file mode 100644
index b5b6c70f72..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/QueueingRecyclerFactory.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.Queue;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.AbstractRecycler;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-
-/**
- * A {@link RecyclerFactory} pooling objects in a queue created using the provided {@link QueueFactory}.
- */
-final class QueueingRecyclerFactory implements RecyclerFactory {
-
-    private final QueueFactory queueFactory;
-
-    private final int capacity;
-
-    QueueingRecyclerFactory(final QueueFactory queueFactory, final int capacity) {
-        if (capacity < 1) {
-            throw new IllegalArgumentException("was expecting `capacity > 0`, found: " + capacity);
-        }
-        this.queueFactory = requireNonNull(queueFactory, "queueFactory");
-        this.capacity = capacity;
-    }
-
-    /**
-     * @return the maximum number of objects retained per thread in recyclers created
-     */
-    int getCapacity() {
-        return capacity;
-    }
-
-    @Override
-    public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
-        requireNonNull(supplier, "supplier");
-        requireNonNull(cleaner, "cleaner");
-        final Queue<V> queue = queueFactory.create(capacity);
-        return new QueueingRecycler<>(supplier, cleaner, queue);
-    }
-
-    // Visible for tests
-    static class QueueingRecycler<V> extends AbstractRecycler<V> {
-
-        private final Consumer<V> cleaner;
-
-        private final Queue<V> queue;
-
-        private QueueingRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
-            super(supplier);
-            this.cleaner = cleaner;
-            this.queue = queue;
-        }
-
-        // Visible for tests
-        Queue<V> getQueue() {
-            return queue;
-        }
-
-        @Override
-        public V acquire() {
-            final V value = queue.poll();
-            return value != null ? value : createInstance();
-        }
-
-        @Override
-        public void release(final V value) {
-            requireNonNull(value, "value");
-            cleaner.accept(value);
-            queue.offer(value);
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
deleted file mode 100644
index aea850f199..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/RecyclerFactories.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static java.util.Objects.requireNonNull;
-import static org.apache.logging.log4j.util.Constants.isThreadLocalsEnabled;
-
-import java.util.Map;
-import java.util.Set;
-import org.apache.logging.log4j.spi.DummyRecyclerFactory;
-import org.apache.logging.log4j.spi.QueueFactory;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-import org.apache.logging.log4j.util.InternalApi;
-
-/**
- * Stores the default {@link RecyclerFactory} instance.
- */
-@InternalApi
-public final class RecyclerFactories {
-
-    /**
-     * The default recycler capacity.
-     */
-    public static final int CAPACITY = Math.max(2 * Runtime.getRuntime().availableProcessors() + 1, 8);
-
-    /**
-     * The default recycler instance.
-     */
-    public static final RecyclerFactory INSTANCE = isThreadLocalsEnabled()
-            ? new ThreadLocalRecyclerFactory(CAPACITY)
-            : new QueueingRecyclerFactory(QueueFactories.MPMC, CAPACITY);
-
-    private RecyclerFactories() {}
-
-    /**
-     * Creates a {@link RecyclerFactory} instance using the provided specification.
-     * <p>
-     * The recycler factory specification string must be formatted as follows:
-     * </p>
-     * <pre>{@code
-     * recyclerFactorySpec            = dummySpec
-     *                                | threadLocalRecyclerFactorySpec
-     *                                | queueingRecyclerFactorySpec
-     *
-     * dummySpec                      = "dummy"
-     *
-     * threadLocalRecyclerFactorySpec = "threadLocal" , [ ":" , capacityArg ]
-     * capacityArg                    = "capacity=" , integer
-     *
-     * queueingRecyclerFactorySpec    = "queue" , [ ":" , queueingRecyclerFactoryArgs ]
-     * queueingRecyclerFactoryArgs    = queueingRecyclerFactoryArg , [ "," , queueingRecyclerFactoryArg ]*
-     * queueingRecyclerFactoryArg     = capacityArg
-     *                                | queueSupplierArg
-     * queueSupplierArg               = ( classPath , ".new" )
-     *                                | ( classPath , "." , methodName )
-     * }</pre>
-     * <p>
-     * If not specified, {@code capacity} will be set to {@code max(8, 2*C+1)}, where {@code C} denotes the value returned by {@link Runtime#availableProcessors()}.
-     * </p>
-     * <p>
-     * You can find some examples below.
-     * </p>
-     * <ul>
-     * <li><code>{@code dummy}</code></li>
-     * <li><code>{@code threadLocal}</code></li>
-     * <li><code>{@code threadLocal:capacity=13}</code></li>
-     * <li><code>{@code queue}</code></li>
-     * <li><code>{@code queue:supplier=java.util.ArrayDeque.new}</code></li>
-     * <li><code>{@code queue:capacity=100}</code></li>
-     * <li><code>{@code queue:supplier=com.acme.AwesomeQueue.create,capacity=42}</code></li>
-     * </ul>
-     * @param recyclerFactorySpec the recycler factory specification string
-     * @return a recycler factory instance
-     */
-    public static RecyclerFactory ofSpec(final String recyclerFactorySpec) {
-
-        // Check arguments
-        requireNonNull(recyclerFactorySpec, "recyclerFactorySpec");
-
-        // Is a dummy factory requested?
-        if (recyclerFactorySpec.equals("dummy")) {
-            return DummyRecyclerFactory.getInstance();
-        }
-
-        // Is a TLA factory requested?
-        else if (recyclerFactorySpec.startsWith("threadLocal")) {
-            return readThreadLocalRecyclerFactory(recyclerFactorySpec);
-        }
-
-        // Is a queueing factory requested?
-        else if (recyclerFactorySpec.startsWith("queue")) {
-            return readQueueingRecyclerFactory(recyclerFactorySpec);
-        }
-
-        // Bogus input, bail out.
-        else {
-            throw new IllegalArgumentException("invalid recycler factory: " + recyclerFactorySpec);
-        }
-    }
-
-    private static RecyclerFactory readThreadLocalRecyclerFactory(final String recyclerFactorySpec) {
-
-        // Parse the spec
-        final String queueFactorySpec = recyclerFactorySpec.substring(
-                "threadLocal".length() + (recyclerFactorySpec.startsWith("threadLocal:") ? 1 : 0));
-        final Map<String, StringParameterParser.Value> parsedValues =
-                StringParameterParser.parse(queueFactorySpec, Set.of("capacity"));
-
-        // Read the capacity
-        final int capacity = readQueueCapacity(queueFactorySpec, parsedValues);
-
-        // Execute the read spec
-        return new ThreadLocalRecyclerFactory(capacity);
-    }
-
-    private static RecyclerFactory readQueueingRecyclerFactory(final String recyclerFactorySpec) {
-
-        // Parse the spec
-        final String queueFactorySpec =
-                recyclerFactorySpec.substring("queue".length() + (recyclerFactorySpec.startsWith("queue:") ? 1 : 0));
-        final Map<String, StringParameterParser.Value> parsedValues =
-                StringParameterParser.parse(queueFactorySpec, Set.of("supplier", "capacity"));
-
-        // Read the capacity
-        final int capacity = readQueueCapacity(queueFactorySpec, parsedValues);
-
-        // Read the supplier path
-        final StringParameterParser.Value supplierValue = parsedValues.get("supplier");
-        final String supplierPath = supplierValue == null || supplierValue instanceof StringParameterParser.NullValue
-                ? null
-                : supplierValue.toString();
-
-        // Execute the read spec
-        final QueueFactory queueFactory =
-                supplierPath != null ? QueueFactories.ofSupplier(supplierPath) : QueueFactories.MPMC;
-        return new QueueingRecyclerFactory(queueFactory, capacity);
-    }
-
-    private static int readQueueCapacity(
-            final String factorySpec, final Map<String, StringParameterParser.Value> parsedValues) {
-        final StringParameterParser.Value capacityValue = parsedValues.get("capacity");
-        if (capacityValue == null || capacityValue instanceof StringParameterParser.NullValue) {
-            return CAPACITY;
-        } else {
-            final int capacity;
-            try {
-                capacity = Integer.parseInt(capacityValue.toString());
-            } catch (final NumberFormatException error) {
-                throw new IllegalArgumentException(
-                        "failed reading `capacity` in recycler factory: " + factorySpec, error);
-            }
-            if (capacity < 1) {
-                throw new IllegalArgumentException(
-                        "was expecting `capacity > 0` in the recycler factory: " + factorySpec);
-            }
-            return capacity;
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringParameterParser.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringParameterParser.java
deleted file mode 100644
index 9d1127ae28..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/StringParameterParser.java
+++ /dev/null
@@ -1,304 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import java.util.*;
-import java.util.concurrent.Callable;
-import org.apache.logging.log4j.util.InternalApi;
-import org.apache.logging.log4j.util.Strings;
-
-/**
- * Utility class for parsing string-formatted parameters, e.g., {@code queue:supplier=com.acme.FastestQueue.new,capacity=42}.
- * <p>
- * See the associated test class for possible combinations and double-, single-quote handling.
- * </p>
- */
-@InternalApi
-public final class StringParameterParser {
-
-    private StringParameterParser() {}
-
-    public static final class Values {
-
-        private Values() {}
-
-        static NullValue nullValue() {
-            return NullValue.INSTANCE;
-        }
-
-        static StringValue stringValue(final String string) {
-            return new StringValue(string);
-        }
-
-        static DoubleQuotedStringValue doubleQuotedStringValue(final String doubleQuotedString) {
-            return new DoubleQuotedStringValue(doubleQuotedString);
-        }
-    }
-
-    public interface Value {}
-
-    public static final class NullValue implements Value {
-
-        private static final NullValue INSTANCE = new NullValue();
-
-        private NullValue() {}
-
-        @Override
-        public String toString() {
-            return "null";
-        }
-    }
-
-    public static final class StringValue implements Value {
-
-        private final String string;
-
-        private StringValue(final String string) {
-            this.string = string;
-        }
-
-        public String getString() {
-            return string;
-        }
-
-        @Override
-        public boolean equals(final Object object) {
-            if (this == object) {
-                return true;
-            }
-            if (object == null || getClass() != object.getClass()) {
-                return false;
-            }
-            final StringValue that = (StringValue) object;
-            return string.equals(that.string);
-        }
-
-        @Override
-        public int hashCode() {
-            return 31 + Objects.hashCode(string);
-        }
-
-        @Override
-        public String toString() {
-            return string;
-        }
-    }
-
-    public static final class DoubleQuotedStringValue implements Value {
-
-        private final String doubleQuotedString;
-
-        private DoubleQuotedStringValue(final String doubleQuotedString) {
-            this.doubleQuotedString = doubleQuotedString;
-        }
-
-        public String getDoubleQuotedString() {
-            return doubleQuotedString;
-        }
-
-        @Override
-        public boolean equals(final Object object) {
-            if (this == object) {
-                return true;
-            }
-            if (object == null || getClass() != object.getClass()) {
-                return false;
-            }
-            final DoubleQuotedStringValue that = (DoubleQuotedStringValue) object;
-            return doubleQuotedString.equals(that.doubleQuotedString);
-        }
-
-        @Override
-        public int hashCode() {
-            return 31 + Objects.hashCode(doubleQuotedString);
-        }
-
-        @Override
-        public String toString() {
-            return doubleQuotedString.replaceAll("\\\\\"", "\"");
-        }
-    }
-
-    private enum State {
-        READING_KEY,
-        READING_VALUE
-    }
-
-    private static final class Parser implements Callable<Map<String, Value>> {
-
-        private final String input;
-
-        private final Map<String, Value> map;
-
-        private State state;
-
-        private int i;
-
-        private String key;
-
-        private Parser(final String input) {
-            this.input = Objects.requireNonNull(input, "input");
-            this.map = new LinkedHashMap<>();
-            this.state = State.READING_KEY;
-            this.i = 0;
-            this.key = null;
-        }
-
-        @Override
-        public Map<String, Value> call() {
-            while (true) {
-                skipWhitespace();
-                if (i >= input.length()) {
-                    break;
-                }
-                switch (state) {
-                    case READING_KEY:
-                        readKey();
-                        break;
-                    case READING_VALUE:
-                        readValue();
-                        break;
-                    default:
-                        throw new IllegalStateException("unknown state: " + state);
-                }
-            }
-            if (state == State.READING_VALUE) {
-                map.put(key, Values.nullValue());
-            }
-            return map;
-        }
-
-        private void readKey() {
-            final int eq = input.indexOf('=', i);
-            final int co = input.indexOf(',', i);
-            final int j;
-            final int nextI;
-            if (eq < 0 && co < 0) {
-                // Neither '=', nor ',' was found.
-                j = nextI = input.length();
-            } else if (eq < 0) {
-                // Found ','.
-                j = nextI = co;
-            } else if (co < 0) {
-                // Found '='.
-                j = eq;
-                nextI = eq + 1;
-            } else if (eq < co) {
-                // Found '=...,'.
-                j = eq;
-                nextI = eq + 1;
-            } else {
-                // Found ',...='.
-                j = co;
-                nextI = co;
-            }
-            key = input.substring(i, j).trim();
-            if (Strings.isEmpty(key)) {
-                final String message = String.format("failed to locate key at index %d: %s", i, input);
-                throw new IllegalArgumentException(message);
-            }
-            if (map.containsKey(key)) {
-                final String message = String.format("conflicting key at index %d: %s", i, input);
-                throw new IllegalArgumentException(message);
-            }
-            state = State.READING_VALUE;
-            i = nextI;
-        }
-
-        private void readValue() {
-            final boolean doubleQuoted = input.charAt(i) == '"';
-            if (doubleQuoted) {
-                readDoubleQuotedStringValue();
-            } else {
-                readStringValue();
-            }
-            key = null;
-            state = State.READING_KEY;
-        }
-
-        private void readDoubleQuotedStringValue() {
-            int j = i + 1;
-            while (j < input.length()) {
-                if (input.charAt(j) == '"' && input.charAt(j - 1) != '\\') {
-                    break;
-                } else {
-                    j++;
-                }
-            }
-            if (j >= input.length()) {
-                final String message = String.format(
-                        "failed to locate the end of double-quoted content starting at index %d: %s", i, input);
-                throw new IllegalArgumentException(message);
-            }
-            final String content = input.substring(i + 1, j).replaceAll("\\\\\"", "\"");
-            final Value value = Values.doubleQuotedStringValue(content);
-            map.put(key, value);
-            i = j + 1;
-            skipWhitespace();
-            if (i < input.length()) {
-                if (input.charAt(i) != ',') {
-                    final String message = String.format("was expecting comma at index %d: %s", i, input);
-                    throw new IllegalArgumentException(message);
-                }
-                i++;
-            }
-        }
-
-        private void skipWhitespace() {
-            while (i < input.length()) {
-                final char c = input.charAt(i);
-                if (!Character.isWhitespace(c)) {
-                    break;
-                } else {
-                    i++;
-                }
-            }
-        }
-
-        private void readStringValue() {
-            int j = input.indexOf(',', i /* + 1*/);
-            if (j < 0) {
-                j = input.length();
-            }
-            final String content = input.substring(i, j);
-            final String trimmedContent = content.trim();
-            final Value value = trimmedContent.isEmpty() ? Values.nullValue() : Values.stringValue(trimmedContent);
-            map.put(key, value);
-            i += content.length() + 1;
-        }
-    }
-
-    public static Map<String, Value> parse(final String input) {
-        return parse(input, null);
-    }
-
-    public static Map<String, Value> parse(final String input, final Set<String> allowedKeys) {
-        if (Strings.isBlank(input)) {
-            return Collections.emptyMap();
-        }
-        final Map<String, Value> map = new Parser(input).call();
-        final Set<String> actualKeys = map.keySet();
-        for (final String actualKey : actualKeys) {
-            final boolean allowed = allowedKeys == null || allowedKeys.contains(actualKey);
-            if (!allowed) {
-                final String message = String.format("unknown key \"%s\" is found in input: %s", actualKey, input);
-                throw new IllegalArgumentException(message);
-            }
-        }
-        return map;
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
deleted file mode 100644
index 56e8930547..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/internal/ThreadLocalRecyclerFactory.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.internal;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.Queue;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import org.apache.logging.log4j.spi.AbstractRecycler;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
-
-/**
- * A {@link RecyclerFactory} pooling objects in a queue stored in a {@link ThreadLocal}.
- * <p>
- * This strategy may not be appropriate in workloads where units of work are independent of operating system threads such as reactive streams, coroutines, or virtual threads.
- * For such use cases, see {@link QueueingRecyclerFactory}.
- * </p>
- *
- * @since 3.0.0
- */
-final class ThreadLocalRecyclerFactory implements RecyclerFactory {
-
-    /**
-     * Maximum number of objects retained per thread.
-     * <p>
-     * This allows to acquire objects in recursive method calls and maintain minimal overhead in the scenarios where the active instance count goes far beyond this for a brief moment.
-     * </p>
-     */
-    private final int capacity;
-
-    ThreadLocalRecyclerFactory(int capacity) {
-        if (capacity < 1) {
-            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
-        }
-        this.capacity = capacity;
-    }
-
-    /**
-     * @return maximum number of objects retained per thread in recyclers created
-     */
-    int getCapacity() {
-        return capacity;
-    }
-
-    @Override
-    public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
-        requireNonNull(supplier, "supplier");
-        requireNonNull(cleaner, "cleaner");
-        return new ThreadLocalRecycler<>(supplier, cleaner, capacity);
-    }
-
-    // Visible for testing
-    static class ThreadLocalRecycler<V> extends AbstractRecycler<V> {
-
-        private final Consumer<V> cleaner;
-
-        private final ThreadLocal<Queue<V>> holder;
-
-        private ThreadLocalRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final int capacity) {
-            super(supplier);
-            this.holder = ThreadLocal.withInitial(() -> QueueFactories.SPSC.create(capacity));
-            this.cleaner = cleaner;
-        }
-
-        @Override
-        public V acquire() {
-            final Queue<V> queue = holder.get();
-            final V value = queue.poll();
-            return value != null ? value : createInstance();
-        }
-
-        @Override
-        public void release(final V value) {
-            requireNonNull(value, "value");
-            cleaner.accept(value);
-            holder.get().offer(value);
-        }
-
-        // Visible for testing
-        Queue<V> getQueue() {
-            return holder.get();
-        }
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ArrayQueue.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ArrayQueue.java
new file mode 100644
index 0000000000..90c573b364
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ArrayQueue.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import java.util.AbstractQueue;
+import java.util.Iterator;
+import java.util.stream.IntStream;
+import javax.annotation.concurrent.NotThreadSafe;
+import org.apache.logging.log4j.util.InternalApi;
+
+/**
+ * An array-backed, fixed-length, not-thread-safe {@link java.util.Queue} implementation.
+ *
+ * @param <E> the element type
+ */
+@InternalApi
+@NotThreadSafe
+final class ArrayQueue<E> extends AbstractQueue<E> {
+
+    private final E[] buffer;
+
+    private int head;
+
+    private int tail;
+
+    private int size;
+
+    @SuppressWarnings("unchecked")
+    ArrayQueue(final int capacity) {
+        if (capacity < 1) {
+            throw new IllegalArgumentException("invalid capacity: " + capacity);
+        }
+        buffer = (E[]) new Object[capacity];
+        head = 0;
+        tail = -1;
+        size = 0;
+    }
+
+    @Override
+    public Iterator<E> iterator() {
+        int[] i = {head};
+        return IntStream.range(0, size)
+                .mapToObj(ignored -> {
+                    final E item = buffer[i[0]];
+                    i[0] = (i[0] + 1) % buffer.length;
+                    return item;
+                })
+                .iterator();
+    }
+
+    @Override
+    public boolean offer(final E item) {
+        if (size == buffer.length) {
+            return false;
+        }
+        tail = (tail + 1) % buffer.length;
+        buffer[tail] = item;
+        size++;
+        return true;
+    }
+
+    @Override
+    public E poll() {
+        if (isEmpty()) {
+            return null;
+        }
+        final E item = buffer[head];
+        buffer[head] = null; // Clear refs for GC
+        head = (head + 1) % buffer.length;
+        size--;
+        return item;
+    }
+
+    @Override
+    public E peek() {
+        if (isEmpty()) {
+            return null;
+        }
+        return buffer[head];
+    }
+
+    @Override
+    public int size() {
+        return size;
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/DummyRecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/DummyRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..f0a531b657
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/DummyRecyclerFactoryProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static java.util.Objects.requireNonNull;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * A {@link Recycler} factory provider such that the recycler does not recycle anything; all instances are freshly created.
+ *
+ * @since 3.0.0
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class DummyRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 900;
+    }
+
+    @Override
+    public String getName() {
+        return "dummy";
+    }
+
+    @Override
+    public RecyclerFactory createForEnvironment(final PropertyEnvironment environment) {
+        return DummyRecyclerFactory.INSTANCE;
+    }
+
+    // Visible for testing
+    static final class DummyRecyclerFactory implements RecyclerFactory {
+
+        private static final DummyRecyclerFactory INSTANCE = new DummyRecyclerFactory();
+
+        private DummyRecyclerFactory() {}
+
+        @Override
+        public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            return new DummyRecycler<>(supplier);
+        }
+
+        private static final class DummyRecycler<V> extends AbstractRecycler<V> {
+
+            private DummyRecycler(final Supplier<V> supplier) {
+                super(supplier);
+            }
+
+            @Override
+            public V acquire() {
+                return createInstance();
+            }
+
+            @Override
+            public void release(final V value) {}
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/QueueingRecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/QueueingRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..655e9e4a9e
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/QueueingRecyclerFactoryProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * A {@link Recycler} factory provider such that the recycler pools objects in a fixed-size queue.
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class QueueingRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 800;
+    }
+
+    @Override
+    public String getName() {
+        return "queue";
+    }
+
+    @Override
+    public RecyclerFactory createForEnvironment(final PropertyEnvironment environment) {
+        requireNonNull(environment, "environment");
+        final int capacity = environment.getIntegerProperty(LoggingSystemProperty.RECYCLER_CAPACITY, DEFAULT_CAPACITY);
+        if (capacity < 1) {
+            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
+        }
+        return new QueueingRecyclerFactory(capacity);
+    }
+
+    // Visible for testing
+    static final class QueueingRecyclerFactory implements RecyclerFactory {
+
+        // Visible for testing
+        final int capacity;
+
+        private QueueingRecyclerFactory(int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            requireNonNull(cleaner, "cleaner");
+            final Queue<V> queue = new ArrayBlockingQueue<>(capacity);
+            return new QueueingRecycler<>(supplier, cleaner, queue);
+        }
+
+        // Visible for testing
+        static final class QueueingRecycler<V> extends AbstractRecycler<V> {
+
+            private final Consumer<V> cleaner;
+
+            // Visible for testing
+            final Queue<V> queue;
+
+            private QueueingRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
+                super(supplier);
+                this.cleaner = cleaner;
+                this.queue = queue;
+            }
+
+            @Override
+            public V acquire() {
+                final V value = queue.poll();
+                return value != null ? value : createInstance();
+            }
+
+            @Override
+            public void release(final V value) {
+                requireNonNull(value, "value");
+                cleaner.accept(value);
+                queue.offer(value);
+            }
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..6cdbba0863
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/internal/recycler/ThreadLocalRecyclerFactoryProvider.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.internal.recycler;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * A {@link Recycler} factory provider such that the recycler pools objects in a fixed-size queue stored in a {@link ThreadLocal}.
+ * <p>
+ * This strategy may not be appropriate in workloads where units of work are independent of operating system threads such as reactive streams, coroutines, or virtual threads.
+ * For such use cases, see {@link QueueingRecyclerFactoryProvider}.
+ * </p>
+ *
+ * @since 3.0.0
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class ThreadLocalRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 700;
+    }
+
+    @Override
+    public String getName() {
+        return "threadLocal";
+    }
+
+    @Nullable
+    @Override
+    public RecyclerFactory createForEnvironment(PropertyEnvironment environment) {
+        requireNonNull(environment, "environment");
+        final boolean threadLocalEnabled =
+                environment.getBooleanProperty(LoggingSystemProperty.THREAD_LOCALS_ENABLE, true);
+        if (!threadLocalEnabled) {
+            return null;
+        }
+        final int capacity = environment.getIntegerProperty(LoggingSystemProperty.RECYCLER_CAPACITY, DEFAULT_CAPACITY);
+        if (capacity < 1) {
+            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
+        }
+        return new ThreadLocalRecyclerFactory(capacity);
+    }
+
+    // Visible for testing
+    static final class ThreadLocalRecyclerFactory implements RecyclerFactory {
+
+        /**
+         * Maximum number of objects retained per thread.
+         * <p>
+         * This allows to acquire objects in recursive method calls and maintain minimal overhead in the scenarios where the active instance count goes far beyond this for a brief moment.
+         * </p>
+         */
+        // Visible for testing
+        final int capacity;
+
+        private ThreadLocalRecyclerFactory(int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            requireNonNull(cleaner, "cleaner");
+            return new ThreadLocalRecycler<>(supplier, cleaner, capacity);
+        }
+
+        // Visible for testing
+        static final class ThreadLocalRecycler<V> extends AbstractRecycler<V> {
+
+            private final Consumer<V> cleaner;
+
+            // Visible for testing
+            final ThreadLocal<Queue<V>> queueRef;
+
+            private ThreadLocalRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final int capacity) {
+                super(supplier);
+                this.queueRef = ThreadLocal.withInitial(() -> new ArrayQueue<>(capacity));
+                this.cleaner = cleaner;
+            }
+
+            @Override
+            public V acquire() {
+                final Queue<V> queue = queueRef.get();
+                final V value = queue.poll();
+                return value != null ? value : createInstance();
+            }
+
+            @Override
+            public void release(final V value) {
+                requireNonNull(value, "value");
+                cleaner.accept(value);
+                final Queue<V> queue = queueRef.get();
+                queue.offer(value);
+            }
+        }
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java
index 50f457140e..21176cd0c5 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MessageFactory.java
@@ -16,7 +16,7 @@
  */
 package org.apache.logging.log4j.message;
 
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 
 /**
  * Creates messages. Implementations can provide different message format syntaxes.
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
index f980631224..aae12e6f37 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ParameterizedMessage.java
@@ -19,7 +19,7 @@ package org.apache.logging.log4j.message;
 import java.util.Arrays;
 import java.util.Objects;
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.StringBuilderFormattable;
 import org.apache.logging.log4j.util.StringBuilders;
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java
index a2523c2ae4..7abd8ac064 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableMessageFactory.java
@@ -17,8 +17,8 @@
 package org.apache.logging.log4j.message;
 
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 
 /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java
index 326da8dabe..928e201f1b 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/ReusableParameterizedMessage.java
@@ -18,8 +18,8 @@ package org.apache.logging.log4j.message;
 
 import java.util.Arrays;
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 import org.apache.logging.log4j.util.StringBuilders;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
index 2b5606714c..5dd788c06b 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractLogger.java
@@ -27,6 +27,8 @@ import org.apache.logging.log4j.message.FlowMessageFactory;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.StringFormattedMessage;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Cast;
 import org.apache.logging.log4j.util.LambdaUtil;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DummyRecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/DummyRecyclerFactory.java
deleted file mode 100644
index 0076cd49e6..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/DummyRecyclerFactory.java
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.spi;
-
-import static java.util.Objects.requireNonNull;
-
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * Recycler strategy which doesn't recycle anything; all instances are freshly created.
- *
- * @since 3.0.0
- */
-public final class DummyRecyclerFactory implements RecyclerFactory {
-
-    private static final DummyRecyclerFactory INSTANCE = new DummyRecyclerFactory();
-
-    private DummyRecyclerFactory() {}
-
-    public static DummyRecyclerFactory getInstance() {
-        return INSTANCE;
-    }
-
-    @Override
-    public <V> Recycler<V> create(final Supplier<V> supplier, final Consumer<V> cleaner) {
-        requireNonNull(supplier, "supplier");
-        return new DummyRecycler<>(supplier);
-    }
-
-    private static class DummyRecycler<V> extends AbstractRecycler<V> {
-
-        private DummyRecycler(final Supplier<V> supplier) {
-            super(supplier);
-        }
-
-        @Override
-        public V acquire() {
-            return createInstance();
-        }
-
-        @Override
-        public void release(final V value) {}
-    }
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java
index 08daa2e05e..46cf2ae91f 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystem.java
@@ -41,13 +41,14 @@ import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.ThreadContext;
-import org.apache.logging.log4j.internal.RecyclerFactories;
 import org.apache.logging.log4j.message.DefaultFlowMessageFactory;
 import org.apache.logging.log4j.message.FlowMessageFactory;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.message.ParameterizedMessageFactory;
 import org.apache.logging.log4j.message.ReusableMessageFactory;
 import org.apache.logging.log4j.simple.SimpleLoggerContextFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
 import org.apache.logging.log4j.util.Constants;
 import org.apache.logging.log4j.util.Lazy;
 import org.apache.logging.log4j.util.LoaderUtil;
@@ -105,7 +106,8 @@ public class LoggingSystem {
             environmentLazy.map(environment -> () -> getProvider().createContextMap(environment));
     private final Lazy<Supplier<ThreadContextStack>> threadContextStackFactoryLazy =
             environmentLazy.map(environment -> () -> getProvider().createContextStack(environment));
-    private final Lazy<RecyclerFactory> recyclerFactoryLazy = Lazy.lazy(() -> RecyclerFactories.INSTANCE);
+    private final Lazy<RecyclerFactory> recyclerFactoryLazy =
+            environmentLazy.map(RecyclerFactoryRegistry::findRecyclerFactory);
 
     public LoggingSystem() {}
 
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java
index b9d814cb15..63183ae763 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggingSystemProperty.java
@@ -190,7 +190,15 @@ public enum LoggingSystemProperty implements PropertyKey {
      * (from either {@code javax} or {@code jakarta}) is checked to see if this is a webapp.
      */
     // Web.isWebApp : calculate | true | false
-    IS_WEBAPP(PropertyComponent.WEB, Constant.IS_WEB_APP);
+    IS_WEBAPP(PropertyComponent.WEB, Constant.IS_WEB_APP),
+    /**
+     * Property to override the default recycler factory.
+     */
+    RECYCLER_FACTORY(PropertyComponent.RECYCLER, Constant.RECYCLER_FACTORY),
+    /**
+     * Property to override the default recycler capacity.
+     */
+    RECYCLER_CAPACITY(PropertyComponent.RECYCLER, Constant.RECYCLER_CAPACITY);
 
     public static final String SIMPLE_LOGGER_LOG_LEVEL = "SimpleLogger.%s.level";
     public static final String SYSTEM_PROPERTY_PREFIX = "log4j2.*.";
@@ -476,5 +484,8 @@ public enum LoggingSystemProperty implements PropertyKey {
         static final String IS_WEB_APP = "isWebApp";
         public static final String WEB =
                 LoggingSystemProperty.SYSTEM_PROPERTY_PREFIX + PropertyComponent.Constant.WEB + DELIM + IS_WEB_APP;
+
+        static final String RECYCLER_FACTORY = "factory";
+        static final String RECYCLER_CAPACITY = "capacity";
     }
 }
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java
index 8a748bc84a..5dd99ef77a 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/PropertyComponent.java
@@ -35,6 +35,7 @@ public enum PropertyComponent {
     LOGGER(Constant.LOGGER),
     LOGGER_CONTEXT(Constant.LOGGER_CONTEXT),
     MESSAGE(Constant.MESSAGE),
+    RECYCLER(Constant.RECYCLER),
     SCRIPT(Constant.SCRIPT),
     SIMPLE_LOGGER(Constant.SIMPLE_LOGGER),
     STATUS_LOGGER(Constant.STATUS_LOGGER),
@@ -74,6 +75,7 @@ public enum PropertyComponent {
         public static final String LOGGER = "Logger";
         public static final String LOGGER_CONTEXT = "LoggerContext";
         public static final String MESSAGE = "Message";
+        public static final String RECYCLER = "Recycler";
         public static final String SCRIPT = "Script";
         public static final String SIMPLE_LOGGER = "SimpleLogger";
         public static final String STATUS_LOGGER = "StatusLogger";
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/QueueFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/QueueFactory.java
deleted file mode 100644
index 6c41f637fd..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/QueueFactory.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.spi;
-
-import java.util.Queue;
-import org.apache.logging.log4j.internal.QueueFactories;
-
-/**
- * A {@link Queue} factory contract.
- *
- * @see QueueFactories
- * @since 3.0.0
- */
-@FunctionalInterface
-public interface QueueFactory {
-
-    <E> Queue<E> create(final int capacity);
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerFactory.java
deleted file mode 100644
index 96c9f010e5..0000000000
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerFactory.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.logging.log4j.spi;
-
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/**
- * Factory for {@link Recycler} strategies. Depending on workloads, different instance recycling strategies may be
- * most performant. For example, traditional multithreaded workloads may benefit from using thread-local instance
- * recycling while different models of concurrency or versions of the JVM may benefit from using an object pooling
- * strategy instead.
- *
- * @since 3.0.0
- */
-@FunctionalInterface
-public interface RecyclerFactory {
-
-    /**
-     * Creates a new recycler using the given supplier function for initial instances. These instances have
-     * no cleaner function and are assumed to always be reusable.
-     *
-     * @param supplier function to provide new instances of a recyclable object
-     * @param <V> the recyclable type
-     * @return a new recycler for V-type instances
-     */
-    default <V> Recycler<V> create(final Supplier<V> supplier) {
-        return create(supplier, ignored -> {});
-    }
-
-    /**
-     * Creates a new recycler using the given functions for providing fresh instances and for cleaning up
-     * existing instances for reuse. For example, a StringBuilder recycler would provide two functions:
-     * a supplier function for constructing a new StringBuilder with a preselected initial capacity and
-     * another function for trimming the StringBuilder to some preselected maximum capacity and setting
-     * its length back to 0 as if it were a fresh StringBuilder.
-     *
-     * @param supplier function to provide new instances of a recyclable object
-     * @param cleaner function to reset a recyclable object to a fresh state
-     * @param <V> the recyclable type
-     * @return a new recycler for V-type instances
-     */
-    <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner);
-}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractRecycler.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/AbstractRecycler.java
similarity index 80%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractRecycler.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/AbstractRecycler.java
index 6b74698a9e..84b5924f25 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/AbstractRecycler.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/AbstractRecycler.java
@@ -14,7 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.spi.recycler;
+
+import static java.util.Objects.requireNonNull;
 
 import java.util.function.Supplier;
 
@@ -22,15 +24,15 @@ public abstract class AbstractRecycler<V> implements Recycler<V> {
 
     private final Supplier<V> supplier;
 
-    public AbstractRecycler(final Supplier<V> supplier) {
-        this.supplier = supplier;
+    protected AbstractRecycler(final Supplier<V> supplier) {
+        this.supplier = requireNonNull(supplier, "supplier");
     }
 
     protected final V createInstance() {
         final V instance = supplier.get();
         if (instance instanceof RecyclerAware) {
             @SuppressWarnings("unchecked")
-            RecyclerAware<V> recyclerAware = (RecyclerAware<V>) instance;
+            final RecyclerAware<V> recyclerAware = (RecyclerAware<V>) instance;
             recyclerAware.setRecycler(this);
         }
         return instance;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Recycler.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/Recycler.java
similarity index 77%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/Recycler.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/Recycler.java
index 50cd53a471..4a9db5b0b0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/Recycler.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/Recycler.java
@@ -14,16 +14,22 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.spi.recycler;
 
 /**
- * Strategy for recycling objects. This is primarily useful for heavyweight objects and buffers.
+ * Contract for recycling strategies.
+ * This is the primary building block for logging components striving for garbage-free operation.
  *
  * @param <V> the recyclable type
  * @since 3.0.0
  */
 public interface Recycler<V> {
 
+    /**
+     * The default recycler capacity: {@code max(2C+1, 8)}, {@code C} denoting the number of available processors
+     */
+    int DEFAULT_CAPACITY = Math.max(2 * Runtime.getRuntime().availableProcessors() + 1, 8);
+
     /**
      * Acquires an instance of V. This may either be a fresh instance of V or a recycled instance of V.
      * Recycled instances will be modified by their cleanup function before being returned.
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerAware.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerAware.java
similarity index 95%
rename from log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerAware.java
rename to log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerAware.java
index 42e4d72163..4483ed3a02 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/spi/RecyclerAware.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerAware.java
@@ -14,7 +14,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.spi;
+package org.apache.logging.log4j.spi.recycler;
+
 /**
  * Interface implemented by classes that need to interact with the {@link Recycler} that created them.
  */
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactory.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactory.java
new file mode 100644
index 0000000000..bb935b1958
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactory.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.spi.recycler;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Contract for {@link Recycler} factories.
+ *
+ * @since 3.0.0
+ */
+public interface RecyclerFactory {
+
+    /**
+     * Creates a new recycler using the given supplier function for initial instances.
+     *
+     * @param supplier a function to provide initial instances
+     * @param <V> the recyclable type
+     * @return a new recycler
+     */
+    default <V> Recycler<V> create(final Supplier<V> supplier) {
+        return create(supplier, ignored -> {});
+    }
+
+    /**
+     * Creates a new recycler using the given supplier and cleaner functions.
+     * <p>
+     * The provided supplier needs to make sure that generated instances are always clean.
+     * </p>
+     * <p>
+     * Recycled instances are always guaranteed to be clean.
+     * The cleaning of an instance can take place either just before acquisition or prior to admitting it back into the reusable instances pool.
+     * The moment when the cleaning will be carried out is implementation dependent.
+     * Though a released instance should ideally be cleaned immediately to avoid keeping references to unused objects.
+     * </p>
+     *
+     * @param supplier a function to provide initial (and clean!) instances
+     * @param cleaner function to reset an instance before reuse
+     * @param <V> the recyclable type
+     * @return a new recycler
+     */
+    <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryProvider.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryProvider.java
new file mode 100644
index 0000000000..d049e2d01e
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.spi.recycler;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+
+/**
+ * Contract for providing {@link RecyclerFactory} instances.
+ *
+ * @since 3.0.0
+ */
+public interface RecyclerFactoryProvider {
+
+    /**
+     * Denotes the value to be used while sorting recycler factory providers to determine the precedence order.
+     * Values will be sorted naturally, that is, lower values will imply higher precedence.
+     *
+     * @return the value to be used while sorting
+     */
+    default int getOrder() {
+        return 100;
+    }
+
+    /**
+     * The name of this recycler factory provider.
+     * Recycler factory providers are required to have unique names.
+     *
+     * @return the name of this recycler factory provider
+     */
+    String getName();
+
+    /**
+     * Creates a recycler factory for the provided environment.
+     * <p>
+     * The return value can be null indicating that the recycler factory is not available for the provided environment.
+     * For instance, the provider of a {@link ThreadLocal}-based recycler factory can return null if the environment is of a web application.
+     * </p>
+     *
+     * @param environment an environment
+     * @return either a recycler factory instance, or null, if the associated recycler factory is not available for the given environment
+     */
+    @Nullable
+    RecyclerFactory createForEnvironment(PropertyEnvironment environment);
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryRegistry.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryRegistry.java
new file mode 100644
index 0000000000..ba17fc98e9
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/RecyclerFactoryRegistry.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.spi.recycler;
+
+import static java.util.Objects.requireNonNull;
+
+import aQute.bnd.annotation.spi.ServiceConsumer;
+import edu.umd.cs.findbugs.annotations.Nullable;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+import org.apache.logging.log4j.util.ServiceLoaderUtil;
+
+/**
+ * Registry of available {@link RecyclerFactoryProvider}s.
+ */
+@ServiceConsumer(RecyclerFactoryProvider.class)
+public final class RecyclerFactoryRegistry {
+
+    private static final Map<String, RecyclerFactoryProvider> PROVIDER_BY_NAME = loadProviders();
+
+    private static Map<String, RecyclerFactoryProvider> loadProviders() {
+        return ServiceLoaderUtil.safeStream(RecyclerFactoryProvider.class, null)
+                .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
+                .collect(Collectors.toMap(
+                        RecyclerFactoryProvider::getName,
+                        Function.identity(),
+                        (provider1, provider2) -> {
+                            final String message = String.format(
+                                    "recycler factory providers `%s` (order=%d) and `%s` (order=%d) have conflicting names: `%s`",
+                                    provider1.getClass().getCanonicalName(),
+                                    provider1.getOrder(),
+                                    provider2.getClass().getCanonicalName(),
+                                    provider2.getOrder(),
+                                    provider2.getName());
+                            throw new IllegalStateException(message);
+                        },
+                        LinkedHashMap::new));
+    }
+
+    private RecyclerFactoryRegistry() {}
+
+    public static Collection<RecyclerFactoryProvider> getRecyclerFactoryProviders() {
+        return PROVIDER_BY_NAME.values();
+    }
+
+    public static RecyclerFactory findRecyclerFactory(final PropertyEnvironment environment) {
+
+        // Check arguments
+        requireNonNull(environment, "environment");
+
+        // Short-circuit if there are no recycler factories available
+        if (PROVIDER_BY_NAME.isEmpty()) {
+            throw new IllegalStateException("couldn't find any recycler factories");
+        }
+
+        // If there is a particular recycler factory requested, get that done.
+        @Nullable final String name = environment.getStringProperty(LoggingSystemProperty.RECYCLER_FACTORY);
+        if (name != null) {
+            return findRecyclerFactoryByName(environment, name);
+        }
+
+        // Otherwise, find one available
+        return PROVIDER_BY_NAME.values().stream()
+                .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
+                .map(provider -> provider.createForEnvironment(environment))
+                .filter(Objects::nonNull)
+                .findFirst()
+                .orElseThrow(() -> {
+                    final String names = PROVIDER_BY_NAME.keySet().stream()
+                            .map(name_ -> '`' + name_ + '`')
+                            .collect(Collectors.joining(", "));
+                    final String message = String.format(
+                            "Couldn't find a recycler factory provider available for the provided environment. Names of the available recycler factory providers: %s",
+                            names);
+                    return new IllegalArgumentException(message);
+                });
+    }
+
+    private static RecyclerFactory findRecyclerFactoryByName(final PropertyEnvironment environment, final String name) {
+        @Nullable final RecyclerFactoryProvider provider = PROVIDER_BY_NAME.get(name);
+        if (provider == null) {
+            final String names = PROVIDER_BY_NAME.keySet().stream()
+                    .map(name_ -> '`' + name_ + '`')
+                    .collect(Collectors.joining(", "));
+            final String message = String.format(
+                    "Couldn't find a recycler factory provider of name `%s`. Names of the available recycler factory providers: %s",
+                    name, names);
+            throw new IllegalArgumentException(message);
+        }
+        @Nullable final RecyclerFactory factory = provider.createForEnvironment(environment);
+        if (factory == null) {
+            final String message = String.format(
+                    "failed to configure recycler factory of name `%s` for the provided environment", name);
+            throw new IllegalArgumentException(message);
+        }
+        return factory;
+    }
+}
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/package-info.java b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/package-info.java
new file mode 100644
index 0000000000..f45bb6e5af
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/spi/recycler/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+/**
+ * Internal interfaces and classes to be used by authors of logging implementations or for internal use by
+ * API classes.
+ */
+@Export
+@Version("3.0.0")
+package org.apache.logging.log4j.spi.recycler;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
index 7f0247ccef..f198bbee3e 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/status/StatusLogger.java
@@ -21,12 +21,12 @@ import java.io.IOException;
 import java.util.Collection;
 import java.util.List;
 import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.Marker;
-import org.apache.logging.log4j.internal.QueueFactories;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
 import org.apache.logging.log4j.simple.SimpleLogger;
@@ -63,7 +63,7 @@ public final class StatusLogger extends AbstractLogger {
 
     private final ReadWriteLock listenersLock = new ReentrantReadWriteLock();
 
-    private final Queue<StatusData> messages;
+    private final Queue<StatusData> messages = new ConcurrentLinkedQueue<>();
 
     private int listenersLevel;
 
@@ -97,7 +97,6 @@ public final class StatusLogger extends AbstractLogger {
         this.logger = logger;
         this.configuration = configuration;
         this.listenersLevel = configuration.getDefaultLevel().intLevel();
-        messages = QueueFactories.MPMC.create(configuration.getMaxEntries());
     }
 
     /**
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java b/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java
index b9be78fcd1..7fe952cbd0 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/util/ServiceLoaderUtil.java
@@ -16,6 +16,9 @@
  */
 package org.apache.logging.log4j.util;
 
+import static java.util.Objects.requireNonNull;
+
+import edu.umd.cs.findbugs.annotations.Nullable;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.ServiceConfigurationError;
@@ -36,7 +39,15 @@ public final class ServiceLoaderUtil {
 
     private ServiceLoaderUtil() {}
 
+    public static <S> Stream<S> safeStream(final Class<S> type, @Nullable final ClassLoader classLoader) {
+        requireNonNull(type, "type");
+        final ClassLoader effectiveLoader = classLoader != null ? classLoader : type.getClassLoader();
+        final ServiceLoader<S> serviceLoader = ServiceLoader.load(type, effectiveLoader);
+        return safeStream(serviceLoader);
+    }
+
     public static <S> Stream<S> safeStream(final ServiceLoader<S> serviceLoader) {
+        requireNonNull(serviceLoader, "serviceLoader");
         final Set<Class<?>> classes = new HashSet<>();
         return StreamSupport.stream(new ServiceLoaderSpliterator<>(serviceLoader), false)
                 // only the first occurrence of a class
diff --git a/log4j-core-its/pom.xml b/log4j-core-its/pom.xml
index c1dee9b916..2b38ca60b5 100644
--- a/log4j-core-its/pom.xml
+++ b/log4j-core-its/pom.xml
@@ -72,12 +72,6 @@
       <artifactId>jackson-dataformat-yaml</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-core-test/pom.xml b/log4j-core-test/pom.xml
index ec7e071ee0..bd3a339e2a 100644
--- a/log4j-core-test/pom.xml
+++ b/log4j-core-test/pom.xml
@@ -155,12 +155,6 @@
       <artifactId>jansi</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java
index b1aacc3e41..9631d9a2e0 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/appender/AsyncAppenderTest.java
@@ -118,14 +118,6 @@ public class AsyncAppenderTest {
         assertEquals("STDOUT", appender.getErrorRef());
     }
 
-    @Test
-    @Tag("jctools")
-    @LoggerContextSource("BlockingQueueFactory-JCToolsBlockingQueue.xml")
-    public void testJcToolsBlockingQueue(final LoggerContext context) throws InterruptedException {
-        rewriteTest(context);
-        exceptionTest(context);
-    }
-
     @Test
     @LoggerContextSource("BlockingQueueFactory-LinkedTransferQueue.xml")
     public void testLinkedTransferQueue(final LoggerContext context) throws InterruptedException {
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index fcdfffd644..bc33f87910 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -40,7 +40,6 @@
       com.lmax.disruptor.*;resolution:=optional,
       org.apache.commons.compress.*;resolution:=optional,
       org.fusesource.jansi;resolution:=optional,
-      org.jctools.*;resolution:=optional,
       <!-- Optional Java modules -->
       <!-- java.management -->
       java.lang.management;resolution:=optional,
@@ -134,12 +133,6 @@
       <artifactId>jansi</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
   </dependencies>
   <build>
     <plugins>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
index d5a2c6f168..0d93461512 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Configuration.java
@@ -39,7 +39,7 @@ import org.apache.logging.log4j.core.util.NetUtils;
 import org.apache.logging.log4j.core.util.WatchManager;
 import org.apache.logging.log4j.plugins.Node;
 import org.apache.logging.log4j.plugins.di.Key;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.PropertyEnvironment;
 
 /**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
index 408d0845d3..2a21b56b22 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/StructuredDataFilter.java
@@ -35,8 +35,8 @@ import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAttribute;
 import org.apache.logging.log4j.plugins.PluginElement;
 import org.apache.logging.log4j.plugins.PluginFactory;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.IndexedReadOnlyStringMap;
 import org.apache.logging.log4j.util.PerformanceSensitive;
 import org.apache.logging.log4j.util.StringBuilders;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
index d4ae74c892..7d9091071b 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/DefaultBundle.java
@@ -45,6 +45,7 @@ import org.apache.logging.log4j.plugins.SingletonFactory;
 import org.apache.logging.log4j.plugins.condition.ConditionalOnMissingBinding;
 import org.apache.logging.log4j.plugins.di.ConfigurableInstanceFactory;
 import org.apache.logging.log4j.spi.*;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * Provides instance binding defaults.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java
index 4e64b8c2cc..44e442eb52 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/MutableLogEvent.java
@@ -34,7 +34,7 @@ import org.apache.logging.log4j.message.ParameterVisitable;
 import org.apache.logging.log4j.message.ReusableMessage;
 import org.apache.logging.log4j.message.SimpleMessage;
 import org.apache.logging.log4j.message.TimestampMessage;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.StackLocatorUtil;
 import org.apache.logging.log4j.util.StringBuilders;
 import org.apache.logging.log4j.util.StringMap;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
index 1ac3f5dd89..6f40131165 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/impl/ReusableLogEventFactory.java
@@ -29,8 +29,8 @@ import org.apache.logging.log4j.core.time.Clock;
 import org.apache.logging.log4j.core.time.NanoClock;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.plugins.Inject;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * Garbage-free LogEventFactory that recycles mutable {@link LogEvent} instances.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java
index dceca48d88..569e3873f0 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/AbstractStringLayout.java
@@ -24,8 +24,8 @@ import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.impl.Log4jPropertyKey;
 import org.apache.logging.log4j.core.impl.LogEventFactory;
 import org.apache.logging.log4j.plugins.PluginElement;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.StringBuilders;
 
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
index f2fe28baa5..09354f7902 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/PatternLayout.java
@@ -36,7 +36,7 @@ import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginBuilderAttribute;
 import org.apache.logging.log4j.plugins.PluginElement;
 import org.apache.logging.log4j.plugins.PluginFactory;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.PropertiesUtil;
 import org.apache.logging.log4j.util.PropertyEnvironment;
 import org.apache.logging.log4j.util.Strings;
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java
index e61083552d..360dd56e06 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/layout/StringBuilderEncoder.java
@@ -23,7 +23,7 @@ import java.nio.charset.CharsetEncoder;
 import java.nio.charset.CodingErrorAction;
 import java.util.Objects;
 import org.apache.logging.log4j.core.util.Constants;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * {@link Encoder} for {@link StringBuilder}s.
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java
index e2a3c74213..a60d6e4c9c 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/JsonUtils.java
@@ -17,7 +17,7 @@
 package org.apache.logging.log4j.core.util;
 
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.Lazy;
 
 /**
diff --git a/log4j-gctests/pom.xml b/log4j-gctests/pom.xml
index a461b5fff7..988808c891 100644
--- a/log4j-gctests/pom.xml
+++ b/log4j-gctests/pom.xml
@@ -99,12 +99,6 @@
       <artifactId>jansi</artifactId>
       <optional>true</optional>
     </dependency>
-    <!-- Alternative implementation of BlockingQueue using JCTools for AsyncAppender -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-api/pom.xml b/log4j-jctools/pom.xml
similarity index 59%
copy from log4j-api/pom.xml
copy to log4j-jctools/pom.xml
index 2ea9e66998..e5e8c01b76 100644
--- a/log4j-api/pom.xml
+++ b/log4j-jctools/pom.xml
@@ -15,44 +15,62 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
   <modelVersion>4.0.0</modelVersion>
+
   <parent>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j</artifactId>
     <version>${revision}</version>
     <relativePath>../log4j-parent</relativePath>
   </parent>
-  <artifactId>log4j-api</artifactId>
-  <packaging>jar</packaging>
-  <name>Apache Log4j API</name>
-  <description>The Apache Log4j API</description>
+
+  <artifactId>log4j-jctools</artifactId>
+  <name>Apache Log4j JCTools-based supplements</name>
+  <description>Provides JCTools-based data structure implementations for the Apache Log4j.</description>
+
   <properties>
-    <maven.javadoc.skip>false</maven.javadoc.skip>
+
+    <log4jParentDir>${basedir}/..</log4jParentDir>
 
     <!--
       ~ OSGi and JPMS options
       -->
-    <bnd-module-name>org.apache.logging.log4j</bnd-module-name>
-    <bnd-extra-package-options>
-      <!-- No JSR 305 at runtime -->
-      !javax.annotation.*,
-      <!-- Optional dependencies -->
-      org.jctools.*;resolution:=optional
-    </bnd-extra-package-options>
-    <bnd-extra-module-options>
-      <!-- No JSR 305 at runtime -->
-      jsr305;ignore=true,
-      <!-- Used in StringBuilders through reflection -->
-      java.sql;static=true
-    </bnd-extra-module-options>
+    <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host>
 
   </properties>
+
   <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>org.jctools</groupId>
       <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
     </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
+
 </project>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/JCToolsBlockingQueueFactory.java b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactory.java
similarity index 95%
rename from log4j-core/src/main/java/org/apache/logging/log4j/core/async/JCToolsBlockingQueueFactory.java
rename to log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactory.java
index 188d49bb5b..d35801cdd6 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/async/JCToolsBlockingQueueFactory.java
+++ b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactory.java
@@ -14,12 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.async;
+package org.apache.logging.log4j.jctools;
 
 import java.util.Collection;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.LockSupport;
+import org.apache.logging.log4j.core.async.BlockingQueueFactory;
 import org.apache.logging.log4j.plugins.Configurable;
 import org.apache.logging.log4j.plugins.Plugin;
 import org.apache.logging.log4j.plugins.PluginAttribute;
@@ -27,13 +28,13 @@ import org.apache.logging.log4j.plugins.PluginFactory;
 import org.jctools.queues.MpscArrayQueue;
 
 /**
- * Factory for creating instances of BlockingQueues backed by JCTools {@link MpscArrayQueue}.
+ * A {@link BlockingQueueFactory} based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
  *
- * @since 2.7
+ * @since 3.0.0
  */
 @Configurable(elementType = BlockingQueueFactory.ELEMENT_TYPE, printObject = true)
 @Plugin("JCToolsBlockingQueue")
-public class JCToolsBlockingQueueFactory implements BlockingQueueFactory {
+public final class JCToolsBlockingQueueFactory implements BlockingQueueFactory {
 
     private final WaitStrategy waitStrategy;
 
diff --git a/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProvider.java b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProvider.java
new file mode 100644
index 0000000000..321b7e9983
--- /dev/null
+++ b/log4j-jctools/src/main/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProvider.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.jctools;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.logging.log4j.spi.recycler.Recycler.DEFAULT_CAPACITY;
+
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.util.Queue;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.spi.LoggingSystemProperty;
+import org.apache.logging.log4j.spi.recycler.AbstractRecycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.util.PropertyEnvironment;
+import org.jctools.queues.MpmcArrayQueue;
+
+/**
+ * A {@link Recycler} factory provider implementation based on <a href="https://jctools.github.io/JCTools/">JCTools</a>.
+ *
+ * @since 3.0.0
+ */
+@ServiceProvider(RecyclerFactoryProvider.class)
+public final class JCToolsRecyclerFactoryProvider implements RecyclerFactoryProvider {
+
+    @Override
+    public int getOrder() {
+        return 600;
+    }
+
+    @Override
+    public String getName() {
+        return "jctools-mpmc";
+    }
+
+    @Override
+    public RecyclerFactory createForEnvironment(final PropertyEnvironment environment) {
+        requireNonNull(environment, "environment");
+        final int capacity = environment.getIntegerProperty(LoggingSystemProperty.RECYCLER_CAPACITY, DEFAULT_CAPACITY);
+        if (capacity < 1) {
+            throw new IllegalArgumentException("was expecting a `capacity` greater than 1, found: " + capacity);
+        }
+        return new JCToolsMpmcRecyclerFactory(capacity);
+    }
+
+    private static final class JCToolsMpmcRecyclerFactory implements RecyclerFactory {
+
+        private final int capacity;
+
+        private JCToolsMpmcRecyclerFactory(final int capacity) {
+            this.capacity = capacity;
+        }
+
+        @Override
+        public <V> Recycler<V> create(Supplier<V> supplier, Consumer<V> cleaner) {
+            requireNonNull(supplier, "supplier");
+            requireNonNull(cleaner, "cleaner");
+            final MpmcArrayQueue<V> queue = new MpmcArrayQueue<>(capacity);
+            return new JCToolsMpmcRecycler<>(supplier, cleaner, queue);
+        }
+
+        private static final class JCToolsMpmcRecycler<V> extends AbstractRecycler<V> {
+
+            private final Consumer<V> cleaner;
+
+            private final Queue<V> queue;
+
+            private JCToolsMpmcRecycler(final Supplier<V> supplier, final Consumer<V> cleaner, final Queue<V> queue) {
+                super(supplier);
+                this.cleaner = cleaner;
+                this.queue = queue;
+            }
+
+            @Override
+            public V acquire() {
+                final V value = queue.poll();
+                return value != null ? value : createInstance();
+            }
+
+            @Override
+            public void release(final V value) {
+                requireNonNull(value, "value");
+                cleaner.accept(value);
+                queue.offer(value);
+            }
+        }
+    }
+}
diff --git a/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactoryTest.java b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactoryTest.java
new file mode 100644
index 0000000000..03e96f05ba
--- /dev/null
+++ b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsBlockingQueueFactoryTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.logging.log4j.jctools;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.LoggingException;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AsyncAppender;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+import org.junit.jupiter.api.Test;
+
+class JCToolsBlockingQueueFactoryTest {
+
+    private static void exceptionTest(final LoggerContext context) throws InterruptedException {
+        final ExtendedLogger logger = context.getLogger(AsyncAppender.class);
+        final Exception parent = new IllegalStateException("Test");
+        final Throwable child = new LoggingException("This is a test", parent);
+        logger.error("This is a test", child);
+        final ListAppender appender = context.getConfiguration().getAppender("List");
+        final List<String> messages;
+        try {
+            messages = appender.getMessages(1, 2, TimeUnit.SECONDS);
+        } finally {
+            appender.clear();
+        }
+        assertNotNull(messages);
+        assertEquals(1, messages.size());
+        assertTrue(messages.get(0).contains(parent.getClass().getName()));
+    }
+
+    private static void rewriteTest(final LoggerContext context) throws InterruptedException {
+        final ExtendedLogger logger = context.getLogger(AsyncAppender.class);
+        logger.error("This is a test");
+        logger.warn("Hello world!");
+        final ListAppender appender = context.getConfiguration().getAppender("List");
+        final List<String> messages;
+        try {
+            messages = appender.getMessages(2, 2, TimeUnit.SECONDS);
+        } finally {
+            appender.clear();
+        }
+        assertNotNull(messages);
+        assertEquals(2, messages.size());
+        final String messagePrefix = JCToolsBlockingQueueFactoryTest.class.getName() + " rewriteTest ";
+        assertEquals(messagePrefix + "This is a test", messages.get(0));
+        assertEquals(messagePrefix + "Hello world!", messages.get(1));
+    }
+
+    @Test
+    @LoggerContextSource("JCToolsBlockingQueueFactoryTest.xml")
+    public void testJcToolsBlockingQueue(final LoggerContext context) throws InterruptedException {
+        rewriteTest(context);
+        exceptionTest(context);
+    }
+}
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/perftest/RunJCTools.java b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProviderTest.java
similarity index 50%
rename from log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/perftest/RunJCTools.java
rename to log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProviderTest.java
index 58622a938a..51e9d9e272 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/perftest/RunJCTools.java
+++ b/log4j-jctools/src/test/java/org/apache/logging/log4j/jctools/JCToolsRecyclerFactoryProviderTest.java
@@ -14,16 +14,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.async.perftest;
+package org.apache.logging.log4j.jctools;
 
-import java.util.concurrent.BlockingQueue;
-import org.apache.logging.log4j.core.async.JCToolsBlockingQueueFactory;
-import org.apache.logging.log4j.core.async.JCToolsBlockingQueueFactory.WaitStrategy;
+import static org.assertj.core.api.Assertions.assertThat;
 
-public class RunJCTools extends AbstractRunQueue {
+import java.util.Comparator;
+import java.util.List;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactoryRegistry;
+import org.junit.jupiter.api.Test;
 
-    @Override
-    BlockingQueue<String> createQueue(final int capacity) {
-        return JCToolsBlockingQueueFactory.createFactory(WaitStrategy.SPIN).create(capacity);
+class JCToolsRecyclerFactoryProviderTest {
+
+    @Test
+    void verify_is_the_first() {
+        final List<Class<?>> providerClasses = RecyclerFactoryRegistry.getRecyclerFactoryProviders().stream()
+                .sorted(Comparator.comparing(RecyclerFactoryProvider::getOrder))
+                .<Class<?>>map(RecyclerFactoryProvider::getClass)
+                .toList();
+        assertThat(providerClasses).startsWith(JCToolsRecyclerFactoryProvider.class);
     }
 }
diff --git a/log4j-core-test/src/test/resources/BlockingQueueFactory-JCToolsBlockingQueue.xml b/log4j-jctools/src/test/resources/JCToolsBlockingQueueFactoryTest.xml
similarity index 95%
rename from log4j-core-test/src/test/resources/BlockingQueueFactory-JCToolsBlockingQueue.xml
rename to log4j-jctools/src/test/resources/JCToolsBlockingQueueFactoryTest.xml
index c53500891b..1b6ff95acb 100644
--- a/log4j-core-test/src/test/resources/BlockingQueueFactory-JCToolsBlockingQueue.xml
+++ b/log4j-jctools/src/test/resources/JCToolsBlockingQueueFactoryTest.xml
@@ -15,7 +15,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<Configuration status="OFF" name="JCToolsBlockingQueueFactory">
+<Configuration status="OFF" name="JCToolsBlockingQueueFactoryTest">
 
   <Appenders>
     <Console name="STDOUT">
diff --git a/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider b/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
new file mode 100644
index 0000000000..25c8b92149
--- /dev/null
+++ b/log4j-jctools/src/test/resources/META-INF/services/org.apache.logging.log4j.spi.recycler.RecyclerFactoryProvider
@@ -0,0 +1,6 @@
+# `META-INF/services/*` files are automatically generated by `bnd-maven-plugin`.
+# We use `bnd:jar` goal, which only places them into the JAR.
+# As a result, tests of the same artifact (running against `target/` contents, not the JAR) don't have them.
+# To mitigate this, we manually create this file here only for tests.
+# `logging-parent` version `10.5.0` will switch from `bnd:jar` to `bnd:bnd-process`, then this hack won't be needed.
+org.apache.logging.log4j.jctools.JCToolsRecyclerFactoryProvider
diff --git a/log4j-layout-template-json-test/pom.xml b/log4j-layout-template-json-test/pom.xml
index 7a24826ad0..f0945107da 100644
--- a/log4j-layout-template-json-test/pom.xml
+++ b/log4j-layout-template-json-test/pom.xml
@@ -78,12 +78,6 @@
       <artifactId>java-allocation-instrumenter</artifactId>
       <scope>test</scope>
     </dependency>
-    <!-- needed for `RecyclerFactoriesTest` -->
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-engine</artifactId>
diff --git a/log4j-layout-template-json-test/src/test/resources/recyclerFactoryCustomizedJsonTemplateLayoutLogging.xml b/log4j-layout-template-json-test/src/test/resources/recyclerFactoryCustomizedJsonTemplateLayoutLogging.xml
deleted file mode 100644
index 63936384cc..0000000000
--- a/log4j-layout-template-json-test/src/test/resources/recyclerFactoryCustomizedJsonTemplateLayoutLogging.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  ~ Licensed to the Apache Software Foundation (ASF) under one or more
-  ~ contributor license agreements.  See the NOTICE file distributed with
-  ~ this work for additional information regarding copyright ownership.
-  ~ The ASF licenses this file to you under the Apache License, Version 2.0
-  ~ (the "License"); you may not use this file except in compliance with
-  ~ the License.  You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-<Configuration status="OFF">
-  <Appenders>
-    <List name="List" raw="true">
-      <JsonTemplateLayout
-          recyclerFactory="queue:supplier=org.jctools.queues.MpmcArrayQueue.new,capacity=512"/>
-    </List>
-  </Appenders>
-  <Loggers>
-    <Root level="TRACE">
-      <AppenderRef ref="List"/>
-    </Root>
-  </Loggers>
-</Configuration>
diff --git a/log4j-layout-template-json/pom.xml b/log4j-layout-template-json/pom.xml
index 651512771e..6279900cd6 100644
--- a/log4j-layout-template-json/pom.xml
+++ b/log4j-layout-template-json/pom.xml
@@ -52,11 +52,6 @@
       <groupId>org.apache.logging.log4j</groupId>
       <artifactId>log4j-plugins</artifactId>
     </dependency>
-    <dependency>
-      <groupId>org.jctools</groupId>
-      <artifactId>jctools-core</artifactId>
-      <optional>true</optional>
-    </dependency>
   </dependencies>
 
   <build>
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
index 30a668b311..f8d92fd4d5 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/JsonTemplateLayout.java
@@ -34,7 +34,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.layout.template.json.util.Uris;
 import org.apache.logging.log4j.plugins.*;
 import org.apache.logging.log4j.plugins.di.Key;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.apache.logging.log4j.util.Strings;
 
 @Configurable(elementType = Layout.ELEMENT_TYPE)
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
index eebc2e9f2b..77a80af09c 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/CounterResolver.java
@@ -23,7 +23,7 @@ import java.util.concurrent.locks.LockSupport;
 import java.util.function.Consumer;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 
 /**
  * Resolves a number from an internal counter.
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
index 27db91b743..0f7fc723cc 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/MessageParameterResolver.java
@@ -21,7 +21,7 @@ import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.ParameterConsumer;
 import org.apache.logging.log4j.message.ParameterVisitable;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 
 /**
  * {@link Message} parameter (i.e., {@link Message#getParameters()}) resolver.
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
index f6da114660..64356bfe2e 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/ReadOnlyStringMapResolver.java
@@ -22,8 +22,8 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 import org.apache.logging.log4j.util.ReadOnlyStringMap;
 import org.apache.logging.log4j.util.TriConsumer;
 
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
index 451c190e97..8d505eff8f 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/resolver/StackTraceStringResolver.java
@@ -25,8 +25,8 @@ import java.util.stream.Collectors;
 import org.apache.logging.log4j.layout.template.json.util.CharSequencePointer;
 import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
 import org.apache.logging.log4j.layout.template.json.util.TruncatingBufferedPrintWriter;
-import org.apache.logging.log4j.spi.Recycler;
-import org.apache.logging.log4j.spi.RecyclerFactory;
+import org.apache.logging.log4j.spi.recycler.Recycler;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
 
 /**
  * Exception stack trace to JSON string resolver used by {@link ExceptionResolver}.
diff --git a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
index 403aeaa09d..58ad4360fd 100644
--- a/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
+++ b/log4j-plugins/src/main/java/org/apache/logging/log4j/plugins/convert/TypeConverterFactory.java
@@ -23,11 +23,9 @@ import java.util.Map;
 import java.util.UnknownFormatConversionException;
 import java.util.concurrent.ConcurrentHashMap;
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.internal.RecyclerFactories;
 import org.apache.logging.log4j.plugins.Inject;
 import org.apache.logging.log4j.plugins.Singleton;
 import org.apache.logging.log4j.plugins.util.TypeUtil;
-import org.apache.logging.log4j.spi.RecyclerFactory;
 import org.apache.logging.log4j.status.StatusLogger;
 import org.apache.logging.log4j.util.Cast;
 import org.apache.logging.log4j.util.EnglishEnums;
@@ -60,7 +58,6 @@ public class TypeConverterFactory {
         registerTypeAlias(Integer.class, Integer.TYPE);
         registerTypeConverter(Long.class, Long::valueOf);
         registerTypeAlias(Long.class, Long.TYPE);
-        registerTypeConverter(RecyclerFactory.class, RecyclerFactories::ofSpec);
         registerTypeConverter(Short.class, Short::valueOf);
         registerTypeAlias(Short.class, Short.TYPE);
         registerTypeConverter(String.class, s -> s);
diff --git a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
index 64be7b447c..919cf2dee5 100644
--- a/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
+++ b/log4j-to-slf4j/src/main/java/org/apache/logging/slf4j/SLF4JLogger.java
@@ -24,7 +24,7 @@ import org.apache.logging.log4j.message.Message;
 import org.apache.logging.log4j.message.MessageFactory;
 import org.apache.logging.log4j.spi.AbstractLogger;
 import org.apache.logging.log4j.spi.LoggingSystem;
-import org.apache.logging.log4j.spi.Recycler;
+import org.apache.logging.log4j.spi.recycler.Recycler;
 import org.slf4j.LoggerFactory;
 import org.slf4j.MarkerFactory;
 import org.slf4j.spi.LocationAwareLogger;
diff --git a/pom.xml b/pom.xml
index 3dcdfa03ce..92041a027c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -245,6 +245,7 @@
     <module>log4j-gctests</module>
     <module>log4j-iostreams</module>
     <module>log4j-jcl</module>
+    <module>log4j-jctools</module>
     <module>log4j-jdbc</module>
     <module>log4j-jdbc-dbcp2</module>
     <module>log4j-jndi</module>
@@ -410,6 +411,12 @@
         <version>${project.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jctools</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-jdbc</artifactId>
diff --git a/src/site/asciidoc/manual/appenders.adoc b/src/site/asciidoc/manual/appenders.adoc
index c2127cf344..5256b8adab 100644
--- a/src/site/asciidoc/manual/appenders.adoc
+++ b/src/site/asciidoc/manual/appenders.adoc
@@ -192,6 +192,7 @@ attribute, `spinPolicy`, which corresponds to the `SpinPolicy` enum.
 |JCToolsBlockingQueue |This uses
 https://jctools.github.io/JCTools/[JCTools], specifically the MPSC
 bounded lock-free queue.
+This implementation is provided by the `log4j-jctools` artifact.
 
 |LinkedTransferQueue |This uses the new in Java 7 implementation
 https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/LinkedTransferQueue.html[`LinkedTransferQueue`].
diff --git a/src/site/asciidoc/runtime-dependencies.adoc b/src/site/asciidoc/runtime-dependencies.adoc
index e46761edc7..e98983a2a6 100644
--- a/src/site/asciidoc/runtime-dependencies.adoc
+++ b/src/site/asciidoc/runtime-dependencies.adoc
@@ -182,8 +182,6 @@ The module info definition may be modified in the future to export these only to
 !
 !requires static com.lmax.disruptor
 !
-!requires static org.jctools.core
-!
 !requires static org.osgi.framework
 !
 !requires static com.conversantmedia.disruptor
@@ -312,8 +310,6 @@ The module info definition may be modified in the future to export these only to
 !
 !requires org.apache.logging.log4j.core
 !
-!requires org.jctools.core
-!
 !===
 
 |log4j-plugin-processor