0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

記一次詭異的內(nèi)存泄漏

CPP開發(fā)者 ? 來源:高性能架構(gòu)探索 ? 2024-02-19 13:44 ? 次閱讀

緣起

最近在補一些基礎(chǔ)知識,恰好涉及到了智能指針std::weak_ptr在解決std::shared_ptr時候循環(huán)引用的問題,如下:

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會有什么問題?

咱們先不論這個問題本身,在隨后的討論中,風(fēng)神突然貼了段代碼:

#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;
}

說實話,當(dāng)初看了這個代碼第一眼,是存在內(nèi)存泄漏的(new一個weak_ptr沒有釋放),而沒有理解風(fēng)神這段代碼真正的含義,于是在本地把這段代碼編譯運行了下,我的乖乖,內(nèi)存占用如圖:

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

emm,雖然存在內(nèi)存泄漏,但也不至于這么大,于是網(wǎng)上進行了搜索,直至我看到了下面這段話:

make_shared 只分配一次內(nèi)存, 這看起來很好. 減少了內(nèi)存分配的開銷. 問題來了, weak_ptr 會保持控制塊(強引用, 以及弱引用的信息)的生命周期, 而因此連帶著保持了對象分配的內(nèi)存, 只有最后一個 weak_ptr 離開作用域時, 內(nèi)存才會被釋放. 原本強引用減為 0 時就可以釋放的內(nèi)存, 現(xiàn)在變?yōu)榱藦娨? 若引用都減為 0 時才能釋放, 意外的延遲了內(nèi)存釋放的時間. 這對于內(nèi)存要求高的場景來說, 是一個需要注意的問題.

如果介意上面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

好了,理由上面已經(jīng)說明白了,不再贅述了,如果你想繼續(xù)分析的話,請看下文,否則~~

原因

雖然上節(jié)給出了原因,不過還是好奇心驅(qū)使,想從源碼角度去了解下,于是打開了好久沒看的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)...);
}

這個函數(shù)函數(shù)體只有一個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)...);
}

創(chuàng)建了一個shared_ptr對象,看下其對應(yīng)的構(gòu)造函數(shù):

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

接著看__shared_ptr這個類對應(yīng)的構(gòu)造函數(shù):

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,也就是說我們通常所說的引用計數(shù)就是由其來管理。

因為調(diào)用make_shared函數(shù),所以這里的_M_ptr指針也就是相當(dāng)于一個空指針,然后繼續(xù)看下_M_refcount(請注意_M_ptr作為參數(shù)傳入)定義:

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
}

這塊代碼當(dāng)時看了很多遍,一直不明白在沒有顯示分配對象內(nèi)存的情況下,是如何使用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)

第一個為強引用技術(shù),也就是shared對象引用計數(shù),另外一個為弱因為計數(shù)。

繼續(xù)看這個類,里面定義了一個class _Impl,其中我們創(chuàng)建的對象類型就在這個類里面定義,即**__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>

繼續(xù)看L3,在這一行中會創(chuàng)建一塊內(nèi)存,這塊內(nèi)存中按照順序為創(chuàng)建對象、強引用計數(shù)、弱引用計數(shù)等(也就是說分配一大塊內(nèi)存,這塊內(nèi)存中 包含對象、強、弱引用計數(shù)所需內(nèi)存等),在創(chuàng)建這塊內(nèi)存的時候,強、弱引用計數(shù)已經(jīng)被初始化

最后是L3,這塊調(diào)用了placement new來創(chuàng)建,其中調(diào)用了對象的構(gòu)造函數(shù):

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流量已經(jīng)完整的梳理完畢,最后返回一個shared_ptr對象。

好了,下面繼續(xù)看下令人迷惑的,存在大內(nèi)存不分配的這行代碼:

newweak_ptr{a0};

其對應(yīng)的構(gòu)造函數(shù)如下:

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即常說的強引用計數(shù)類型為__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,弱引用計數(shù)加1,也就是說此時_M_weak_count為1。

接著,退出作用域,此時有std::make_shared創(chuàng)建的對象開始釋放,因此其內(nèi)部的成員變量r._M_refcount也跟著釋放:

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

