vmmap 分析内存泄露问题
扫描二维码
随时随地手机看文章
vmmap是sysinternals工具集中的一个工具,主要用于分析一个进程的虚拟内存和物理内存的使用情况。更有效的是,可以通过对比两个不同时间的内存使用情况的Snapshot,来查找内存泄露问题。
vmmap介绍
当你用vmmap去查看一个正在运行的进程的时候。可以看到如下图,不同类型的内存使用采用不同的颜色标明。VMMap主要列举了以下几种类型的内存使用情况:
-
Free: 图中显示
137434599232K,是不是被吓到了。这个一般是指虚拟地址空间。每个进程都有自己的虚拟地址空间,比如32位的一般为4G,其中2G是内核地址空间, 2GB用户态地址空间;64位理论上为2^64个字节,实际上没那么大,按照MSDN的描述64位的Windows用户态可使用地址空间为128TB。
-
Heap: 这个主要就是指我们通过C/C 的malloc, new;以及HeapAlloc等申请的内存大小
-
Image: 比较好理解,一般指进程启动的运行文件,比如Exe或者加载的DLL文件。
-
Managed Heap: 这个一般指用C#编写代码使用的托管堆。比如一个程序可能是C#和C 均有实现,这个时候可以查看是不是托管堆占用的内存持续增高,那么就可以判断一般是C#部分托管堆使用有问题造成了泄露。
-
Mapped File: 主要是指内存映射文件,熟悉的同学应该知道,这也是常用的进程间通信的一种方式。
-
Private Data: 主要指通过VirtualAlloc申请的内存空间。这里也注意同Free主要是指已经使用的地址空间,而非已经Commit的内存。比如下图中,
-
Stack: 函数栈所使用的内存大小
-
Shareable: 主要是进程间可以共享的内存,但是后备存储器为RAM或者Paging File(一般是指虚拟内存page.sys)。
-
Page Table: 主要指内核中和该进程页表相关联的内存
对于其他的描述,本人本人主要介绍两种需要关注的:
-
Committed: 对于一个虚拟地址空间的使用,我们可以是申请地址空间,但不提交(
commit),如果不提交,则不会占用真实的存储器空间(比如RAM或者Paging File),只有commit后才会使用物理内存(RAM或者Paging File)。那么VMMap这里所指的内存就是后备存储器为RAM, Paging File, 或者Mapped file。
-
Working Set: 一般内存有RAM,还有虚拟内存(page.sys),而根据内存的调度原理,并不是所有的内存都常驻RAM。Working Set就是主要指在RAM中所使用的内存。
VMMap分析内存泄露
笔者曾经有一次用过VMMap分析过内存泄露,但是最终问题并不是通过VMMap分析出来的,主要是因为当运行到比较长的时间的时候VMMap偶尔会出现崩溃的情况。但是VMMap确实可以辅助分析出内存泄露问题,笔者也是将这个方法分享给大家。
下面是一段便于读者理解Vmmap分析方法的样例。首先每隔10秒钟,申请10M内存,总共申请10次;然后每隔10秒释放1次内存,只释放5次。这样操作,可以简单模拟,一个程序在运行中既有正常的内存申请释放的场景,也有申请后却没有释放的场景,这样交错在一起,让问题更加逼近现实。这样也便于使用这种方法,在未来碰到问题的时候进行实战。
#include
#include
#include
#include
void HeapMemoryLeakSample() {
const int iListSize = 10;
char* pHeapList[iListSize];
//Alloc 10 Heap STR_SIZE
const int STR_SIZE = 10 * 1000 * 1000;
for (int i = 0; i < iListSize; i ) {
pHeapList[i] = new char [STR_SIZE];
strcpy_s(pHeapList[i], STR_SIZE, "Alloc Memory");
std::cout << pHeapList[i] << std::endl;
std::this_thread::sleep_for (std::chrono::seconds(10));
}
//Free 5 Heap space
for (int i = 0; i < iListSize; i ) {
if (i % 2 == 0) {
delete pHeapList[i];
std::cout << "Free Memory" << std::endl;
std::this_thread::sleep_for (std::chrono::seconds(10));
}
}
}
int main() {
HeapMemoryLeakSample();
while (true) {
std::this_thread::sleep_for (std::chrono::seconds(10));
}
return 0;
}
接下来一起来查看是如何定位一个程序的内存泄露的。
第一步配置好程序的位置,工作目录,以及符号文件目录:
第二步当运行程序,首先看到整个VMMap界面。这个时候映入眼帘的好多好多数据,该看什么呢?首先对于一般的C 程序而言,堆的内存泄露使用是最常见,那么就先看下Heap部分的Committed大小是不是很大。比如本文的样例,发现已经有70M左右的大小。先锁定到溢出内存类型为Heap。
第三步个人认为查找内存泄露也需要一些技巧和常识的。比如程序刚启动不久的时候,申请的很多资源是全局的,或者伴随着整个进程的生命周期的,那么刚启动后的内存的增长一般可以忽略,不认为是内存泄露的原因。再大概程序运行一段时间后(根据自己程序实际情况而定),基本的伴随整个进程的生命周期的资源已经创建完毕。此时可以使用Timeline和Address部分的功能对照查看。
这个时候首先选择Heap(点击一下),那么Address部分将会显示Heap所占用的内存。然后当我们打开Timeline,选择特定的时间段区域,比如上图中选择区域为刚开始申请内存的部分,每隔10秒,增加申请10M内存。此时重要的是Address部分也会动态的展示这段时间的内存变化。
然后注意其中的内存使用比如000001B39E445000的内存被申请了,然后拉长时间线,发现很长时间还是存在在Address栏中,并且绿色,就说明一直没有被释放。
此时当你选中这个地址,再选择Heap Allocations,便可以看到其申请的大小为10000000, 双击打开后便可以查看到函数调用栈了。如下图所示便可以找到是在HeapMemoryLeakSample函数内调用了new,并且有行号提示(不过这里的行号提示不够精准,但是也不影响你去分析问题了)。
也可以不选择区间,而选个某个时间点,查看内存的状态。
第四步如果很幸运,第三步已经找到问题了。第四步本来想说一说Call Stack的追踪的,比如通过申请的内存的Count或者Bytes来查找到可疑的内存泄露点的函数调用栈。可是笔者多次实验后均发现,数据对不上。比如下图的Count百分比和Bytes百分比之和均对不上100%。所以笔者也不会对此做过多的赘述,调试软件同样也是软件,也可能存在bug或者一些限制。但是通过如上的方法和思想,也许能够协助你找到内存泄露点,至少可以起到辅助的作用。