0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

记一次诡异的内存泄漏

CPP开发者 来源:高性能架构探索 2024-02-19 13:44 次阅读

缘起

最近在补一些基础知识,恰好涉及到了智能指针std::weak_ptr在解决std::shared_ptr时候循环引用的问题,如下:

classA{
public:
std::weak_ptrb_ptr;
};

classB{
public:
std::weak_ptra_ptr;
};

autoa=std::make_shared();
autob=std::make_shared();

a->b_ptr=b;
b->a_ptr=a;

就问了下,通常的用法是将A或者B中间的某一个变量声明为std::weak_ptr,如果两者都声明为std::weak_ptr会有什么问题?

咱们先不论这个问题本身,在随后的讨论中,风神突然贴了段代码:

#include
#include
#include

usingnamespacestd;

structA{
charbuffer[1024*1024*1024];//1GB
weak_ptrnext;
};

intmain(){
while(true){
autoa0=make_shared();
autoa1=make_shared();
autoa2=make_shared();
a0->next=a1;
a1->next=a2;
a2->next=a0;
//thisweak_ptrleak:
newweak_ptr{a0};
this_thread::sleep_for(chrono::seconds(3));
}
return0;
}

说实话,当初看了这个代码第一眼,是存在内存泄漏的(new一个weak_ptr没有释放),而没有理解风神这段代码真正的含义,于是在本地把这段代码编译运行了下,我的乖乖,内存占用如图:

812d709c-ceda-11ee-a297-92fbcf53809c.png

emm,虽然存在内存泄漏,但也不至于这么大,于是网上进行了搜索,直至我看到了下面这段话:

make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题.

如果介意上面new那点泄漏的话,不妨修改代码如下:

#include
#include
#include

usingnamespacestd;

structA{
charbuffer[1024*1024*1024];//1GB
weak_ptrnext;
};

intmain(){
std::weak_ptrwptr;
{
autosptr=make_shared();
wptr=sptr;
}

this_thread::sleep_for(chrono::seconds(30));
return0;
}

也就是说,对于std::shared_ptr ptr(new Obj),形如下图:

813cefe0-ceda-11ee-a297-92fbcf53809c.png

而对于std::make_shared,形如下图:

814bddac-ceda-11ee-a297-92fbcf53809c.png

好了,理由上面已经说明白了,不再赘述了,如果你想继续分析的话,请看下文,否则~~

原因

虽然上节给出了原因,不过还是好奇心驱使,想从源码角度去了解下,于是打开了好久没看的gcc源码。

std::make_shared

首先看下它的定义:

template
inlineshared_ptr<_Tp>make_shared(_Args&&...__args){
typedeftypenamestd::remove_cv<_Tp>::type_Tp_nc;
returnstd::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
std::forward<_Args>(__args)...);
}

这个函数函数体只有一个std::allocate_shared,接着看它的定义:

template
inlineshared_ptr<_Tp>
allocate_shared(const_Alloc&__a,_Args&&...__args){
returnshared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
std::forward<_Args>(__args)...);
}

创建了一个shared_ptr对象,看下其对应的构造函数:

template
shared_ptr(_Sp_alloc_shared_tag<_Alloc>__tag,_Args&&...__args)
:__shared_ptr<_Tp>(__tag,std::forward<_Args>(__args)...){}

接着看__shared_ptr这个类对应的构造函数:

template
__shared_ptr(_Sp_alloc_shared_tag<_Alloc>__tag,_Args&&...__args)
:_M_ptr(),_M_refcount(_M_ptr,__tag,std::forward<_Args>(__args)...)
{_M_enable_shared_from_this_with(_M_ptr);}

其中,_M_refcount的类型为__shared_count,也就是说我们通常所说的引用计数就是由其来管理。

因为调用make_shared函数,所以这里的_M_ptr指针也就是相当于一个空指针,然后继续看下_M_refcount(请注意_M_ptr作为参数传入)定义:

template
__shared_count(_Tp*&__p,_Sp_alloc_shared_tag<_Alloc>__a,_Args&&...__args){
typedef_Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp>_Sp_cp_type;//L1
typename_Sp_cp_type::__allocator_type__a2(__a._M_a);//L2
auto__guard=std::__allocate_guarded(__a2);
_Sp_cp_type*__mem=__guard.get();//L3
auto__pi=::new(__mem)_Sp_cp_type(__a._M_a,std::forward<_Args>(__args)...);//L4
__guard=nullptr;
_M_pi=__pi;
__p=__pi->_M_ptr();//L5
}

这块代码当时看了很多遍,一直不明白在没有显示分配对象内存的情况下,是如何使用placement new的,直至今天上午,灵光一闪,突然明白了,且听慢慢道来。

首先看下L1行,其声明了模板类_Sp_counted_ptr_inplace的别名为_Sp_cp_type,其定义如下:

template
class_Sp_counted_ptr_inplacefinal:public_Sp_counted_base<_Lp>
{
class_Impl:_Sp_ebo_helper<0, _Alloc>
{
typedef_Sp_ebo_helper<0, _Alloc>_A_base;

public:
explicit_Impl(_Alloc__a)noexcept:_A_base(__a){}

_Alloc&_M_alloc()noexcept{return_A_base::_S_get(*this);}

__gnu_cxx::__aligned_buffer<_Tp>_M_storage;
};
public:
using__allocator_type=__alloc_rebind<_Alloc, _Sp_counted_ptr_inplace>;

//Allocparameterisnotareferencesodoesn'taliasanythingin__args
template
_Sp_counted_ptr_inplace(_Alloc__a,_Args&&...__args)
:_M_impl(__a)
{
//_GLIBCXX_RESOLVE_LIB_DEFECTS
//2070.allocate_sharedshoulduseallocator_traits::construct
allocator_traits<_Alloc>::construct(__a,_M_ptr(),
std::forward<_Args>(__args)...);//mightthrow
}

~_Sp_counted_ptr_inplace()noexcept{}

virtualvoid
_M_dispose()noexcept
{
allocator_traits<_Alloc>::destroy(_M_impl._M_alloc(),_M_ptr());
}

//Overridebecausetheallocatorneedstoknowthedynamictype
virtualvoid
_M_destroy()noexcept
{
__allocator_type__a(_M_impl._M_alloc());
__allocated_ptr<__allocator_type>__guard_ptr{__a,this};
this->~_Sp_counted_ptr_inplace();
}

private:
friendclass__shared_count<_Lp>;//Tobeabletocall_M_ptr().

_Tp*_M_ptr()noexcept{return_M_impl._M_storage._M_ptr();}

_Impl_M_impl;
};

这个类继承于_Sp_counted_base,这个类定义不再次列出,需要注意的是其中有两个变量:

_Atomic_word_M_use_count;//#shared
_Atomic_word_M_weak_count;//#weak+(#shared!=0)

第一个为强引用技术,也就是shared对象引用计数,另外一个为弱因为计数。

继续看这个类,里面定义了一个class _Impl,其中我们创建的对象类型就在这个类里面定义,即**__gnu_cxx::__aligned_buffer<_Tp> _M_storage;**

接着看L2,这行定义了一个对象__a2,其对象类型为using __allocator_type = __alloc_rebind<_Alloc, _Sp_counted_ptr_inplace>;,这行的意思是重新封装rebind_alloc<_Sp_counted_ptr_inplace>

继续看L3,在这一行中会创建一块内存,这块内存中按照顺序为创建对象、强引用计数、弱引用计数等(也就是说分配一大块内存,这块内存中 包含对象、强、弱引用计数所需内存等),在创建这块内存的时候,强、弱引用计数已经被初始化

最后是L3,这块调用了placement new来创建,其中调用了对象的构造函数:

template
_Sp_counted_ptr_inplace(_Alloc__a,_Args&&...__args)
:_M_impl(__a)
{
//_GLIBCXX_RESOLVE_LIB_DEFECTS
//2070.allocate_sharedshoulduseallocator_traits::construct
allocator_traits<_Alloc>::construct(__a,_M_ptr(),
std::forward<_Args>(__args)...);//mightthrow
}