接著往下看_M_release()實現(xiàn):

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對象的引用計數(shù)本來就為1(沒有其他地方使用),所以if語句成立,執(zhí)行_M_dispose()函數(shù),在分析這個函數(shù)之前,先看下前面提到的代碼:

__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()進行創(chuàng)建的,所以_M_ptr為空,此時傳入_M_refcount的第一個參數(shù)也為空。接著看_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,因此調(diào)用了_Sp_counted_ptr的特化版本,因此_M_dispose()這個函數(shù)什么都沒做。因為_M_pi->_M_weak_add_ref();這個操作,此時這個計數(shù)經(jīng)過減1之后不為0,因此沒有沒有執(zhí)行_M_destroy()操作,因此之前申請的大塊內(nèi)存沒有被釋放,下面是_M_destroy()實現(xiàn):

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

也就是說真正調(diào)用了這個函數(shù),內(nèi)存才會被分配,示例代碼中,顯然不會,這就是造成內(nèi)存一直不被釋放的原因。

總結(jié)

下面解釋下我當(dāng)時閱讀這塊代碼最難理解的部分,下面是make_shared執(zhí)行過程:

wKgZomXS6xCAPfHOAAFuI1MFt6A914.jpg

下面是析構(gòu)過程:

wKgZomXS6yaAf3q-AABxX48GAd0214.jpg

整體看下來,比較重要的一個類就是_Sp_counted_base 不僅充當(dāng)引用計數(shù)功能,還充當(dāng)內(nèi)存管理功能。從上面的分析可以看到,_Sp_counted_base負責(zé)釋放用戶申請的申請的內(nèi)存,即

?當(dāng) _M_use_count 遞減為 0 時,調(diào)用 _M_dispose() 釋放 *this 管理的資源?當(dāng) _M_weak_count 遞減為 0 時,調(diào)用 _M_destroy() 釋放 *this 對象




審核編輯:劉清

  • STD
    STD
    +關(guān)注

    關(guān)注

    0

    文章

    36

    瀏覽量

    14359
  • 變量
    +關(guān)注

    關(guān)注

    0

    文章

    613

    瀏覽量

    28370
  • 內(nèi)存泄漏
    +關(guān)注

    關(guān)注

    0

    文章

    39

    瀏覽量

    9218

原文標(biāo)題:一次詭異的內(nèi)存泄漏

