[main] Port `InternalLoggerRegistry` from `2.x` (#3418 and #3681 ports) (#4157)

* Port `InternalLoggerRegistry` from `2.x`

Minimizes lock usage by moving logger instantiation outside the write lock (PR #3418). Adds stale entry detection via ReferenceQueue (PR #3681).

Signed-off-by: Vasily Pelikh <2010720+vpelikh@users.noreply.github.com>

* Apply spotless formatting to InternalLoggerRegistryTest

* Fix InternalLoggerRegistry null-MessageFactory lookup mismatch in Log4j 3

InternalLoggerRegistry.getLogger(name, null) normalized null to a hardcoded ParameterizedMessageFactory.INSTANCE, but LoggerContext stored loggers under its DI-injected defaultMessageFactory (ReusableMessageFactory in Log4j 3). This store-key/get-key mismatch caused testGetLoggerRetrievesExistingLogger, testHasLoggerReturnsCorrectStatus, and testExpungeStaleWeakReferenceEntries to all fail.

- InternalLoggerRegistry: constructor-inject defaultMessageFactory (final field), resolve null→defaultMessageFactory instead of hardcoded INSTANCE.
- LoggerContext: construct InternalLoggerRegistry with its own defaultMessageFactory after DI resolution.
- Test: iterate all message-factory buckets (.values()) instead of assuming ParameterizedMessageFactory.INSTANCE.

* InternalLoggerRegistry: add Javadoc, align with 2.x naming, fix return-in-finally, add expungeStaleEntries

---------

Signed-off-by: Vasily Pelikh <2010720+vpelikh@users.noreply.github.com>
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistryTest.java
new file mode 100644
index 0000000..28ca886
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistryTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.core.util.internal;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.util.Map;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.SimpleMessageFactory;
+import org.apache.logging.log4j.plugins.di.DI;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+class InternalLoggerRegistryTest {
+    private LoggerContext loggerContext;
+    private InternalLoggerRegistry registry;
+
+    @BeforeEach
+    void setUp(final TestInfo testInfo) throws Exception {
+        loggerContext = new LoggerContext(testInfo.getDisplayName(), null, (URI) null, DI.createInitializedFactory());
+        final Field registryField = LoggerContext.class.getDeclaredField("loggerRegistry");
+        registryField.setAccessible(true);
+        registry = (InternalLoggerRegistry) registryField.get(loggerContext);
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (loggerContext != null) {
+            loggerContext.stop();
+        }
+    }
+
+    @Test
+    void testGetLoggerReturnsNullForNonExistentLogger() {
+        assertNull(registry.getLogger("nonExistent", null));
+    }
+
+    @Test
+    void testComputeIfAbsentCreatesLogger() {
+        final Logger logger = loggerContext.getLogger("testLogger", null);
+        assertNotNull(logger);
+        assertEquals("testLogger", logger.getName());
+    }
+
+    @Test
+    void testGetLoggerRetrievesExistingLogger() {
+        final Logger logger = loggerContext.getLogger("testLogger", null);
+        assertSame(logger, registry.getLogger("testLogger", null));
+    }
+
+    @Test
+    void testHasLoggerReturnsCorrectStatus() {
+        assertFalse(registry.hasLogger("testLogger", (MessageFactory) null));
+        loggerContext.getLogger("testLogger", null);
+        assertTrue(registry.hasLogger("testLogger", (MessageFactory) null));
+    }
+
+    @Test
+    void testExpungeStaleWeakReferenceEntries() throws Exception {
+        final String loggerNamePrefix = "testLogger_";
+        final int numberOfLoggers = 1000;
+
+        for (int i = 0; i < numberOfLoggers; i++) {
+            final Logger logger = loggerContext.getLogger(loggerNamePrefix + i, null);
+            logger.info("Using logger {}", logger.getName());
+        }
+
+        await().atMost(10, SECONDS).pollInterval(100, MILLISECONDS).untilAsserted(() -> {
+            System.gc();
+            registry.getLogger("triggerExpunge", null);
+
+            final Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerRefByNameByMessageFactory =
+                    reflectAndGetLoggerMapFromRegistry();
+
+            int unexpectedCount = 0;
+            for (final Map<String, WeakReference<Logger>> loggerRefByName : loggerRefByNameByMessageFactory.values()) {
+                for (int i = 0; i < numberOfLoggers; i++) {
+                    if (loggerRefByName.containsKey(loggerNamePrefix + i)) {
+                        unexpectedCount++;
+                    }
+                }
+            }
+            assertEquals(
+                    0, unexpectedCount, "Found " + unexpectedCount + " unexpected stale entries for MessageFactory");
+        });
+    }
+
+    @Test
+    void testExpungeStaleMessageFactoryEntry() throws Exception {
+        final SimpleMessageFactory mockMessageFactory = new SimpleMessageFactory();
+        Logger logger = loggerContext.getLogger("testLogger", mockMessageFactory);
+        logger.info("Using logger {}", logger.getName());
+        logger = null;
+
+        await().atMost(10, SECONDS).pollInterval(100, MILLISECONDS).untilAsserted(() -> {
+            System.gc();
+            registry.getLogger("triggerExpunge", null);
+
+            final Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerRefByNameByMessageFactory =
+                    reflectAndGetLoggerMapFromRegistry();
+            assertNull(
+                    loggerRefByNameByMessageFactory.get(mockMessageFactory),
+                    "Stale MessageFactory entry was not removed from the outer map");
+        });
+    }
+
+    private Map<MessageFactory, Map<String, WeakReference<Logger>>> reflectAndGetLoggerMapFromRegistry()
+            throws NoSuchFieldException, IllegalAccessException {
+        final Field loggerMapField = InternalLoggerRegistry.class.getDeclaredField("loggerRefByNameByMessageFactory");
+        loggerMapField.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        final Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerMap =
+                (Map<MessageFactory, Map<String, WeakReference<Logger>>>) loggerMapField.get(registry);
+        return loggerMap;
+    }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index 4b5d3dd..1acbff7 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -51,6 +51,7 @@
 import org.apache.logging.log4j.core.util.ExecutorServices;
 import org.apache.logging.log4j.core.util.NetUtils;
 import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
+import org.apache.logging.log4j.core.util.internal.InternalLoggerRegistry;
 import org.apache.logging.log4j.kit.env.PropertyEnvironment;
 import org.apache.logging.log4j.kit.env.internal.ContextualEnvironmentPropertySource;
 import org.apache.logging.log4j.kit.env.internal.ContextualJavaPropsPropertySource;
@@ -88,8 +89,8 @@
 
     public static final Key<LoggerContext> KEY = Key.forClass(LoggerContext.class);
 
-    private final LoggerRegistry<Logger> loggerRegistry = new LoggerRegistry<>();
     private final MessageFactory defaultMessageFactory;
+    private final InternalLoggerRegistry loggerRegistry;
 
     private final Collection<Consumer<Configuration>> configurationStartedListeners = new ArrayList<>();
     private final Collection<Consumer<Configuration>> configurationStoppedListeners = new ArrayList<>();
@@ -147,6 +148,7 @@
         this.environment = instanceFactory.getInstance(PropertyEnvironment.class);
         this.configurationScheduler = instanceFactory.getInstance(ConfigurationScheduler.class);
         this.defaultMessageFactory = instanceFactory.getInstance(MessageFactory.class);
+        this.loggerRegistry = new InternalLoggerRegistry(this.defaultMessageFactory);
 
         this.configuration = new DefaultConfiguration(this);
         this.nullConfiguration = new NullConfiguration(this);
@@ -571,15 +573,9 @@
     @Override
     public Logger getLogger(final String name, final MessageFactory messageFactory) {
         final MessageFactory actualMessageFactory = messageFactory != null ? messageFactory : defaultMessageFactory;
-        // Note: This is the only method where we add entries to the 'loggerRegistry' ivar.
-        Logger logger = loggerRegistry.getLogger(name, actualMessageFactory);
-        if (logger != null) {
-            checkMessageFactory(logger, actualMessageFactory);
-            return logger;
-        }
-        logger = newLogger(name, actualMessageFactory);
-        loggerRegistry.putIfAbsent(name, logger.getMessageFactory(), logger);
-        return loggerRegistry.getLogger(name, logger.getMessageFactory());
+        final Logger logger = loggerRegistry.computeIfAbsent(name, actualMessageFactory, this::newLogger);
+        checkMessageFactory(logger, actualMessageFactory);
+        return logger;
     }
 
     /**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistry.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistry.java
new file mode 100644
index 0000000..0f7aa95
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/InternalLoggerRegistry.java
@@ -0,0 +1,353 @@
+/*
+ * 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.core.util.internal;
+
+import static java.util.Objects.requireNonNull;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.spi.LoggerRegistry;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * A registry of {@link Logger}s namespaced by name and message factory using {@link WeakReference}s.
+ * <p>
+ * This class extends {@link LoggerRegistry} to provide garbage-free logger tracking
+ * with minimal lock contention. Loggers are created outside the write lock to prevent
+ * deadlocks and improve concurrency.
+ * </p>
+ * @since 3.0.0
+ */
+@NullMarked
+public class InternalLoggerRegistry extends LoggerRegistry<Logger> {
+
+    private final Map<MessageFactory, Map<String, WeakReference<Logger>>> loggerRefByNameByMessageFactory =
+            new WeakHashMap<>();
+
+    private final ReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private final Lock writeLock = lock.writeLock();
+
+    // ReferenceQueue to track stale WeakReferences
+    private final ReferenceQueue<Logger> staleLoggerRefs = new ReferenceQueue<>();
+
+    private final MessageFactory defaultMessageFactory;
+
+    /**
+     * Constructs a registry whose default message factory (used when a lookup is requested with a
+     * {@code null} message factory) matches the one a {@link org.apache.logging.log4j.spi.LoggerContext}
+     * uses to create loggers. The two must resolve {@code null} to the same instance, otherwise
+     * loggers stored under the context default will not be found by null-factory lookups.
+     *
+     * @param defaultMessageFactory the default message factory (non-null)
+     */
+    public InternalLoggerRegistry(final MessageFactory defaultMessageFactory) {
+        this.defaultMessageFactory = requireNonNull(defaultMessageFactory, "defaultMessageFactory");
+    }
+
+    /**
+     * Expunges stale entries for logger references and message factories.
+     */
+    private void expungeStaleEntries() {
+        final Reference<? extends Logger> loggerRef = staleLoggerRefs.poll();
+
+        if (loggerRef != null) {
+            writeLock.lock();
+            try {
+                while (staleLoggerRefs.poll() != null) {
+                    // Clear refQueue
+                }
+
+                final Iterator<Map.Entry<MessageFactory, Map<String, WeakReference<Logger>>>>
+                        loggerRefByNameByMessageFactoryEntryIt =
+                                loggerRefByNameByMessageFactory.entrySet().iterator();
+                while (loggerRefByNameByMessageFactoryEntryIt.hasNext()) {
+                    final Map.Entry<MessageFactory, Map<String, WeakReference<Logger>>>
+                            loggerRefByNameByMessageFactoryEntry = loggerRefByNameByMessageFactoryEntryIt.next();
+                    final Map<String, WeakReference<Logger>> loggerRefByName =
+                            loggerRefByNameByMessageFactoryEntry.getValue();
+                    loggerRefByName.values().removeIf(weakRef -> weakRef.get() == null);
+                    if (loggerRefByName.isEmpty()) {
+                        loggerRefByNameByMessageFactoryEntryIt.remove();
+                    }
+                }
+            } finally {
+                writeLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Returns the logger associated with the given name and message factory.
+     * <p>
+     * In the absence of a message factory, there can be made no assumptions on the message factory of the returned
+     * logger. This lenient behaviour is only kept for backward compatibility. Callers are strongly advised to
+     * <b>provide a message factory parameter to the method!</b>
+     * </p>
+     *
+     * @param name a logger name
+     * @param messageFactory a message factory
+     * @return the logger associated with the given name and message factory
+     */
+    @Override
+    public @Nullable Logger getLogger(final String name, @Nullable final MessageFactory messageFactory) {
+        requireNonNull(name, "name");
+
+        expungeStaleEntries();
+
+        final MessageFactory mf = messageFactory != null ? messageFactory : defaultMessageFactory;
+
+        readLock.lock();
+        try {
+            final Map<String, WeakReference<Logger>> loggerRefByName = loggerRefByNameByMessageFactory.get(mf);
+            if (loggerRefByName != null) {
+                final WeakReference<Logger> loggerRef = loggerRefByName.get(name);
+                if (loggerRef != null) {
+                    return loggerRef.get();
+                }
+            }
+            return null;
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * Returns all registered loggers.
+     *
+     * @return all registered loggers
+     */
+    @Override
+    public Collection<Logger> getLoggers() {
+        expungeStaleEntries();
+
+        readLock.lock();
+        try {
+            // Return a new collection to allow concurrent iteration over the loggers
+            //
+            // https://github.com/apache/logging-log4j2/issues/3234
+            return loggerRefByNameByMessageFactory.values().stream()
+                    .flatMap(loggerRefByName -> loggerRefByName.values().stream())
+                    .flatMap(loggerRef -> {
+                        @Nullable Logger logger = loggerRef.get();
+                        return logger != null ? Stream.of(logger) : Stream.empty();
+                    })
+                    .collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * Adds all registered loggers to the given destination collection.
+     *
+     * @param destination the collection to add loggers to
+     * @return the destination collection with all registered loggers added
+     */
+    @Override
+    public Collection<Logger> getLoggers(final Collection<Logger> destination) {
+        requireNonNull(destination, "destination");
+
+        expungeStaleEntries();
+
+        readLock.lock();
+        try {
+            for (final Map<String, WeakReference<Logger>> loggerRefByName : loggerRefByNameByMessageFactory.values()) {
+                for (final WeakReference<Logger> loggerRef : loggerRefByName.values()) {
+                    @Nullable Logger logger = loggerRef.get();
+                    if (logger != null) {
+                        destination.add(logger);
+                    }
+                }
+            }
+        } finally {
+            readLock.unlock();
+        }
+        return destination;
+    }
+
+    /**
+     * Checks if a logger associated with the given name and message factory exists.
+     * <p>
+     * In the absence of a message factory, there can be made no assumptions on the message factory of the found
+     * logger. This lenient behaviour is only kept for backward compatibility. Callers are strongly advised to
+     * <b>provide a message factory parameter to the method!</b>
+     * </p>
+     *
+     * @param name a logger name
+     * @param messageFactory a message factory
+     * @return {@code true}, if the logger exists; {@code false} otherwise.
+     */
+    @Override
+    public boolean hasLogger(final String name, @Nullable final MessageFactory messageFactory) {
+        requireNonNull(name, "name");
+        return getLogger(name, messageFactory) != null;
+    }
+
+    /**
+     * Checks if a logger associated with the given name and message factory type exists.
+     *
+     * @param name a logger name
+     * @param messageFactoryClass a message factory class
+     * @return {@code true}, if the logger exists; {@code false} otherwise.
+     */
+    @Override
+    public boolean hasLogger(final String name, final Class<? extends MessageFactory> messageFactoryClass) {
+        requireNonNull(name, "name");
+        requireNonNull(messageFactoryClass, "messageFactoryClass");
+
+        expungeStaleEntries();
+
+        readLock.lock();
+        try {
+            return loggerRefByNameByMessageFactory.entrySet().stream()
+                    .filter(entry -> messageFactoryClass.equals(entry.getKey().getClass()))
+                    .anyMatch(entry -> entry.getValue().containsKey(name));
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * Registers the provided logger.
+     * <p>
+     * The logger is registered using the {@code name} and {@code messageFactory} parameters as keys.
+     * </p>
+     *
+     * @param name a logger name
+     * @param messageFactory a message factory
+     * @param logger a logger instance
+     */
+    @Override
+    public void putIfAbsent(final String name, @Nullable final MessageFactory messageFactory, final Logger logger) {
+        requireNonNull(name, "name");
+        requireNonNull(logger, "logger");
+
+        expungeStaleEntries();
+
+        final MessageFactory mf = messageFactory != null ? messageFactory : defaultMessageFactory;
+
+        writeLock.lock();
+        try {
+            Map<String, WeakReference<Logger>> loggerRefByName = loggerRefByNameByMessageFactory.get(mf);
+            // noinspection Java8MapApi (avoid the allocation of lambda passed to `Map::computeIfAbsent`)
+            if (loggerRefByName == null) {
+                loggerRefByNameByMessageFactory.put(mf, loggerRefByName = new HashMap<>());
+            }
+            final WeakReference<Logger> loggerRef = loggerRefByName.get(name);
+            if (loggerRef == null || loggerRef.get() == null) {
+                loggerRefByName.put(name, new WeakReference<>(logger, staleLoggerRefs));
+            }
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    /**
+     * Returns the logger associated with the given name and message factory, creating it if necessary
+     * using the provided supplier. The logger is created outside the write lock to avoid deadlocks
+     * and reduce contention.
+     *
+     * @param name a logger name
+     * @param messageFactory a message factory
+     * @param loggerSupplier a function to create the logger
+     * @return the existing or newly created logger
+     */
+    public Logger computeIfAbsent(
+            final String name,
+            final MessageFactory messageFactory,
+            final BiFunction<String, MessageFactory, Logger> loggerSupplier) {
+        // Check arguments
+        requireNonNull(name, "name");
+        requireNonNull(messageFactory, "messageFactory");
+        requireNonNull(loggerSupplier, "loggerSupplier");
+        // Skipping `expungeStaleEntries()`, it will be invoked by the `getLogger()` invocation below
+
+        // Read lock fast path: See if logger already exists
+        @Nullable Logger logger = getLogger(name, messageFactory);
+        if (logger != null) {
+            return logger;
+        }
+
+        // Intentionally moving the logger creation outside the write lock, because:
+        //
+        // - Logger instantiation is expensive (causes contention on the write-lock)
+        //
+        // - User code might have circular code paths, though through different threads.
+        //   Consider `T1[ILR:computeIfAbsent] -> ... -> T1[Logger::new] -> ... -> T2[ILR::computeIfAbsent]`.
+        //   Hence, having logger instantiation while holding a write lock might cause deadlocks:
+        //   https://github.com/apache/logging-log4j2/issues/3252
+        //   https://github.com/apache/logging-log4j2/issues/3399
+        //
+        // - Creating loggers without a lock, allows multiple threads to create loggers in parallel, which also improves
+        // performance.
+        //
+        // Since all loggers with the same parameters are equivalent, we can safely return the logger from the
+        // thread that finishes first.
+        Logger newLogger = loggerSupplier.apply(name, messageFactory);
+
+        // Report name and message factory mismatch if there are any
+        final String loggerName = newLogger.getName();
+        final MessageFactory loggerMessageFactory = newLogger.getMessageFactory();
+        if (!loggerName.equals(name) || !loggerMessageFactory.equals(messageFactory)) {
+            StatusLogger.getLogger()
+                    .error(
+                            "Newly registered logger with name `{}` and message factory `{}`, "
+                                    + "is requested to be associated with a different name `{}` or message factory `{}`.\n"
+                                    + "Effectively the message factory of the logger will be used and the other one will be ignored.\n"
+                                    + "This generally hints a problem at the logger context implementation.\n"
+                                    + "Please report this using the Log4j project issue tracker.",
+                            loggerName,
+                            loggerMessageFactory,
+                            name,
+                            messageFactory);
+        }
+
+        // Write lock slow path: Insert the logger
+        writeLock.lock();
+        try {
+            Map<String, WeakReference<Logger>> loggerRefByName = loggerRefByNameByMessageFactory.get(messageFactory);
+            // noinspection Java8MapApi (avoid the allocation of lambda passed to `Map::computeIfAbsent`)
+            if (loggerRefByName == null) {
+                loggerRefByNameByMessageFactory.put(messageFactory, loggerRefByName = new HashMap<>());
+            }
+            final WeakReference<Logger> loggerRef = loggerRefByName.get(name);
+            if (loggerRef == null || (logger = loggerRef.get()) == null) {
+                loggerRefByName.put(name, new WeakReference<>(logger = newLogger, staleLoggerRefs));
+            }
+            return logger;
+        } finally {
+            writeLock.unlock();
+        }
+    }
+}