【问题标题】:Finding where a shared_ptr's reference count is incremented查找 shared_ptr 的引用计数在哪里增加
【发布时间】:2015-02-03 04:43:47
【问题描述】:

我有一些代码存在内存泄漏,因为它在其shared_ptr 实例之间循环引用(这是两个shared_ptr 实例指向对象的地方,每个对象都有一个对另一个类实例的内部shared_ptr 引用。这意味着两个类都不会被销毁,因为每个类实例仍在被另一个类使用,从而导致内存泄漏。在某些情况下,它也是一个引用自身的类的单个 shared_ptr 实例。)

通过 Valgrind 运行代码很有帮助,因为它告诉我最初分配内存的位置,但这不是循环引用的来源。我需要找到特定共享指针(Valgrind 抱怨的那个)的引用计数增加的所有地方,因为其中一个必须更改为 weak_ptr 才能解决问题。

如何选择特定的shared_ptr 并获取其引用计数增加的所有源代码行的列表?

我正在使用 GCC/GDB 和 Valgrind 在 Linux 下运行,但欢迎使用平台中立的解决方案。

这里有一些示例代码来演示这个问题:

#include <boost/shared_ptr.hpp>

struct Base {
    int i;
};
struct A: public Base {
    int a;
    boost::shared_ptr<Base> ptrInA;
};
struct B: public Base {
    int b;
    boost::shared_ptr<Base> ptrInB;
};

int main(void)
{
    boost::shared_ptr<A> a(new A);   // Line 17
    boost::shared_ptr<B> b(new B);
    a->ptrInA = b;                   // Line 19
    b->ptrInB = a;
    return 0;
}

在 Valgrind 下运行时,它会说:

HEAP SUMMARY:
    in use at exit: 96 bytes in 4 blocks
  total heap usage: 4 allocs, 0 frees, 96 bytes allocated

