Skip to content

Commit 462b0fc

Browse files
committed
Removes weak references from LoggerRepository (#3199)
Removes weak references to `Logger`s in `LoggerRepository`. The usage of weak references in `LoggerRepository` might cause `null` to be returned by `LogManager.getLogger()` of all Log4j Core versions up to `2.24.1`. Versions of Log4j API up to `2.24.0` did hold **hard** references to all the registered loggers, so the change will not alter the previous behavior. This PR also inverts the order of the `String` and `MessageFactory` keys to the `LoggerRepository` multi-map to limit the number of internal maps. The external map is a `WeakHashMap` to allow logger-specific message factories to be GC-ed. Closes #3143.
1 parent 3fc2088 commit 462b0fc

File tree

7 files changed

+295
-51
lines changed

7 files changed

+295
-51
lines changed

log4j-api-test/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@
144144
<artifactId>mockito-inline</artifactId>
145145
<scope>test</scope>
146146
</dependency>
147+
<dependency>
148+
<groupId>org.jspecify</groupId>
149+
<artifactId>jspecify</artifactId>
150+
<scope>test</scope>
151+
</dependency>
147152
<!-- Used by ServiceLoaderUtilTest -->
148153
<dependency>
149154
<groupId>org.osgi</groupId>
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.spi;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
import java.lang.ref.WeakReference;
22+
import java.util.stream.Stream;
23+
import org.apache.logging.log4j.message.MessageFactory;
24+
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
25+
import org.apache.logging.log4j.message.ReusableMessageFactory;
26+
import org.apache.logging.log4j.test.TestLogger;
27+
import org.jspecify.annotations.Nullable;
28+
import org.junit.jupiter.params.ParameterizedTest;
29+
import org.junit.jupiter.params.provider.MethodSource;
30+
31+
class LoggerRegistryTest {
32+
33+
private static final String LOGGER_NAME = LoggerRegistryTest.class.getName();
34+
35+
static Stream<@Nullable MessageFactory> doesNotLoseLoggerReferences() {
36+
return Stream.of(
37+
ParameterizedMessageFactory.INSTANCE,
38+
ReusableMessageFactory.INSTANCE,
39+
new ParameterizedMessageFactory(),
40+
null);
41+
}
42+
43+
/**
44+
* @see <a href="https://github.com/apache/logging-log4j2/issues/3143>Issue #3143</a>
45+
*/
46+
@ParameterizedTest
47+
@MethodSource
48+
void doesNotLoseLoggerReferences(@Nullable MessageFactory messageFactory) {
49+
LoggerRegistry<TestLogger> loggerRegistry = new LoggerRegistry<>();
50+
TestLogger logger = new TestLogger(LOGGER_NAME, messageFactory);
51+
WeakReference<TestLogger> loggerRef = new WeakReference<>(logger);
52+
// Register logger
53+
loggerRegistry.putIfAbsent(LOGGER_NAME, messageFactory, logger);
54+
// The JIT compiler/optimizer might figure out by himself the `logger` and `messageFactory` are no longer used:
55+
// https://shipilev.net/jvm/anatomy-quarks/8-local-var-reachability/
56+
// We help him with the task though.
57+
logger = null;
58+
// Trigger a GC run
59+
System.gc();
60+
// Check if the logger is still there
61+
assertThat(loggerRef.get()).isNotNull();
62+
assertThat(loggerRegistry.getLogger(LOGGER_NAME, messageFactory)).isInstanceOf(TestLogger.class);
63+
}
64+
}

log4j-api/src/main/java/org/apache/logging/log4j/spi/LoggerRegistry.java

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,26 @@
1818

1919
import static java.util.Objects.requireNonNull;
2020

21-
import java.lang.ref.WeakReference;
2221
import java.util.ArrayList;
2322
import java.util.Collection;
2423
import java.util.Collections;
2524
import java.util.HashMap;
2625
import java.util.Map;
27-
import java.util.Objects;
2826
import java.util.WeakHashMap;
2927
import java.util.concurrent.ConcurrentHashMap;
3028
import java.util.concurrent.locks.Lock;
3129
import java.util.concurrent.locks.ReadWriteLock;
3230
import java.util.concurrent.locks.ReentrantReadWriteLock;
3331
import org.apache.logging.log4j.message.MessageFactory;
3432
import org.apache.logging.log4j.message.ParameterizedMessageFactory;
33+
import org.jspecify.annotations.Nullable;
3534

3635
/**
3736
* Convenience class to be used as an {@link ExtendedLogger} registry by {@code LoggerContext} implementations.
3837
*/
3938
public class LoggerRegistry<T extends ExtendedLogger> {
4039

41-
private final Map<String, Map<MessageFactory, WeakReference<T>>> loggerRefByMessageFactoryByName = new HashMap<>();
40+
private final Map<String, Map<MessageFactory, T>> loggerByMessageFactoryByName = new HashMap<>();
4241

4342
private final ReadWriteLock lock = new ReentrantReadWriteLock();
4443

@@ -127,7 +126,7 @@ public LoggerRegistry(final MapFactory<T> mapFactory) {
127126
* @param name a logger name
128127
* @return the logger associated with the name
129128
*/
130-
public T getLogger(final String name) {
129+
public @Nullable T getLogger(final String name) {
131130
requireNonNull(name, "name");
132131
return getLogger(name, null);
133132
}
@@ -144,39 +143,29 @@ public T getLogger(final String name) {
144143
* @param messageFactory a message factory
145144
* @return the logger associated with the given name and message factory
146145
*/
147-
public T getLogger(final String name, final MessageFactory messageFactory) {
146+
public @Nullable T getLogger(final String name, @Nullable final MessageFactory messageFactory) {
148147
requireNonNull(name, "name");
149148
readLock.lock();
150149
try {
151-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
152-
loggerRefByMessageFactoryByName.get(name);
153-
if (loggerRefByMessageFactory == null) {
154-
return null;
155-
}
150+
final @Nullable Map<MessageFactory, T> loggerByMessageFactory = loggerByMessageFactoryByName.get(name);
156151
final MessageFactory effectiveMessageFactory =
157152
messageFactory != null ? messageFactory : ParameterizedMessageFactory.INSTANCE;
158-
final WeakReference<T> loggerRef = loggerRefByMessageFactory.get(effectiveMessageFactory);
159-
if (loggerRef == null) {
160-
return null;
161-
}
162-
return loggerRef.get();
153+
return loggerByMessageFactory == null ? null : loggerByMessageFactory.get(effectiveMessageFactory);
163154
} finally {
164155
readLock.unlock();
165156
}
166157
}
167158

168159
public Collection<T> getLoggers() {
169-
return getLoggers(new ArrayList<T>());
160+
return getLoggers(new ArrayList<>());
170161
}
171162

172163
public Collection<T> getLoggers(final Collection<T> destination) {
173164
requireNonNull(destination, "destination");
174165
readLock.lock();
175166
try {
176-
loggerRefByMessageFactoryByName.values().stream()
177-
.flatMap(loggerRefByMessageFactory ->
178-
loggerRefByMessageFactory.values().stream().map(WeakReference::get))
179-
.filter(Objects::nonNull)
167+
loggerByMessageFactoryByName.values().stream()
168+
.flatMap(loggerByMessageFactory -> loggerByMessageFactory.values().stream())
180169
.forEach(destination::add);
181170
} finally {
182171
readLock.unlock();
@@ -196,7 +185,7 @@ public Collection<T> getLoggers(final Collection<T> destination) {
196185
*/
197186
public boolean hasLogger(final String name) {
198187
requireNonNull(name, "name");
199-
final T logger = getLogger(name);
188+
final @Nullable T logger = getLogger(name);
200189
return logger != null;
201190
}
202191

@@ -215,7 +204,7 @@ public boolean hasLogger(final String name) {
215204
*/
216205
public boolean hasLogger(final String name, final MessageFactory messageFactory) {
217206
requireNonNull(name, "name");
218-
final T logger = getLogger(name, messageFactory);
207+
final @Nullable T logger = getLogger(name, messageFactory);
219208
return logger != null;
220209
}
221210

@@ -232,7 +221,7 @@ public boolean hasLogger(final String name, final Class<? extends MessageFactory
232221
requireNonNull(messageFactoryClass, "messageFactoryClass");
233222
readLock.lock();
234223
try {
235-
return loggerRefByMessageFactoryByName.getOrDefault(name, Collections.emptyMap()).keySet().stream()
224+
return loggerByMessageFactoryByName.getOrDefault(name, Collections.emptyMap()).keySet().stream()
236225
.anyMatch(messageFactory -> messageFactoryClass.equals(messageFactory.getClass()));
237226
} finally {
238227
readLock.unlock();
@@ -243,32 +232,30 @@ public boolean hasLogger(final String name, final Class<? extends MessageFactory
243232
* Registers the provided logger.
244233
* <b>Logger name and message factory parameters are ignored</b>, those will be obtained from the logger instead.
245234
*
246-
* @param name ignored – kept for backward compatibility
247-
* @param messageFactory ignored – kept for backward compatibility
235+
* @param name a logger name
236+
* @param messageFactory a message factory
248237
* @param logger a logger instance
249238
*/
250-
public void putIfAbsent(final String name, final MessageFactory messageFactory, final T logger) {
239+
public void putIfAbsent(final String name, @Nullable final MessageFactory messageFactory, final T logger) {
251240

252241
// Check arguments
242+
requireNonNull(name, "name");
253243
requireNonNull(logger, "logger");
254244

255245
// Insert the logger
256246
writeLock.lock();
257247
try {
258-
final Map<MessageFactory, WeakReference<T>> loggerRefByMessageFactory =
259-
loggerRefByMessageFactoryByName.computeIfAbsent(
260-
logger.getName(), this::createLoggerRefByMessageFactoryMap);
261-
final MessageFactory loggerMessageFactory = logger.getMessageFactory();
262-
final WeakReference<T> loggerRef = loggerRefByMessageFactory.get(loggerMessageFactory);
263-
if (loggerRef == null || loggerRef.get() == null) {
264-
loggerRefByMessageFactory.put(loggerMessageFactory, new WeakReference<>(logger));
265-
}
248+
final MessageFactory effectiveMessageFactory =
249+
messageFactory != null ? messageFactory : ParameterizedMessageFactory.INSTANCE;
250+
loggerByMessageFactoryByName
251+
.computeIfAbsent(name, this::createLoggerRefByMessageFactoryMap)
252+
.putIfAbsent(effectiveMessageFactory, logger);
266253
} finally {
267254
writeLock.unlock();
268255
}
269256
}
270257

271-
private Map<MessageFactory, WeakReference<T>> createLoggerRefByMessageFactoryMap(final String ignored) {
258+
private Map<MessageFactory, T> createLoggerRefByMessageFactoryMap(final String ignored) {
272259
return new WeakHashMap<>();
273260
}
274261
}

log4j-api/src/main/java/org/apache/logging/log4j/spi/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* API classes.
2020
*/
2121
@Export
22-
@Version("2.24.1")
22+
@Version("2.24.2")
2323
package org.apache.logging.log4j.spi;
2424

2525
import org.osgi.annotation.bundle.Export;

log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.concurrent.TimeUnit;
3434
import java.util.concurrent.locks.Lock;
3535
import java.util.concurrent.locks.ReentrantLock;
36+
import java.util.stream.Collectors;
3637
import org.apache.logging.log4j.LogManager;
3738
import org.apache.logging.log4j.core.config.Configuration;
3839
import org.apache.logging.log4j.core.config.ConfigurationFactory;
@@ -47,11 +48,11 @@
4748
import org.apache.logging.log4j.core.util.ExecutorServices;
4849
import org.apache.logging.log4j.core.util.NetUtils;
4950
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
51+
import org.apache.logging.log4j.core.util.internal.InternalLoggerRegistry;
5052
import org.apache.logging.log4j.message.MessageFactory;
5153
import org.apache.logging.log4j.spi.LoggerContextFactory;
5254
import org.apache.logging.log4j.spi.LoggerContextShutdownAware;
5355
import org.apache.logging.log4j.spi.LoggerContextShutdownEnabled;
54-
import org.apache.logging.log4j.spi.LoggerRegistry;
5556
import org.apache.logging.log4j.spi.Terminable;
5657
import org.apache.logging.log4j.spi.ThreadContextMapFactory;
5758
import org.apache.logging.log4j.util.PropertiesUtil;
@@ -82,7 +83,7 @@ public class LoggerContext extends AbstractLifeCycle
8283
*/
8384
private static final MessageFactory DEFAULT_MESSAGE_FACTORY = Logger.getEffectiveMessageFactory(null);
8485

85-
private final LoggerRegistry<Logger> loggerRegistry = new LoggerRegistry<>();
86+
private final InternalLoggerRegistry loggerRegistry = new InternalLoggerRegistry();
8687
private final CopyOnWriteArrayList<PropertyChangeListener> propertyChangeListeners = new CopyOnWriteArrayList<>();
8788
private volatile List<LoggerContextShutdownAware> listeners;
8889

@@ -512,7 +513,7 @@ public Logger getLogger(final String name) {
512513
* @return a collection of the current loggers.
513514
*/
514515
public Collection<Logger> getLoggers() {
515-
return loggerRegistry.getLoggers();
516+
return loggerRegistry.getLoggers().collect(Collectors.toList());
516517
}
517518

518519
/**
@@ -526,23 +527,22 @@ public Collection<Logger> getLoggers() {
526527
public Logger getLogger(final String name, final MessageFactory messageFactory) {
527528
final MessageFactory effectiveMessageFactory =
528529
messageFactory != null ? messageFactory : DEFAULT_MESSAGE_FACTORY;
529-
final Logger oldLogger = loggerRegistry.getLogger(name, effectiveMessageFactory);
530-
if (oldLogger != null) {
531-
return oldLogger;
532-
}
533-
final Logger newLogger = newInstance(name, effectiveMessageFactory);
534-
loggerRegistry.putIfAbsent(name, effectiveMessageFactory, newLogger);
535-
return loggerRegistry.getLogger(name, effectiveMessageFactory);
530+
return loggerRegistry.computeIfAbsent(name, effectiveMessageFactory, this::newInstance);
536531
}
537532

538533
/**
539534
* Gets the LoggerRegistry.
540535
*
541536
* @return the LoggerRegistry.
542537
* @since 2.17.2
538+
* @deprecated since 2.25.0 without a replacement.
543539
*/
544-
public LoggerRegistry<Logger> getLoggerRegistry() {
545-
return loggerRegistry;
540+
@Deprecated
541+
public org.apache.logging.log4j.spi.LoggerRegistry<Logger> getLoggerRegistry() {
542+
org.apache.logging.log4j.spi.LoggerRegistry<Logger> result =
543+
new org.apache.logging.log4j.spi.LoggerRegistry<>();
544+
loggerRegistry.getLoggers().forEach(l -> result.putIfAbsent(l.getName(), l.getMessageFactory(), l));
545+
return result;
546546
}
547547

548548
/**
@@ -775,9 +775,7 @@ public void updateLoggers() {
775775
*/
776776
public void updateLoggers(final Configuration config) {
777777
final Configuration old = this.configuration;
778-
for (final Logger logger : loggerRegistry.getLoggers()) {
779-
logger.updateConfiguration(config);
780-
}
778+
loggerRegistry.getLoggers().forEach(logger -> logger.updateConfiguration(config));
781779
firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, old, config));
782780
}
783781

0 commit comments

Comments
 (0)