内存泄漏 - 每个程序员的恐怖?

Cat*_*ode 21 c++ lua memory-leaks

我正在用C++编写一个游戏引擎,它也支持Lua.

我最大的恐怖:内存泄漏.

这并不像我的游戏已经被它们所侵扰,我很害怕它们像蘑菇一样从地面冒出来,当开发处于后期并且项目庞大而复杂时.

我很害怕他们,因为他们似乎很难找到我.尤其是在复杂系统中.如果我的引擎差不多完成,游戏就会运行,内存就会消失,我该怎么办?我将从哪里开始搜索?

  • 我对内存泄漏的恐惧是否合理?
  • 如何找出内存泄漏的位置?
  • 是不是有很好的工具可以帮助找到今天的内存泄漏源?

Jam*_*ong 37

如何找出内存泄漏的位置?

Valgrind的


Chr*_*ard 17

原始指针只是内存泄漏的一个潜在原因.

即使您使用智能指针(如shared_ptr),如果您有一个循环,也可能会出现泄漏 - 解决此问题的方法是在某处使用weak_ptr来打破循环.使用智能指针并不能解决内存泄漏的问题.

您还可以忘记基类中的虚拟析构函数,并以此方式获取泄漏.

即使没有删除新ed对象也没有问题,由于地址空间碎片,长时间运行的进程可能会增长(并且似乎会泄漏).

像valgrind这样的工具对于查找泄漏非常非常有用,但它们并不总能告诉您修复应该在哪里(例如,在循环或对象保持智能指针的情况下)


fra*_*ji1 12

需要一个定义良好的"对象生命周期"模型.无论何时你做"新",你都需要考虑

  1. 谁拥有这个堆对象?即谁负责维护此指针并允许其他"客户"引用它?

  2. 谁负责删除此对象?这通常是#1,但不一定.

  3. 该对象的客户端的生命周期是否长于此对象的客户端?如果这是真的,并且它们实际上正在存储这个堆指针,它将取消引用不再"存在"的内存.您可能需要添加一些通知机制或重新设计"对象生命周期"模型.

很多时候你修复了内存泄漏,但后来遇到问题#3.这就是为什么在编写太多代码之前最好考虑一下对象生命周期模型.


fre*_*low 11

你永远不使用原始指针来对抗内存泄漏.如果你有使用原始指针的代码,重构.

  • 原始指针只是内存泄漏的一个潜在原因.即使您使用智能指针(如shared_ptr),如果您有一个循环,也可能会出现泄漏.您还可以忘记基类中的虚拟析构函数,并以此方式获取泄漏.即使没有删除新ed对象也没有问题,由于地址空间碎片,长时间运行的进程可能会增长(并且似乎会泄漏). (6认同)

小智 7

我对内存泄漏的恐惧是否合理?

简短的回答是肯定的.很长的答案是肯定的,但有一些技术可以减少它们,并且通常会使它更容易.在我的意见中,最重要的是不要轻易使用new/delete并设计程序以减少或消除尽可能多的动态分配.不要分配内存,请尝试以下操作,只有当这些方法不适合您时才会分配自己的内存(大致按照您应该喜欢的顺序):

  • 在堆栈上分配
  • 使用标准容器(C++标准库和/或Boost),例如,使用std :: list而不是编写自己的链表
  • 与上述相关,如果可以的话,按值存储对象(即,如果复制构造不是太昂贵),或者至少引用(使用引用,您不需要担心null)来堆叠已分配的对象
  • 尽可能传递对堆栈对象的引用
  • 当你确实需要分配自己的内存时,尝试使用RAII在构造函数中分配并在析构函数中再次释放它.确保在堆栈上分配此对象.
  • 当您需要使用指针手动分配数据时,使用智能指针自动释放不再使用的对象
  • 清楚地定义哪些对象拥有什么内存.尝试将类层次结构限制为尽可能少的资源所有者,让他们处理为您分配和释放对象
  • 如果需要更一般的动态内存访问,请编写一个获取对象所有权的资源管理器.这里描述的句柄系统也很有用,因为它允许您"垃圾收集"不再需要的内存.句柄也可以用什么子系统拥有什么内存来标记,因此你可以转储系统内存使用状态,例如"子系统A拥有32%的已分配内存"
  • 为您分配的类重载operator new和delete,以便您可以维护有关who/what/where /何时分配的其他元数据