至此,整个std::make_shared流量已经完整的梳理完毕,最后返回一个shared_ptr对象。

好了,下面继续看下令人迷惑的,存在大内存不分配的这行代码:

newweak_ptr{a0};

其对应的构造函数如下:

template>
__weak_ptr(const__shared_ptr<_Yp, _Lp>&__r)noexcept
:_M_ptr(__r._M_ptr),_M_refcount(__r._M_refcount)
{}

其中_M_refcount的类型为__weak_count,而\__r._M_refcount即常说的强引用计数类型为__shared_count,其继承于接着往下看:

__weak_count(const__shared_count<_Lp>&__r)noexcept
:_M_pi(__r._M_pi)
{
if(_M_pi!=nullptr)
_M_pi->_M_weak_add_ref();
}

emm,弱引用计数加1,也就是说此时_M_weak_count为1。

接着,退出作用域,此时有std::make_shared创建的对象开始释放,因此其内部的成员变量r._M_refcount也跟着释放:

~__shared_count()noexcept
{
if(_M_pi!=nullptr)
_M_pi->_M_release();
}

接着往下看_M_release()实现:

template<>
inlinevoid
_Sp_counted_base<_S_single>::_M_release()noexcept
{
if(--_M_use_count==0)
{
_M_dispose();
if(--_M_weak_count==0)
_M_destroy();
}
}

此时,因为shared_ptr对象的引用计数本来就为1(没有其他地方使用),所以if语句成立,执行_M_dispose()函数,在分析这个函数之前,先看下前面提到的代码:

__shared_ptr(_Sp_alloc_shared_tag<_Alloc>__tag,_Args&&...__args)
:_M_ptr(),_M_refcount(_M_ptr,__tag,std::forward<_Args>(__args)...)
{_M_enable_shared_from_this_with(_M_ptr);}

因为是使用std::make_shared()进行创建的,所以_M_ptr为空,此时传入_M_refcount的第一个参数也为空。接着看_M_dispose()定义:

template<>
inlinevoid
_Sp_counted_ptr::_M_dispose()noexcept{}

template<>
inlinevoid
_Sp_counted_ptr::_M_dispose()noexcept{}

template<>
inlinevoid
_Sp_counted_ptr::_M_dispose()noexcept{}

因为传入的指针为nullptr,因此调用了_Sp_counted_ptr的特化版本,因此_M_dispose()这个函数什么都没做。因为_M_pi->_M_weak_add_ref();这个操作,此时这个计数经过减1之后不为0,因此没有没有执行_M_destroy()操作,因此之前申请的大块内存没有被释放,下面是_M_destroy()实现:

virtualvoid
_M_destroy()noexcept
{
__allocator_type__a(_M_impl._M_alloc());
__allocated_ptr<__allocator_type>__guard_ptr{__a,this};
this->~_Sp_counted_ptr_inplace();
}

也就是说真正调用了这个函数,内存才会被分配,示例代码中,显然不会,这就是造成内存一直不被释放的原因。

总结

下面解释下我当时阅读这块代码最难理解的部分,下面是make_shared执行过程:

wKgZomXS6xCAPfHOAAFuI1MFt6A914.jpg

下面是析构过程:

wKgZomXS6yaAf3q-AABxX48GAd0214.jpg

整体看下来,比较重要的一个类就是_Sp_counted_base 不仅充当引用计数功能,还充当内存管理功能。从上面的分析可以看到,_Sp_counted_base负责释放用户申请的申请的内存,即

当 _M_use_count 递减为 0 时,调用 _M_dispose() 释放 *this 管理的资源当 _M_weak_count 递减为 0 时,调用 _M_destroy() 释放 *this 对象




审核编辑:刘清

  • STD
    STD
    +关注

    关注

    0

    文章

    35

    浏览量

    14231
  • 变量
    +关注

    关注

    0

    文章

    595

    浏览量

    28112
  • 内存泄漏
    +关注

    关注

    0

    文章

    38

    浏览量

    9166

原文标题:一次诡异的内存泄漏

