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消耗问题,属于明显的实现缺陷。
原因分析
release方法未正确重置reserved状态:release仅调用Clearable.clear(),但如果ReusableParameterizedMessage的clear()方法没有将reserved字段重置为false,下一次调用getParameterized()时会判定实例仍处于占用状态,直接创建新实例并放入ThreadLocal,旧实例被遗弃但仍被ThreadLocal持有,持续积累。- ThreadLocal未清理旧实例:
创建新实例时,旧的ReusableParameterizedMessage仍被ThreadLocalMap持有,没有调用threadLocalParameterized.remove()移除,导致ThreadLocal条目不断增加。 - 并发场景下的状态竞争:
若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
相关产品推荐
相关产品推荐

