Operations 53 min read

Log4j2 Thread Blocking Causes and Mitigation Strategies

This article examines how excessive logging, AsyncAppender queue saturation, JVM reflection optimizations, and Lambda class loading can cause thread blocking in Log4j2, analyzes root causes with code examples, and provides practical guidelines and best‑practice configurations to prevent performance degradation in high‑throughput Java services.

DataFunSummit
DataFunSummit
DataFunSummit
Log4j2 Thread Blocking Causes and Mitigation Strategies

In high‑throughput Java services, developers often rely on Apache Log4j2 for logging. While Log4j2 offers powerful asynchronous appenders, improper usage can lead to severe thread blocking, degrading overall system performance. This article dissects the main culprits—log queue saturation, reflection‑based class loading, and Lambda‑generated classes—and presents concrete mitigation techniques.

1. Log Queue Saturation

AsyncAppender maintains a bounded BlockingQueue (default size 128). When the queue fills, Log4j2 applies the AsyncQueueFullPolicy . If blocking=true , the calling thread may block; otherwise, the default DefaultErrorHandler logs to StatusConsoleListener , which writes to System.out under a synchronized block, causing additional contention.

@Override
public void append(LogEvent logEvent) {
    if (!isStarted()) {
        throw new IllegalStateException("AsyncAppender " + getName() + " is not active");
    }
    Log4jLogEvent memento = Log4jLogEvent.createMemento(logEvent, includeLocation);
    if (!transfer(memento)) {
        if (blocking) {
            EventRoute route = asyncQueueFullPolicy.getRoute(thread.getId(), memento.getLevel());
            route.logMessage(this, memento);
        } else {
            error("Appender " + getName() + " is unable to write primary appenders. queue is full");
            logToErrorAppenderIfNecessary(false, memento);
        }
    }
}

To avoid blocking, configure log4j2.AsyncQueueFullPolicy=DISCARD or implement a custom appender that silently drops overflowed events.

2. ThrowableProxy and Class Loading

When an exception is logged, Log4j2 creates a ThrowableProxy to capture stack‑trace details. The constructor iterates over each StackTraceElement , loads the corresponding class via Class.forName , and extracts JAR location and version information. This triggers class loading under a synchronized lock, which becomes a bottleneck under heavy logging.

public ThrowableProxy(Throwable throwable) {
    this.name = throwable.getClass().getName();
    // ...
    this.extendedStackTrace = toExtendedStackTrace(stack, map, null, throwable.getStackTrace());
}

private ExtendedStackTraceElement[] toExtendedStackTrace(Stack< Class
> stack,
        Map
map, StackTraceElement[] rootTrace,
        StackTraceElement[] stackTrace) {
    // For each element, load the class to obtain JAR info
    Class
clazz = loadClass(lastLoader, className);
    // ...
}

Because the cache uses StackTraceElement.toString() as the key but looks up with getClassName() , the cache is ineffective, leading to repeated class loads.

3. JVM Reflection Inflation

Log4j2 formats messages via reflection. The JVM optimizes reflective calls by generating byte‑code classes such as sun.reflect.GeneratedMethodAccessorN after a threshold (default 15). These generated classes are loaded by DelegatingClassLoader , which other class loaders cannot access, causing repeated synchronized loading and thread blocking.

private MethodAccessor generateMethod(Class
declaringClass, String name,
        Class
[] parameterTypes, Class
returnType,
        Class
[] checkedExceptions, int modifiers) {
    // Generates class name like sun/reflect/GeneratedMethodAccessor0
    String generatedName = generateName(false, false);
    return (MethodAccessor) AccessController.doPrivileged(
        () -> ClassDefiner.defineClass(generatedName, bytes, 0, bytes.length,
                declaringClass.getClassLoader()).newInstance());
}

Disabling inflation (setting -Dsun.reflect.inflationThreshold=Integer.MAX_VALUE ) avoids the extra class loading but sacrifices reflection performance.

4. Lambda‑Generated Classes (JDK 8 Bug)

Lambda expressions are compiled to synthetic classes named $$Lambda$N . In JDK 8 prior to update 171, these classes could not be loaded by the web‑app class loader, leading to the same synchronized loading issue when exceptions contain Lambda stack frames.

com.example.Service$$Lambda$35/1331430278

Upgrading to JDK 9+ (or JDK 8 u171) resolves this bug.

5. AsyncLoggerConfig (Disruptor) Path

When using <AsyncLogger> , Log4j2 employs a Disruptor ring buffer. The translator ( MUTABLE_TRANSLATOR ) calls MutableLogEvent.initFrom() , which also creates a ThrowableProxy , reproducing the class‑loading bottleneck.

private static final EventTranslatorTwoArg<Log4jEventWrapper, LogEvent, AsyncLoggerConfig> MUTABLE_TRANSLATOR =
    (ringBufferElement, sequence, logEvent, loggerConfig) -> {
        ((MutableLogEvent) ringBufferElement.event).initFrom(logEvent);
        ringBufferElement.loggerConfig = loggerConfig;
    };

Replacing <AsyncLogger> with regular <Logger> or using a custom appender eliminates this path.

Mitigation Summary

Use custom appenders (e.g., AsyncScribeAppender ) that build Log4jLogEvent without invoking ThrowableProxy .

Prefer synchronous <Logger> over <AsyncLogger> unless the async context selector is explicitly enabled.

Set log4j2.AsyncQueueFullPolicy=DISCARD or implement an error handler that silently drops overflowed events.

Upgrade the JDK to 9+ (or at least 8u171) to fix the Lambda class‑loading bug.

Configure pattern layouts to use %ex instead of the default %xEx to avoid extra stack‑trace processing.

Avoid ConsoleAppender in production, as its synchronized output adds contention.

Below is a recommended log4j2.xml configuration that incorporates these practices.

<configuration status="warn">
    <appenders>
        <Console name="Console" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="%d{yyyy/MM/dd HH:mm:ss.SSS} %t [%p] %c{1} (%F:%L) %msg%n %ex"/>
        </Console>
        <XMDFile name="ShepherdLog" fileName="shepherd.log">
            <PatternLayout pattern="%d{yyyy/MM/dd HH:mm:ss.SSS} %t [%p] %c{1} (%F:%L) %msg%n %ex"/>
        </XMDFile>
        <XMDFile name="LocalServiceLog" fileName="request.log">
            <PatternLayout pattern="%d{yyyy/MM/dd HH:mm:ss.SSS} %t [%p] %c{1} (%F:%L) %msg%n %ex"/>
        </XMDFile>
        <AsyncScribe name="LogCenterAsync" blocking="false">
            <LcLayout/>
        </AsyncScribe>
    </appenders>
    <loggers>
        <logger name="com.sankuai.shepherd" level="info" additivity="false">
            <AppenderRef ref="ShepherdLog" level="warn"/>
            <AppenderRef ref="LogCenterAsync" level="info"/>
        </logger>
        <root level="info">
            <!-- Remove Console in production -->
            <appender-ref ref="LocalServiceLog"/>
            <appender-ref ref="LogCenterAsync"/>
        </root>
    </loggers>
</configuration>

By following these guidelines, developers can eliminate hidden thread‑blocking hotspots in Log4j2 and ensure stable, high‑performance logging for large‑scale Java services.

performancelog4j2ThreadBlockingAsyncAppenderJavaLogging
DataFunSummit
Written by

DataFunSummit

Official account of the DataFun community, dedicated to sharing big data and AI industry summit news and speaker talks, with regular downloadable resource packs.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.