文章出处:【微信号:CPP开发者,微信公众号:CPP开发者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    一次读取大数据占用内存很大

    本帖最后由 elecfans跑堂 于 2015-8-31 10:50 编辑 在编写个读取wav文件的程序,发现一次读取40M以上的文件,消耗内存1G左右,太吓人了,请高手指点啊http
    发表于 08-31 09:40

    使用HAProxy软件的一次学习过程介绍

    一次学习过程
    发表于 06-13 10:48

    AliOS Things 维测典型案例分析 —— 内存泄漏

    个已经压测出来的问题出发,通过维测工具的使用,来看一次内存泄漏的分析。1. 问题现象:xx平台压测反复断AP电源第488连接通道时出现dump机现象**2. 重现步骤: 设备认证连接
    发表于 10-17 11:29

    一次网站设计稿的方法

    一次网站设计稿
    发表于 06-16 09:43

    内存泄漏定位该如何去实现呢

    。对于内存泄漏的情况,如果开始不做预防,定位内存泄漏就会相当繁琐,定位也会很长,非常的耗时、耗力。这里可通过malloc、free的第二
    发表于 12-17 07:24

    写了内存泄漏检查工具

    嵌入式环境内存泄漏检查比较麻烦,valgrind比较适合于在pc上跑,嵌入式上首先移植就很麻烦,移植完了内存比较小,跑起来也比较费劲。所以手动写了
    发表于 12-17 08:25

    分享内存泄漏定位排查技巧

    常见的泄漏方式在嵌入式开发中,经常会使用malloc,free分配释放堆内存,稍不小心就可能导致内存点点地泄露,直至堆内存泄露完,导致设备
    发表于 12-17 08:13

    什么是内存泄漏内存泄漏有哪些现象

    内存泄漏几乎是很难避免的,不管是老手还是新手,都存在这个问题,甚至 Windows 与 Linux 这类系统软件也或多或少存在着内存泄漏
    的头像 发表于 09-05 17:24 8942次阅读

    怎么解决C语言中的内存泄漏问题呢?

    只有在堆内存里面才会发生内存泄漏的问题,在栈内存中不会发生内存泄漏。因为栈
    发表于 06-11 17:31 392次阅读
    怎么解决C语言中的<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>问题呢?

    一次Rust内存泄漏排查之旅

    在某次持续压测过程中,我们发现 GreptimeDB 的 Frontend 节点内存即使在请求量平稳的阶段也在持续上涨,直至被 OOM kill。我们判断 Frontend 应该是有内存泄漏了,于是开启了排查
    的头像 发表于 07-02 11:52 418次阅读
    记<b class='flag-5'>一次</b>Rust<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>排查之旅

    什么是内存泄漏?如何避免JavaScript内存泄漏

    JavaScript 代码中常见的内存泄漏的常见来源: 研究内存泄漏问题就相当于寻找符合垃圾回收机制的编程方式,有效避免对象引用的问题。
    发表于 10-27 11:30 128次阅读
    什么是<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>?如何避免JavaScript<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>

    内存泄漏如何避免

    的数,那就是内存溢出。 2. 内存泄漏 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的
    的头像 发表于 11-10 11:04 375次阅读
    <b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>如何避免

    内存泄漏会产生哪些后果

    内存泄漏原因 内存泄漏在C/C++这种不带GC(Garbage Collection)的语言里,是一个经常发生的问题。因为没有GC,所以分配的内存
    的头像 发表于 11-10 15:06 393次阅读
    <b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>会产生哪些后果

    线程内存泄漏问题的定位

    记录一个关于线程内存泄漏问题的定位过程,以及过程中的收获。 1. 初步定位 是否存在内存泄漏:想到内存
    的头像 发表于 11-13 11:38 322次阅读
    线程<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>问题的定位

    C语言内存泄漏问题原理

    内存泄漏问题只有在使用堆内存的时候才会出现,栈内存不存在内存泄漏问题,因为栈
    发表于 03-19 11:38 163次阅读
    C语言<b class='flag-5'>内存</b><b class='flag-5'>泄漏</b>问题原理