内存未释放 std::list<std::shared_ptr<std::string>> C++

Memory not freeing up std::list<std::shared_ptr<std::string>> C++

我正在填充一个字符串共享指针列表。在我的程序中的某个时刻,我清除了列表。但是即使我调用了列表的clear()函数,我的程序的内存消耗也没有减少。知道为什么吗?

#include <memory>
#include <string>
#include <list>
#include <iostream>

int main()
{
    std::string text = "                                                            \
        PREFACE                                                                     \
                                                                                    \
        Most of the adventures recorded in this book really occurred; one or two    \
        were experiences of my own, the rest those of boys who were schoolmates     \
        of mine. Huck Finn is drawn from life; Tom Sawyer also, but not from an     \
        individual--he is a combination of the characteristics of three boys whom   \
        I knew, and therefore belongs to the composite order of architecture.       \
                                                                                    \
        The odd superstitions touched upon were all prevalent among children and    \
        slaves in the West at the period of this story--that is to say, thirty or   \
        forty years ago.                                                            \
                                                                                    \
        Although my book is intended mainly for the entertainment of boys and       \
        girls, I hope it will not be shunned by men and women on that account,      \
        for part of my plan has been to try to pleasantly remind adults of what     \
        they once were themselves, and of how they felt and thought and talked,     \
        and what queer enterprises they sometimes engaged in.                       \
    ";

    std::list<std::shared_ptr<std::string>> data;
    for (auto i = 0u; i < 999999; ++i) {
        data.push_back(std::make_shared<std::string>(text));
    }

    std::cout << "Data loaded. Press any key to continue...";
    std::cin.get();

    data.clear(); // memory does not reduce
    std::cout << "Data unloaded. Press any key to continue...";
    std::cin.get();

    std::cout << "Container size:" << data.size() << std::endl;
    std::cout << "Press any key to exit...";
    std::cin.get();

    return 0;
}

我正在 WSL 上调试。我同时使用 linux top(在 WSL 上)和 Windows 任务管理器来检查每个站点的内存使用情况。但是 valgrind 没有报告任何内存泄漏。

P.S。请不要问我为什么要使用共享指针。提出不同的方法会很有用,但这个问题的主要目的是理解这种行为。因为即使 cppref 也没有解释这一点。感谢有人可以解释这种行为而不是修复它。

我不需要修复。我需要一个解释。

正如 Nicolai Josuttis 在 this talk 中提到的,make_shared 不会调用 new 两次,而是只为控制块和您指向的资源分配一个内存,因此它不会释放内存块,直到所有引用该控制块的共享指针和弱指针都被删除

但是在这种情况下,不再有 shared/weak 指针指向那些控制块,事实上,如果您不使用 std::string 而使用您的对象,您将看到析构函数被调用为clear() 通话:

#include <memory>
#include <string>
#include <list>
#include <iostream>
class A{
public:
    ~A(){std::cout<<"destructor"<<std::endl;}
};
int main()
{

    std::list<std::shared_ptr<A>> data;
    for (auto i = 0u; i < 10; ++i) {
        data.push_back(std::make_shared<A>());
    }

    std::cout << "Data loaded. Press any key to continue...";
    std::cin.get();

    data.clear(); // memory does not reduce
    std::cout << "Data unloaded. Press any key to continue...";
    std::cin.get();

    std::cout << "Container size:" << data.size() << std::endl;
    std::cout << "Press any key to exit...";
    std::cin.get();

    return 0;
}

输出:

Data loaded. Press any key to continue...
destructor
destructor
destructor
destructor
destructor
destructor
destructor
destructor
destructor
destructor
Data unloaded. Press any key to continue...

所以你看不到内存立即减少,因为你正在使用的分配器正在优化内存,所以 freeing 内存只意味着 "free to overwrite this piece of memory" 而不是将它还给OS,以便在以后的分配中使用它,而不必每次都涉及 OS

I used both linux top (on WSL) and Windows Task Manager to check memory usage at each stop. But valgrind does not report any memory leaks.

TLDR:相对于其他代码路径,分配内存是一项昂贵的操作。为了提高性能,几乎所有的堆管理器都会继续保留内存以供后续分配,而不是直接将其从原来的地方归还。

当您的程序代码分配内存时,它会通过一层内存堆来授予该分配。当您的程序调用 "new"(通过 make_shared)时,它会调用 C/C++ 运行时来分配内存。 C/C++ 运行时,如果它的堆中没有足够大的连续字节范围来授予该分配,它将通过 OS 特定的库调用向下转换到进程堆以询问以获得更多内存。进程堆,如果没有足够的空间立即分配,它会进行系统调用以分配更多的虚拟内存……并且可能还会分配更多的堆。别忘了内存可能需要分页,但我离题了。

这些堆访问中的每一个都需要锁定数据结构以管理请求的堆分配,并且可能需要对 OS 的系统调用。并且可能需要一些额外的努力来根据需要折叠或重新排列内存块。这就是内存堆分层的原因。如果每个 new 和 delete 调用都直接转到虚拟内存管理器,那么程序和系统性能会因为执行此操作的系统调用数量而非常慢。

同样,将内存释放回原来的位置也是类似的性能损失。当它想要压缩时,这些堆可能会释放回其父堆,但不要期望它通过 "delete".

的单次调用就可以做到这一点

Top 和任务管理器等工具只能从OS 观察进程分配的虚拟内存量。他们不知道由程序的运行时库管理的免费分配。而 Valgrind 将自身插入到您的代码中,并且可以将自身挂钩到更接近 C++ 内存管理器的位置。但即使是 Valgrind 也会时不时地报告误报。

默认使用tcmalloc,你可以通过“export LD_PRELOAD=/usr/lib64/libjemalloc.so.1”来使用jemalloc。 tcmalloc 分配器优化了使用的内存,它假设 prog 下次将使用内存以获得大量最小内存,如果您使用 vector(连续内存),tcmalloc 将在销毁它后释放它。或者您可以在列表后添加一些代码,例如:

std::vector<std::string> myVec;
for(int i = 0; i < 1000; ++i)
{
    myVec.push_back(string(256, 'a'));
}
myVec.clear();

然后你会发现:列表内存被释放了!