背景

公司的系统使用log4cxx作为日志库,近期将程序迁移到Linux环境,结果发现非常严重的内存泄露。经过分析,将内存定位到log4cxx。使用的版本为0.9.7

分析

分析log4cxx库发现,其使用引用计数控制动态内存的释放,所以在打日志的时候,会有以下的核心代码:

void Logger::forcedLog(const LevelPtr& level, const String& message,
    const char* file, int line)
{
    callAppenders(new LoggingEvent(FQCN, this, level, message, file, line));
}

void Logger::forcedLog(const String& fqcn, const LevelPtr& level, const String& message,
            const char* file, int line)
{
    callAppenders(new LoggingEvent(fqcn, this, level, message, file, line));
}

上述代码中,new出来的对象指针存放在一个指针对象中,在callAppenders调用结束之后,该指针对象将会析构。在其析构过程中,会通过引用计数去释放new出来的LoggingEvent对象,从而达到内存释放的目的。

现在问题出来了,new出来的对象实际上并未被释放,log4cxx的引用计数机制在linux下并没有生效。引用计数核心代码在objectimpl.cpp中,如下

void ObjectImpl::addRef() const
{
    Thread::InterlockedIncrement(&ref);
}

void ObjectImpl::releaseRef() const
{
    if (Thread::InterlockedDecrement(&ref) == 0)
    {   
        delete this;
    }   
}

我们在看看InterlockedIncrement 和 InterlockedDecrement的具体实现源码,在thread.cpp中,如下

long Thread::InterlockedIncrement(volatile long * val)
{   
#ifdef __GLIBCPP__
    return __exchange_and_add((volatile _Atomic_word *)val, 1 ) + 1;
#elif defined(__i386__)
    long ret;
    
    __asm__ __volatile__ ("lock; xaddl %0, %1"
                  : "=r" (ret), "=m" (*val)
                  : "0" (1), "m" (*val));

    return ret+1;
#elif defined(sparc) && defined(__SUNPRO_CC)
    sparc_atomic_add_32(val, 1);
    return *val;
#elif defined(HAVE_MS_THREAD)
#if _MSC_VER == 1200    // MSDEV 6
    return ::InterlockedIncrement((long *)val);
#else
    return ::InterlockedIncrement(val);
#endif // _MSC_VER
    return *val + 1 // unsafe
#endif
}

long Thread::InterlockedDecrement(volatile long * val)
{
#ifdef __GLIBCPP__
    return __exchange_and_add((volatile _Atomic_word *)val, -1 ) - 1;
#elif defined(__i386__)
    long ret;

    __asm__ __volatile__ ("lock; xaddl %0, %1"
                  : "=r" (ret), "=m" (*val)
                  : "0" (-1), "m" (*val));

    return ret-1;

#elif defined(sparc) && defined(__SUNPRO_CC)
    sparc_atomic_add_32(val, -1);
    return *val;
#elif defined(HAVE_MS_THREAD)
#if _MSC_VER == 1200    // MSDEV 6
    return ::InterlockedDecrement((long *)val);
#else
    return ::InterlockedDecrement(val);
#endif // _MSC_VER
    return *val - 1; // unsafe
#endif
}

这段代码非常让人纠结,根本看不懂啊,这么多环境相关的宏,谁知道走的是哪段代码啊?没办法,只好写一个程序测试自己的Linux系统都命中了哪些宏,测试的结果让人大跌眼镜。接口中的宏没有一个命中!!坑爹啊,这是。结果在Linux下,这2个接口都成为了空函数,啥也没干,连return语句都没有。注意,接口中//unsafe那一行其实也没有编译到代码中!

我以为//unsafe对应的代码段应该是所有宏都未定义的时候所编译的代码段,于是加上#else让其能够得到编译,结果。。。程序运行出core;好吧,是因为没有完成实际的计数值增减操作,那就加上代码完成增减,为此我使用了各种能想到的linux原子级加锁增减接口,结果还是不行,仍然出core。最后都快崩溃了。

解决

经过各种尝试不行之后,突然想到,是否可以抛弃log4cxx中的引用计数机制呢?于是回到前面的Logger::forcedLog接口。不就是内存释放,不用引用计数,手动释放还不行吗?于是我修改了这段代码,如下

void Logger::forcedLog(const LevelPtr& level, const String& message,
    const char* file, int line)
{
    LoggingEvent* pLog = new LoggingEvent(FQCN, this, level, message, file, line);
    callAppenders(pLog);
    delete pLog;
}

void Logger::forcedLog(const String& fqcn, const LevelPtr& level, const String& message,
            const char* file, int line)
{
    LoggingEvent* pLog = new LoggingEvent(fqcn, this, level, message, file, line);
    callAppenders(pLog);
    delete pLog;
}

这样,每一次打日志之后都手动释放内存。然后就是编译、测试,多线程运行,结果内存非常稳定,done!

总结

开源库不是万能的,条件编译也不是万能的,总会出现这样那样不同的环境导致库出现一些问题。好在可以看到源码。所以还是有办法解决的。当然,作者的解决方案其实并不是特别完美的,也有可能会出现其他的问题,希望各位大神不吝指教~


更多技术博客,请关注:www.chenkeblog.com


Logo

更多推荐