You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Log4j2 ReusableMessageFactory创建过多ThreadLocal致125MB内存占用问题

问题描述

我们项目使用ReusableMessageFactory创建日志消息,其中getParameterized方法的实现如下:

private static ThreadLocal<ReusableParameterizedMessage> threadLocalParameterized = new ThreadLocal<>();
private static ReusableParameterizedMessage getParameterized() {
    ReusableParameterizedMessage result = threadLocalParameterized.get();
    if (result == null) {
        result = new ReusableParameterizedMessage();
        threadLocalParameterized.set(result);
    }
    return result.reserved ? new ReusableParameterizedMessage().reserve() : result.reserve();
}

public static void release(final Message message) { // LOG4J2-1583
    if (message instanceof Clearable) {
        ((Clearable) message).clear();
    }
}

当前出现以下问题:

  • getParameterized方法最后一行会创建大量ReusableParameterizedMessage实例,且未调用threadLocal.remove(),导致线程中存在过多ThreadLocal条目。
  • 内存dump显示单个Tomcat线程有155857个ThreadLocal条目,几乎全部由ReusableParameterizedMessage.buffer创建,占用内存125MB。
  • 过多ThreadLocal引发ThreadLocalMap频繁哈希冲突,ThreadLocalMap.getEntryAfterMiss()方法消耗大量CPU。

疑问:通常logger.info()会调用ReusableMessageFactory.release()将reserved设为false,为何还会创建大量实例和buffer?此现象是否正常?若异常该如何修复?


问题分析与解决方案

现象是否正常?

完全异常。ReusableMessageFactory的设计初衷是复用消息实例、减少对象创建开销,当前情况不仅违背设计目的,还引发严重的内存占用和CPU消耗问题,属于明显的实现缺陷。

原因分析

  1. release方法未正确重置reserved状态:
    release仅调用Clearable.clear(),但如果ReusableParameterizedMessage的clear()方法没有将reserved字段重置为false,下一次调用getParameterized()时会判定实例仍处于占用状态,直接创建新实例并放入ThreadLocal,旧实例被遗弃但仍被ThreadLocal持有,持续积累。
  2. ThreadLocal未清理旧实例:
    创建新实例时,旧的ReusableParameterizedMessage仍被ThreadLocalMap持有,没有调用threadLocalParameterized.remove()移除,导致ThreadLocal条目不断增加。
  3. 并发场景下的状态竞争:
    若Tomcat线程池中的线程被复用,且日志操作的reserve和release未保证线程安全,可能导致reserved状态被错误标记为true,触发大量新实例创建。

修复方案

方案1:修正release方法,确保重置reserved状态

修改release方法,直接重置ReusableParameterizedMessage的reserved状态(若字段可访问,优先调用官方提供的状态重置方法):

public static void release(final Message message) { // LOG4J2-1583
    if (message instanceof Clearable) {
        ((Clearable) message).clear();
    }
    // 额外重置reserved状态
    if (message instanceof ReusableParameterizedMessage) {
        ((ReusableParameterizedMessage) message).reserved = false;
        // 若有公开的unreserve()方法,优先调用
        // ((ReusableParameterizedMessage) message).unreserve();
    }
}

方案2:修正getParameterized方法,避免不必要的实例创建

调整逻辑,当ThreadLocal中的实例处于reserved状态时,先清理旧实例再创建新实例:

private static ReusableParameterizedMessage getParameterized() {
    ReusableParameterizedMessage result = threadLocalParameterized.get();
    if (result == null) {
        result = new ReusableParameterizedMessage();
        threadLocalParameterized.set(result);
    } else if (result.reserved) {
        // 移除旧的占用实例
        threadLocalParameterized.remove();
        result = new ReusableParameterizedMessage();
        threadLocalParameterized.set(result);
    }
    return result.reserve();
}

方案3:替换为官方标准实现

如果自定义的ReusableMessageFactory存在设计缺陷,建议直接使用Log4j2官方提供的ReusableMessageFactory实现,官方版本已处理实例复用和状态重置逻辑,可避免此类问题。

方案4:线程池场景补充ThreadLocal清理

在Tomcat线程池的任务执行完毕后,手动清理ThreadLocal中的实例,避免线程复用导致的实例积累:

// 任务执行完成后调用
threadLocalParameterized.remove();

内容的提问来源于stack exchange,提问作者chengzi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 01:19:50