文章出處:【微信號:CPP開發(fā)者,微信公眾號:CPP開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    【freeRTOS開發(fā)筆記】一次坑爹的freeTOS升級

    【freeRTOS開發(fā)筆記】一次坑爹的freeTOS-v9.0.0升級到freeRTOS-v10.4.4
    的頭像 發(fā)表于 07-11 09:15 ?4644次閱讀
    【freeRTOS開發(fā)筆記】<b class='flag-5'>記</b><b class='flag-5'>一次</b>坑爹的freeTOS升級

    AliOS Things 維測典型案例分析 —— 內(nèi)存泄漏

    個已經(jīng)壓測出來的問題出發(fā),通過維測工具的使用,來看一次內(nèi)存泄漏的分析。1. 問題現(xiàn)象:xx平臺壓測反復(fù)斷AP電源第488連接通道時出現(xiàn)dump機現(xiàn)象**2. 重現(xiàn)步驟: 設(shè)備認證連接
    發(fā)表于 10-17 11:29

    一次網(wǎng)站設(shè)計稿的方法

    一次網(wǎng)站設(shè)計稿
    發(fā)表于 06-16 09:43

    內(nèi)存泄漏定位該如何去實現(xiàn)呢

    。對于內(nèi)存泄漏的情況,如果開始不做預(yù)防,定位內(nèi)存泄漏就會相當(dāng)繁瑣,定位也會很長,非常的耗時、耗力。這里可通過malloc、free的第二
    發(fā)表于 12-17 07:24

    寫了內(nèi)存泄漏檢查工具

    嵌入式環(huán)境內(nèi)存泄漏檢查比較麻煩,valgrind比較適合于在pc上跑,嵌入式上首先移植就很麻煩,移植完了內(nèi)存比較小,跑起來也比較費勁。所以手動寫了
    發(fā)表于 12-17 08:25

    分享內(nèi)存泄漏定位排查技巧

    常見的泄漏方式在嵌入式開發(fā)中,經(jīng)常會使用malloc,free分配釋放堆內(nèi)存,稍不小心就可能導(dǎo)致內(nèi)存點點地泄露,直至堆內(nèi)存泄露完,導(dǎo)致設(shè)備
    發(fā)表于 12-17 08:13

    嵌入式裝置內(nèi)存泄漏檢測系統(tǒng)設(shè)計

    ,極易出現(xiàn)應(yīng)用程序內(nèi)存泄漏。內(nèi)存泄漏按照發(fā)生的頻率可分為常發(fā)性、偶發(fā)性、一次性以及隱式內(nèi)存
    發(fā)表于 04-26 14:35 ?3次下載
    嵌入式裝置<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>檢測系統(tǒng)設(shè)計

    什么是內(nèi)存泄漏內(nèi)存泄漏有哪些現(xiàn)象

    內(nèi)存泄漏幾乎是很難避免的,不管是老手還是新手,都存在這個問題,甚至 Windows 與 Linux 這類系統(tǒng)軟件也或多或少存在著內(nèi)存泄漏。
    的頭像 發(fā)表于 09-05 17:24 ?9700次閱讀

    一次性輸液器泄漏正負壓檢測儀

    一次性輸液器泄漏正負壓測試儀是根據(jù)《GB8368-2018一次性使用輸液器 重力輸液式》中的相關(guān)條款設(shè)計研發(fā)制造的,是款專業(yè)用于檢測一次
    發(fā)表于 01-28 16:44 ?860次閱讀
    <b class='flag-5'>一次</b>性輸液器<b class='flag-5'>泄漏</b>正負壓檢測儀

    一次性輸液器泄漏正負壓檢測儀

    一次性輸液器泄漏正負壓測試儀是根據(jù)《GB8368-2018一次性使用輸液器 重力輸液式》中的相關(guān)條款設(shè)計研發(fā)制造的,是款專業(yè)用于檢測一次
    的頭像 發(fā)表于 01-29 15:30 ?1146次閱讀
    <b class='flag-5'>一次</b>性輸液器<b class='flag-5'>泄漏</b>正負壓檢測儀

    一次Rust內(nèi)存泄漏排查之旅

    在某次持續(xù)壓測過程中,我們發(fā)現(xiàn) GreptimeDB 的 Frontend 節(jié)點內(nèi)存即使在請求量平穩(wěn)的階段也在持續(xù)上漲,直至被 OOM kill。我們判斷 Frontend 應(yīng)該是有內(nèi)存泄漏了,于是開啟了排查
    的頭像 發(fā)表于 07-02 11:52 ?673次閱讀
    <b class='flag-5'>記</b><b class='flag-5'>一次</b>Rust<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>排查之旅

    什么是內(nèi)存泄漏?如何避免JavaScript內(nèi)存泄漏

    JavaScript 代碼中常見的內(nèi)存泄漏的常見來源: 研究內(nèi)存泄漏問題就相當(dāng)于尋找符合垃圾回收機制的編程方式,有效避免對象引用的問題。
    發(fā)表于 10-27 11:30 ?399次閱讀
    什么是<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>?如何避免JavaScript<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>

    內(nèi)存泄漏如何避免

    的數(shù),那就是內(nèi)存溢出。 2. 內(nèi)存泄漏 內(nèi)存泄露 memory leak,是指程序在申請內(nèi)存后,無法釋放已申請的
    的頭像 發(fā)表于 11-10 11:04 ?748次閱讀
    <b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>如何避免

    線程內(nèi)存泄漏問題的定位

    記錄個關(guān)于線程內(nèi)存泄漏問題的定位過程,以及過程中的收獲。 1. 初步定位 是否存在內(nèi)存泄漏:想到內(nèi)存
    的頭像 發(fā)表于 11-13 11:38 ?619次閱讀
    線程<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>問題的定位

    C語言內(nèi)存泄漏問題原理

    內(nèi)存泄漏問題只有在使用堆內(nèi)存的時候才會出現(xiàn),棧內(nèi)存不存在內(nèi)存泄漏問題,因為棧
    發(fā)表于 03-19 11:38 ?527次閱讀
    C語言<b class='flag-5'>內(nèi)存</b><b class='flag-5'>泄漏</b>問題原理