|
现代计算语言的一个最重要特性是能够动态分配内存块。这种自由度使得建立算法时能够做得比在静态内存分配模式下更有弹性,但亦会引入一些问题:
- 能分配多少内存?
- 使用内存的方式影响算法的性能吗?
- 程序是否正确使用内存,或是否比实际需要分配太多的内存?
- 程序是否已经释放了不再使用的内存?
通常您在程序中插入一些额外的代码显示有关内存分配和释放的信息,但这样做非常乏味和费时,并且提供的信息也不是很有用。
克服动态内存问题
许多高级算法大量使用动态内存,但很少能够达到内存使用的最佳状态。事实上许多程序可以从优化内存使用中得到好处,或者通过修改内存分配请求的顺序减少内存碎片。在软件的内存需要接近物理内存的界限时显得尤其重要。当程序使用的内存超过可以得到的物理内存时,就要与磁盘交换-这是一个速度很慢的过程。不幸的是,内存量不仅计算程序自己分配的,还要加上其它开销和浪费的内存。因此,应该将无效内存控制在最小。
许多算法还含有一定的内存泄漏,在程序运行时可能会浪费掉大量宝贵的内存。这样对于长时间不间断运行的程序来说非常危险。一些错误能通过Insure++自动检测出来,另一些则需要通过持续监视内存使用情况检测出来。
Inuse是一个图示化工具,其设计目标就是通过实时和动态显示内存分配过程,帮助开发人员避免内存问题,同时更好地理解内存使用模式,进而优化其行为。Inuse能够帮助您:
- 寻找内存泄漏。
- 查看程序为响应特定的用户事件使用了多少内存。
- 检查整个内存的使用情况是否与用户预期的相符。
- 观察不同的内存分配策略能否改善性能。
- 通过函数、调用栈和内存块大小分析内存使用情况。
实时观察内存使用
Inuse能够将这一过程自动化,并以直观、动画的方式显示数据,这样您就可以方便地回答上述问题和理解程序对内存的实际使用过程。Inuse提供多种可视化实时报告:
历史数据报告:显示所有分配的内存。该图随时间周期性自动更新以显示应用的当前状态,可用于保持应用的执行轨迹。
- 频度报告:显示已分配的各种大小内存块的数量。它对于选择潜在的优化内存分配策略很有帮助。
- 内存结构报告:显示动态分配的内存结构框架,包括分配内存块之间的自由空间。可用于查看内存碎片和内存泄漏。用鼠标单击图中的任何内存块可以得到该块的地址、大小和状态。单击任何已分配或泄漏的内存块还能显示该块的标识号、地址、栈尺寸和栈轨迹。
- 时间结构报告:显示分配内存块的顺序。新分配的内存块自动加到显示的末端,当内存块被释放时,它们变成绿色。从该图中,能够看到按时间分配的内存块大小,从而确定程序的分配策略。
- 使用汇总报告:显示每个内存操作函数的调用次数,并显示当前的内存堆大小和使用中的内存大小。
- 查询报告:允许您根据标识号、大小和栈轨迹查看程序分配的内存块。通过按这种方式"组合"内存块,可以更好地理解程序如何使用内存。
用Inuse解决内存问题
Inuse能够非常有效地检测和避免许多不同的动态内存问题,包括内存崩溃、内存碎片、内存浪费和内存瓶颈。
问题一:内存崩溃(Memory
Blowout)
许多程序员还没有意识到内存崩溃所造成的危害性。内存崩溃是怎样发生的?通常程序按需分配了大量内存,用完后释放(但并没有真正还给操作系统),该程序还可以再使用这些内存。这是因为操作系统在程序释放掉内存后并没有马上将其释放,直到程序退出后才释放。其结果是,可能导致其它程序得不到所需的内存。这就是内存崩溃。
Inuse能帮助您防止内存崩溃。下图是Inuse生成的一个堆内存历史数据报告,通过分析报告很容易识别内存崩溃:注意到图中显示了程序“占用”了很大的堆空间(黑),但只分配使用了少量的内存(红),这表明实际分配内存对堆空间的比率很低。没有Inuse显然是很难判断的。