96 (24 direct, 72 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4
   at 0x4C2A4F0: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
   by 0x40099A: main (test.cpp:17)

LEAK SUMMARY:
   definitely lost: 24 bytes in 1 blocks
   indirectly lost: 72 bytes in 3 blocks

我正在寻找一种解决方案,将源文件中的第 19-20 行作为循环的可能原因,以便我可以检查代码并决定是否需要对其进行更改。

【问题讨论】:

  • 暂时将代码中的定义更改为unique_ptr 怎么样,因为这会在复制共享指针的所有位置出现编译错误。
  • 我要推荐一个可怕的东西,但是调整 shared_ptr 并放入钩子以查看发生了什么事情。 :X

标签: c++ memory-leaks valgrind shared-ptr


【解决方案1】:

你有一个设计错误。因此,您需要使用设计调试工具。

准备一支笔和一张纸。
为每个类类型绘制一个矩形。从每个拥有 shared_ptr 的类中画一个箭头指向它拥有的类。
如果你找到一个圈子,那是你的问题。

现在,每个箭头或链接都是通过 shared_ptr 分配在某处创建的。
查看可疑的箭头,那些关闭圆圈的箭头,看看它们是否正确释放。

【讨论】:

    【解决方案2】:

    虽然 Yochai Timmer 的方法适用于较小的项目,但我最近不得不处理一个相当大的代码库,该代码库到处都使用了 shared_ptr,Boost 变体。这是在 Windows 上,UI 是在 MFC 之上完成的,shared_ptr 指向主要的CWinApp 派生应用程序类。而且它永远不会被破坏,导致大量的析构函数没有被调用,结果是一些令人讨厌的行为。

    在尝试了各种泄漏检测器之后,我通过让调试器在访问有问题的shared_ptr 的第一行中断,然后搜索相关标头直到找到引用计数器的确切位置来解决问题。然后,我在引用计数器的地址中添加了一个内存断点,并让 VS 调试器在每次递增/递减时中断,直到 ref ctr 的值未能回落到其“正常”值。

    就我而言,我知道在应用程序初始化完成后,shared_ptr 的引用计数不应 > 2。当它达到 3 并且再也没有回落到 2 时,我知道我找到了泄漏。而且内存断点只需要被命中1000次左右……

    是的,我确信有更好的方法来追踪涉及shared_ptr 的内存泄漏,但如果所有其他方法都失败了,那么总是有观察引用计数器的蛮力方法。当然,细节取决于您的shared_ptr 实现以及您的应用程序的组织方式。

    【讨论】:

      【解决方案3】:

      基于@dandan78 的方法。这是一个更详细的 GDB 示例。

      main.cpp:

      #include <iostream>
      #include <memory>
      
      using namespace std;
      
      #define DBG(msg) std::cout << msg << std::endl;
      
      class A {
          public:
              A(int i) {
                  mI = i;
                  DBG("A() this:"<<this<<" i:"<<mI);
              }
              ~A() {
                  DBG("~A() this:"<<this<<" i:"<<mI);
              }
          private:
              int mI = 0;
      };
      
      int main() {
          std::shared_ptr<A> p1(new A(0x12345678));
          DBG("p1 use_count:"<<p1.use_count());
          {
              auto p2 = p1;
              DBG("p1 use_count:"<<p1.use_count());
              DBG("p2 use_count:"<<p2.use_count());
              auto p3 = p1;
              DBG("p1 use_count:"<<p1.use_count());
              DBG("p2 use_count:"<<p2.use_count());
              DBG("p3 use_count:"<<p3.use_count());
          }
          DBG("p1 use_count:"<<p1.use_count());
          return 0;
      }
      

      生成文件:

      CXXFLAGS = -O0 -ggdb
      
      main: main.cpp
          $(CXX) $(CXXFLAGS) -o $@ $<
      

      程序的输出:

      A() this:0x6c6fb0 i:305419896
      p1 use_count:1
      p1 use_count:2
      p2 use_count:2
      p1 use_count:3
      p2 use_count:3
      p3 use_count:3
      p1 use_count:1
      ~A() this:0x6c6fb0 i:305419896
      

      编译并运行 gdb(不要将 # cmets 粘贴到 gdb):

      make
      gdb main 2>&1 | tee out.log
      

      GDB 会话:

      (gdb) b main.cpp:23   # right after the p1 initialization
      (gdb) r
      Thread 1 hit Breakpoint 1, main () at main.cpp:23
      (gdb) x/2xg &p1
      0x62fe00:       0x0000000000fd4a10      0x0000000000fd4a50
      # First pointer points to the target A object, sencond points to the reference counter
      # Inspect the refcount data:
      (gdb) x/4xw 0x0000000000fd4a50
      0xfd4a50:       0x00405670      0x00000000      0x00000003      0x00000001
      # The third integer is use_count of the shared_ptr, which can be printed by:
      (gdb) x/1xw 0x0000000000fd4a50 + 8
      0xfd4a58:       0x00000001
      
      # Add a watchpoint for the use_count address
      (gdb) watch *(int*)(0x0000000000fd4a50 + 8)
      Hardware watchpoint 2: *(int*)(0x0000000000fd4a50 + 8)
      # Add commands for the new watchpoint 2:
      (gdb) commands 2
      bt             # backtrace
      c              # continue
      end            # end of the handler script
      
      (gdb) c        # Continue the program
      

      现在您可以检查 out.log 文件并分析 use_count 更改的所有回溯。

      gdb watchpoint 也可以直接添加:

      watch *(*((int**)(&p1) + 1) + 2)
                         ^--------------- the shared_ptr variable
                               ^--------- +1 pointer to the right (+8 bytes in 64bit programm)
                                    ^---- +2 integers to the right (+8 bytes)
      

      如果您使用优化进行编译,shared_ptr 变量可能已被优化。只需将其直接打印到您的代码中,然后获取 shared_ptr 对象的地址并将其粘贴到您的 gdb 会话中:

      std::cout << "p1:" << (void*)&p1 << std::endl;
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-06-24
        • 1970-01-01
        • 2011-02-24
        • 1970-01-01
        • 2016-07-30
        • 2015-02-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多