1818
1919import static java .util .Objects .requireNonNull ;
2020
21+ import java .lang .ref .Reference ;
22+ import java .lang .ref .ReferenceQueue ;
2123import java .lang .ref .WeakReference ;
2224import java .util .Collection ;
2325import java .util .HashMap ;
3941/**
4042 * A registry of {@link Logger}s namespaced by name and message factory.
4143 * This class is internally used by {@link LoggerContext}.
42- * <p>
43- * We don't use {@linkplain org.apache.logging.log4j.spi.LoggerRegistry the registry from Log4j API} to keep Log4j Core independent from the version of Log4j API at runtime.
44- * This also allows Log4j Core to evolve independently from Log4j API.
45- * </p>
44+ *
45+ * Handles automatic cleanup of stale logger references to prevent memory leaks.
4646 *
4747 * @since 2.25.0
4848 */
@@ -53,23 +53,44 @@ public final class InternalLoggerRegistry {
5353 new WeakHashMap <>();
5454
5555 private final ReadWriteLock lock = new ReentrantReadWriteLock ();
56-
5756 private final Lock readLock = lock .readLock ();
58-
5957 private final Lock writeLock = lock .writeLock ();
6058
59+ // ReferenceQueue to track stale WeakReferences
60+ private final ReferenceQueue <Logger > staleLoggerRefs = new ReferenceQueue <>();
61+
6162 public InternalLoggerRegistry () {}
6263
64+ /**
65+ * Expunges stale logger references from the registry.
66+ */
67+ private void expungeStaleEntries () {
68+ Reference <? extends Logger > loggerRef ;
69+ while ((loggerRef = staleLoggerRefs .poll ()) != null ) {
70+ removeLogger (loggerRef );
71+ }
72+ }
73+
74+ /**
75+ * Removes a logger from the registry.
76+ */
77+ private void removeLogger (Reference <? extends Logger > loggerRef ) {
78+ writeLock .lock ();
79+ try {
80+ loggerRefByNameByMessageFactory .values ().forEach (map -> map .values ().removeIf (ref -> ref == loggerRef ));
81+ } finally {
82+ writeLock .unlock ();
83+ }
84+ }
85+
6386 /**
6487 * Returns the logger associated with the given name and message factory.
65- *
66- * @param name a logger name
67- * @param messageFactory a message factory
68- * @return the logger associated with the given name and message factory
6988 */
7089 public @ Nullable Logger getLogger (final String name , final MessageFactory messageFactory ) {
7190 requireNonNull (name , "name" );
7291 requireNonNull (messageFactory , "messageFactory" );
92+ expungeStaleEntries (); // Clean up before retrieving
93+
7394 readLock .lock ();
7495 try {
7596 final Map <String , WeakReference <Logger >> loggerRefByName =
@@ -87,11 +108,10 @@ public InternalLoggerRegistry() {}
87108 }
88109
89110 public Collection <Logger > getLoggers () {
111+ expungeStaleEntries (); // Clean up before retrieving
112+
90113 readLock .lock ();
91114 try {
92- // Return a new collection to allow concurrent iteration over the loggers
93- //
94- // https://github.com/apache/logging-log4j2/issues/3234
95115 return loggerRefByNameByMessageFactory .values ().stream ()
96116 .flatMap (loggerRefByName -> loggerRefByName .values ().stream ())
97117 .flatMap (loggerRef -> {
@@ -104,29 +124,17 @@ public Collection<Logger> getLoggers() {
104124 }
105125 }
106126
107- /**
108- * Checks if a logger associated with the given name and message factory exists.
109- *
110- * @param name a logger name
111- * @param messageFactory a message factory
112- * @return {@code true}, if the logger exists; {@code false} otherwise.
113- */
114127 public boolean hasLogger (final String name , final MessageFactory messageFactory ) {
115128 requireNonNull (name , "name" );
116129 requireNonNull (messageFactory , "messageFactory" );
117130 return getLogger (name , messageFactory ) != null ;
118131 }
119132
120- /**
121- * Checks if a logger associated with the given name and message factory type exists.
122- *
123- * @param name a logger name
124- * @param messageFactoryClass a message factory class
125- * @return {@code true}, if the logger exists; {@code false} otherwise.
126- */
127133 public boolean hasLogger (final String name , final Class <? extends MessageFactory > messageFactoryClass ) {
128134 requireNonNull (name , "name" );
129135 requireNonNull (messageFactoryClass , "messageFactoryClass" );
136+ expungeStaleEntries (); // Clean up before checking
137+
130138 readLock .lock ();
131139 try {
132140 return loggerRefByNameByMessageFactory .entrySet ().stream ()
@@ -142,37 +150,21 @@ public Logger computeIfAbsent(
142150 final MessageFactory messageFactory ,
143151 final BiFunction <String , MessageFactory , Logger > loggerSupplier ) {
144152
145- // Check arguments
146153 requireNonNull (name , "name" );
147154 requireNonNull (messageFactory , "messageFactory" );
148155 requireNonNull (loggerSupplier , "loggerSupplier" );
149156
150- // Read lock fast path: See if logger already exists
157+ expungeStaleEntries (); // Clean up before adding a new logger
158+
151159 @ Nullable Logger logger = getLogger (name , messageFactory );
152160 if (logger != null ) {
153161 return logger ;
154162 }
155163
156- // Intentionally moving the logger creation outside the write lock, because:
157- //
158- // - Logger instantiation is expensive (causes contention on the write-lock)
159- //
160- // - User code might have circular code paths, though through different threads.
161- // Consider `T1[ILR:computeIfAbsent] -> ... -> T1[Logger::new] -> ... -> T2[ILR::computeIfAbsent]`.
162- // Hence, having logger instantiation while holding a write lock might cause deadlocks:
163- // https://github.com/apache/logging-log4j2/issues/3252
164- // https://github.com/apache/logging-log4j2/issues/3399
165- //
166- // - Creating loggers without a lock, allows multiple threads to create loggers in parallel, which also improves
167- // performance.
168- //
169- // Since all loggers with the same parameters are equivalent, we can safely return the logger from the
170- // thread that finishes first.
171164 Logger newLogger = loggerSupplier .apply (name , messageFactory );
172-
173- // Report name and message factory mismatch if there are any
174165 final String loggerName = newLogger .getName ();
175166 final MessageFactory loggerMessageFactory = newLogger .getMessageFactory ();
167+
176168 if (!loggerName .equals (name ) || !loggerMessageFactory .equals (messageFactory )) {
177169 StatusLogger .getLogger ()
178170 .error (
@@ -186,17 +178,16 @@ public Logger computeIfAbsent(
186178 messageFactory );
187179 }
188180
189- // Write lock slow path: Insert the logger
190181 writeLock .lock ();
191182 try {
192183 Map <String , WeakReference <Logger >> loggerRefByName = loggerRefByNameByMessageFactory .get (messageFactory );
193- // noinspection Java8MapApi (avoid the allocation of lambda passed to `Map::computeIfAbsent`)
194184 if (loggerRefByName == null ) {
195185 loggerRefByNameByMessageFactory .put (messageFactory , loggerRefByName = new HashMap <>());
196186 }
187+
197188 final WeakReference <Logger > loggerRef = loggerRefByName .get (name );
198189 if (loggerRef == null || (logger = loggerRef .get ()) == null ) {
199- loggerRefByName .put (name , new WeakReference <>(logger = newLogger ));
190+ loggerRefByName .put (name , new WeakReference <>(logger = newLogger , staleLoggerRefs ));
200191 }
201192 return logger ;
202193 } finally {
0 commit comments