前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一次诡异的内存泄漏

一次诡异的内存泄漏

作者头像
高性能架构探索
发布2024-01-15 18:45:49
1520
发布2024-01-15 18:45:49
举报
文章被收录于专栏:技术随笔心得技术随笔心得

你好,我是雨乐!

前几天的时候,群里聊到了个问题,感觉很有意思,所以借助本文分享下~~

缘起

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

代码语言:javascript
复制
class A {
public:
    std::weak_ptr<B> b_ptr;
};

class B {
public:
    std::weak_ptr<A> a_ptr;
};

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

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

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

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

代码语言:javascript
复制
#include <chrono>
#include <memory>
#include <thread>

using namespace std;

struct A {
    char buffer[1024 * 1024 * 1024]; // 1GB
    weak_ptr<A> next;
};

int main() {
    while (true) {
        auto a0 = make_shared<A>();
        auto a1 = make_shared<A>();
        auto a2 = make_shared<A>();
        a0->next = a1;
        a1->next = a2;
        a2->next = a0;
        // this weak_ptr leak:
        new weak_ptr<A>{a0};
        this_thread::sleep_for(chrono::seconds(3));
    }
    return 0;
}

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

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

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

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

代码语言:javascript
复制
#include <chrono>
#include <memory>
#include <thread>

using namespace std;

struct A {
    char buffer[1024 * 1024 * 1024]; // 1GB
    weak_ptr<A> next;
};

int main() {
    std::weak_ptr<A> wptr;
    {
        auto sptr = make_shared<A>();
        wptr = sptr;
    }
    
    this_thread::sleep_for(chrono::seconds(30));
    return 0;
}

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

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

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

原因

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

std::make_shared

首先看下它的定义:

代码语言:javascript
复制
template<typename _Tp, typename... _Args>
inline shared_ptr<_Tp> make_shared(_Args&&... __args) {
  typedef typename std::remove_cv<_Tp>::type _Tp_nc;
  return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
      std::forward<_Args>(__args)...);
}

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

代码语言:javascript
复制
template<typename _Tp, typename _Alloc, typename... _Args>
inline shared_ptr<_Tp>
allocate_shared(const _Alloc& __a, _Args&&... __args) {
  return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
      std::forward<_Args>(__args)...);
}

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

代码语言:javascript
复制
template<typename _Alloc, typename... _Args>
shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
  : __shared_ptr<_Tp>(__tag, std::forward<_Args>(__args)...) { }

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

代码语言:javascript
复制
template<typename _Alloc, typename... _Args>
__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作为参数传入)定义:

代码语言:javascript
复制
template<typename _Tp, typename _Alloc, typename... _Args>
__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,其定义如下:

代码语言:javascript
复制
  template<typename _Tp, typename _Alloc, _Lock_policy _Lp>
    class _Sp_counted_ptr_inplace final : 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>;

      // Alloc parameter is not a reference so doesn't alias anything in __args
      template<typename... _Args>
    _Sp_counted_ptr_inplace(_Alloc __a, _Args&&... __args)
    : _M_impl(__a)
    {
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // 2070.  allocate_shared should use allocator_traits<A>::construct
      allocator_traits<_Alloc>::construct(__a, _M_ptr(),
          std::forward<_Args>(__args)...); // might throw
    }

      ~_Sp_counted_ptr_inplace() noexcept { }

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

      // Override because the allocator needs to know the dynamic type
      virtual void
      _M_destroy() noexcept
      {
    __allocator_type __a(_M_impl._M_alloc());
    __allocated_ptr<__allocator_type> __guard_ptr{ __a, this };
    this->~_Sp_counted_ptr_inplace();
      }

    private:
      friend class __shared_count<_Lp>; // To be able to call _M_ptr().

      _Tp* _M_ptr() noexcept { return _M_impl._M_storage._M_ptr(); }

      _Impl _M_impl;
    };

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

代码语言:javascript
复制
 _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来创建,其中调用了对象的构造函数:

代码语言:javascript
复制
 template<typename... _Args>
    _Sp_counted_ptr_inplace(_Alloc __a, _Args&&... __args)
    : _M_impl(__a)
    {
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // 2070.  allocate_shared should use allocator_traits<A>::construct
      allocator_traits<_Alloc>::construct(__a, _M_ptr(),
          std::forward<_Args>(__args)...); // might throw
    }

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

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

代码语言:javascript
复制
new weak_ptr<A>{a0};

其对应的构造函数如下:

代码语言:javascript
复制
template<typename _Yp, typename = _Compatible<_Yp>>
    __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,其继承于接着往下看:

代码语言:javascript
复制
 __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也跟着释放:

代码语言:javascript
复制
 ~__shared_count() noexcept
      {
    if (_M_pi != nullptr)
      _M_pi->_M_release();
      }

接着往下看_M_release()实现:

代码语言:javascript
复制
template<>
    inline void
    _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()函数,在分析这个函数之前,先看下前面提到的代码:

代码语言:javascript
复制
__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()定义:

代码语言:javascript
复制
template<>
    inline void
    _Sp_counted_ptr<nullptr_t, _S_single>::_M_dispose() noexcept { }

  template<>
    inline void
    _Sp_counted_ptr<nullptr_t, _S_mutex>::_M_dispose() noexcept { }

  template<>
    inline void
    _Sp_counted_ptr<nullptr_t, _S_atomic>::_M_dispose() noexcept { }

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

代码语言:javascript
复制
 virtual void
      _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执行过程:

?_Sp_counted_base有两个成员变量,分别为_M_use_count用于表示强引用计数和_M_weak_count用于表示弱引用计数?__shared_count继承于_Sp_counted_base,其内部有一个变量_M_ptr指向对象指针?__shared_ptr中存在成员变量__shared_count?使用make_shared()函数创建shared_ptr时候,会创建__shared_count对象?在创建__shared_count对象时候,存在变量_Sp_counted_ptr_inplace?_Sp_counted_ptr_inplace继承于_Sp_counted_base?_Sp_counted_ptr_inplace会存在一个class Impl,其中包含对象Obj?在创建其内部会使用rebind重新封装rebind_alloc<_Sp_counted_ptr_inplace>?_Sp_counted_ptr_inplace获取其分配器,然后进程get()操作,此时会创建一块大小为_Sp_counted_ptr_inplace的对象(这个大小为Obj大小 + 其本身继承的_Sp_counted_base大小等),且顺序为对象、强引用计数、弱引用计数等?使用placement new对上述分配的内存块进行初始化(只初始化Obj大小不符,引用计数等已经初始化完成)?创建shared_ptr,因为使用的make_shared初始化,所以传入的指针为空,相应的_Sp_counted_base中的_M_ptr也为空

下面是析构过程:

?析构shared_ptr对象的同时,释放其内部成员变量_M_use_count?_M_use_count调用Release()函数,在这个函数中,如果强引用计数为1,则调用_M_dispose()函数?在_M_dispose()中,会调用_Sp_counted_ptr的特化版本,即不进行任何操作?判断弱因为计数是否为1,如果是则调用_M_destroy(),此时才会真正的释放内存块

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

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

最后,借助群里经常说的一句话cpp? 狗都不学结束本文。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-10,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 高性能架构探索 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 缘起
  • 原因
    • std::make_shared
    • 总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
    http://www.vxiaotou.com