如何找出内存泄漏的位置?

Valgrind的的工具套件:MEMCHECK,Cachegrind,Callgrind,地块,Helgrind ...

您也可以尝试使用电围栏(-sfence for gcc)或您的编译器进行编译.您还可以尝试使用Intels工具套件,尤其是在编写多线程代码或性能敏感代码(例如,Parallel Studio)时,尽管它们很昂贵.

是不是有很好的工具可以帮助找到今天的内存泄漏源?

当然有.往上看.

现在,既然您正在编写游戏,我将分享一些与游戏开发相关的想法/体验:

  • 在每个级别的开头,尽可能多地预分配(理想情况下一切).然后在一个级别中,您不必担心内存泄漏.保持指向你分配的所有内容的指针,并在下一个级别的开始,释放所有,因为当下一级加载其自己的干净数据集时,应该没有办法悬挂指针存在.
  • 使用堆栈分配器(使用实际堆栈或在堆中创建自己的堆栈)来管理级别或框架内的分配.当您需要内存时,将其从堆栈顶部弹出.然后,当该级别或框架完成时,您可以简单地清除堆栈(如果您只存储POD类型,这是快速而简单的:只需重置堆栈指针.我用它在我自己的游戏引擎中分配消息或我的消息系统:在一个帧期间,消息(在我的引擎中是固定大小的POD类型)被分配到类似堆栈的内存池(内存被预先分配).所有事件都是从栈中获取的.在帧结束时,我将堆栈"交换"为第二个(以便事件处理程序也可以发送事件)并在事件上调用每个处理程序.最后,我只是重置指针.这使得消息分配非常快,不可能泄漏内存.
  • 对于您无法通过内存池或堆栈分配器管理的所有资源,请使用资源系统(带句柄):缓冲区,级别数据,图像,音频.例如,我的资源系统也支持后台的流媒体资源.句柄立即创建,但标记为"未准备好",直到资源完成流式传输.
  • 设计数据,而不是代码(即首先设计数据结构).尽可能隔离内存分配.
  • 尝试将类似的数据保存在一起.这不仅可以更轻松地管理其生命周期,而且还可以提高您的引擎性能,因为更好的缓存利用率(例如,字符的所有位置都存储在位置容器中,所有位置一起更新/处理等).
  • 最后,如果你能在纯功能性风格编程成为可能,而不是依赖于面向对象太多,可以简化许多问题:内存管理更容易,因为你的代码的部分可以变异的数据是有限的.分配在调用函数之前发生,在完成时释放(函数调用的数据流管道).其次,如果处理处理不可变数据的纯函数代码,多核编程将大大简化.双赢.例如,在我的引擎中,游戏对象的数据由纯功能代码处理,该代码以当前游戏状态作为输入,并作为输出返回下一帧游戏状态.这使得很容易跟踪代码的哪些部分可以分配或释放内存,并且通常跟踪对象的生命周期.由于这个原因,我也可以并行处理游戏对象.

希望有所帮助.


sho*_*osh 6

AFAIK Valgrid只是Linux.
对于Windows,您有BoundsCheckerPurify等工具.
如果您使用的是Visual Studio,那么C运行时库(CRT)也提供了一个非常简单且有用的工具,可用于查找开箱即用的内存泄漏.阅读_CrtDumpMemoryLeaks及其相关函数和宏.
基本上,它可以让你得到的内存泄漏的索引转储当进程退出,然后允许你设置一个断点在泄漏的内存被分配,看看到底当它发生的时间.这与大多数其他工具形成鲜明对比,这些工具只能为您提供事后分析,而无法重现导致内存泄漏的事件.
从第一天开始使用这些小宝石可以让您相对安心,保持良好状态.