问题二:内存碎片(Memory
Fragmentation)
使用大量动态内存的一个风险是内存碎片。过度使用内存会产生内存碎片,这将降低内存分配速度。
例如,当一个程序交替分配释放小的和大的内存块时,程序员期望总的分配量是保持不变的。但事实上如果大小不同,内存分配程序不会重新使用前面已释放的内存,这将导致程序从操作系统请求新的内存页面。这将引起程序消耗更多的内存并最终造成动态内存溢出,但没有泄漏一点内存。
程序员可以使用Inuse的堆内存报告监视程序中的内存碎片。从图上可以立即看到动态分配的内存块(红)以及在它们之间的空闲内存(绿)的布局,在某个内存块上按下鼠标,可以看到该块的状态、内存地址、大小和栈轨迹。
在Inuse中观察内存碎片的另一种方法是在堆的历史数据图中查看是否有很大的系统开销(黄,操作系统在分配一块内存时会额外占用一些空间来保存相关信息,程序无法使用这些空间。要避免大量系统开销浪费的一种方法是调整内存分配策略,尽量分配大块的内存,而不是分配很多小块)。

问题三:内存浪费(Memory
Overuse - Hogging)
对于许多程序这是个常见的问题。程序分配了内存,但一直没有释放(可能已经不再使用)。这不是内存泄漏,因为程序中还有指向它的指针。结果是系统中有越来越多的内存垃圾。
可以从Inuse的堆内存历史数据图中看到内存浪费。如果泄漏的内存数量很少(可以忽略)且保持稳定,或Insure++没有报告任何内存泄漏,但内存使用呈阶梯状,这表明程序正在浪费内存并最终导致内存溢出。这时可以使用查询报告查看不同的堆栈分配的内存总量,如果一种大小的内存持续增长(见问题四),此时应该分析一下程序是否确实需要这样做,如果不是那就是算法问题。

问题四:内存瓶颈(Memory
Bottlenecks)
当操作系统在调度内存时比运行程序花费更多的时间,就会出现瓶颈。当一个程序使用大量动态内存或者大量调用程序的不同部分,经常会出现内存瓶颈。
有时这些问题会变得很严重,因为一个程序在内部测试时可能运行地很好,但却会在用户现场发生问题。这种改变是因为用户现场会引入不同的条件,如更少的内存或更多的数据。结果是程序使用了更大的内存,并迫使操作系统忙于调度。严重的话就会出现问题。
要避免这类问题,就需对程序进行分析,不同的部分需要多少内存。Inuse的查询功能允许开发人员计算特定的路径、程序或内存块类型分配了多少内存。这种分析对于理解算法问题非常关键,并更容易改善内存性能。

真正理解您的程序是如何使用内存的
当程序员编写程序时,关于程序中如何分配内存都有一定的想法,有时这种想法与程序的实际表现相差很远。Inuse通过在运行时的可视化内存分配使得程序员能够看到“真实的情况”。
程序员可以用单步调试的方法在malloc、free、new和delete等语句处设置断点,然后在Inuse中观察内存分配。这样可以确定算法的性能并验证其正确性。及早检查内存的使用,可以避免许多麻烦。

清除内存泄漏
并不是所有的内存泄漏都必须清除,但是一个频繁发生的内存泄漏一定要清除,例如在一个循环中。使用Inuse的TimeLayout显示能够方便地确定严重的内存泄漏问题。

与Insure一起使用:一个例子
Inuse与Insure++一起使用能够非常有效地检测不同的动态内存问题,同时您能够得到最完整的调试信息,包括多种易于理解的图示化数据。
考察下面有内存泄漏的程序。Insure++将报告一个错误在第23行,并给出一个完整的问题诊断报告。该内存泄漏也将出现在Inuse的图示化报告中,让您准确看到在运行时它是如何影响程序。联合使用上述工具,使您能够更全面地解决问题。
1:
#include <stdlib.h>
2: #include <stdio.h>
3:
4: #define SLOTS 16
5: main()
6: {
7: int i, which;
8: char **pp;
9:
10: while(1) {
11: pp=(char
**)malloc(SLOTS*sizeof(char *));
12: for(i=0;
i<SLOTS; i++) {
13:
pp[i] = malloc(64+i);
14: }
15: sleep(1);
16: for(i=0;
i<SLOTS; i++) {
17:
which = (rand() >> 4) % SLOTS;
18:
if(pp[which]) {
19:
free(pp[which]);
20:
pp[which] = 0;
21:
}
22: }
23: free(pp);
24: }
25: exit(0);
26: }
结语
动态内存是现代计算机语言的一个强大而方便的特性。但它也增加了程序员全面了解动态分配内存对程序的影响的难度。Inuse正是用于自动化这一过程,将内存使用状况用一种直观、格式化的方法表现出来,使得程序员能够更好地理解程序是如何使用内存的。
|