全面介绍Windows内存管理机制及C++内存分配实例(五):堆

来源:互联网 发布:淘宝三唑仑网上什么卖 编辑:程序博客网 时间:2024/05/01 16:41

原文链接:http://blog.csdn.net/yeming81/article/details/2052311


5.      内存管理机制--堆 (Heap)

·        使用场合

堆是进程创建时在进程空间建立的区域,由堆管理器来管理。一个进程可以有很多个堆。进程有一个默认堆为1M,可以动态的扩大。

当程序需要管理很多小对象时,适合用堆;当需要的空间大于1M时,最好用虚拟内存来管理。

堆的优点是,有堆管理器来替它管理,不需管理具体的事情如页面边界

        和分配粒度等问题,你可以从调用函数看的出来,比VirtualAlloc的参数少了

        不少。

         堆的缺点是分配和释放的速度比前几种机制要慢,所以最好不要超过

         1M;不像虚拟内存那样随时提交和释放,因为它是由堆管理器决定的。如果

         用堆分配1G的空间,需要1分种,而用虚拟内存,则感觉不到任何延迟。    

·        默认堆

进程默认堆是供所有线程使用的,每当线程需要从堆中分配释放内存区时,系

 统会同步堆,所以访问速度较慢。

它的默认大小是1M,同样的,你可以通过以下链接命令改变其大小:

#pragma comment(linker,"/HEAP:102400000,1024000")

第一个值是堆的保留空间,第二个值是堆开始时提交的物理内存大小。本文将堆改变为100M。

当你在程序中扩大了堆提交的物理内存时,进程运行时,物理内存将减少扩大的数量。但是,默认堆总是可以扩大的,不能限制它的最大值。

当你在程序中扩大了堆保留的空间时,进程运行时,可用进程空间将会减少扩大的数量。

每次你用New操作符分配内存时,进程空间会相应的减少,物理内存也会相应的减少。

 

一个重要的提示,本文经过测试,如果你需要的内存块大部分都超过512K,那么,建堆时给它的初始大小不应该很大,因为,如果你所需内存块大于512K的话,它不是从堆中分配的,也就是说不用堆中默认的空间,但其仍然属于堆管理。

 

默认堆的一个用处是系统函数需要利用它运行。比如,Windows2000的字符集是UNICODE的,如果调用ANSI版本的函数,系统需要利用堆来从ANSI到UNICODE的转换,调用UNICODE版本的函数。

·        自建堆

ü      使用场合

保护数据结构:

将不同的数据结构存在不同的堆中,可以防止不同的结构之间由于指针误操作而破坏了它们。

消除内存碎片:

将大小不同的结构保存在一个堆中,会导致碎片的产生,比如释放一个小结构时,大结构也不能利用它。

独享堆的快速:

如果用默认堆的话,线程之间是同步访问,速度慢;如果创建独享堆,则系统可以不需同步,比较快。

第二个快速体现在释放的快速,默认堆中,你只能释放某个内存块,而不能释放整个堆;而独享堆可以一次释放堆,也就是释放了所有的内存块。

ü      开始使用

         建立堆:

            使用以下API  

            HANDLE HeapCreate(DWORD 选项,SIZE_T 初始大小,SIZE_T 最大值)

“选项” 取值为0 ,不是以下任意一个

HEAP_NO_SERIALIZE,系统无需同步堆

HEAP_GENERATE_EXCEPTIONS,当创建失败或分配失败时产生异常。

“初始大小”是堆的大小,系统会规整到页面的整数倍,如0~4096的任何数都为4096;但是,进程空间至少要64K。

“最大值”是堆允许的最大值;为0则无限。

使用HEAP_NO_SERIALIZE需确定只有单线程访问这个堆,否则有可能破坏堆;或程序有同步代码来同步堆。

C++程序如下:

pHeap=(char*)GetProcessHeap();

printf("默认堆地址=%x/n",pHeap);

 

MEMORYSTATUS memStatus2;

            GlobalMemoryStatus(&memStatus2);

HANDLE hHeap=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*50,0);

            char* pHeap=(char*)hHeap;

            printf("新建堆1地址=%x/n",pHeap);  

            if(hHeap==NULL)

            {

                        cout<<"创建堆失败!"<<endl;

            }

            MEMORYSTATUS memStatus3;

            GlobalMemoryStatus(&memStatus3);

            cout<<"建立堆后:"<<endl;

cout<<"减少物理内存="<<memStatus2.dwAvailPhys-memStatus3.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus2.dwAvailPageFile-memStatus3.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="<<memStatus2.dwAvailVirtual-memStatus3.dwAvailVirtual<<endl<<endl;

 

HANDLE hHeap2=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*10,0);

            char* pHeap2=(char*)hHeap2;

            printf("新建堆2地址=%x/n",pHeap2);

 

            结果如下:

            

当建立堆1时,它分配了50M的物理内存给堆使用;当建立堆2时,堆2的地址是0x04bc 0000=0x019c 0000+50*1024*1024.

        分配内存:

            使用以下API

            PVOID HeapAlloc(HANDLE 堆句柄,DWORD 选项,SIZE_T 字节数)

            “选项”可以是,

            HEAP_ZERO_MEMORY,所有字节初始化为0

            HEAP_NO_SERIALIZE,堆这个内存区独享

HEAP_GENERATE_EXCEPTIONS,产生异常。如果创建堆有了它就不用再设了。异常可能为:STATUS_NO_MEMOR(无足够内存)和STATUS_ACCESS_VIOLATION(堆被破坏,分配失败)。

 

C++程序如下:

GlobalMemoryStatus(&memStatus3);

PVOID pV=HeapAlloc(hHeap,

HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*507);

            if(pV==NULL)

            {

                        cout<<"分配堆内存失败!"<<endl;

            }

            char * pC=(char*)pV;

            printf("第一次分配地址=%x/n",pC);

           

            MEMORYSTATUS memStatus4;

            GlobalMemoryStatus(&memStatus4);

            cout<<"第一次堆分配后:"<<endl;

cout<<"减少物理内存="<<memStatus3.dwAvailPhys-memStatus4.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus3.dwAvailPageFile-memStatus4.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="<<memStatus3.dwAvailVirtual-memStatus4.dwAvailVirtual<<endl<<endl;

 

PVOID pV2=HeapAlloc(hHeap,

HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*508);

            if(pV2==NULL)

            {

                        cout<<"分配堆内存失败!"<<endl;

            }

            char * pC2=(char*)pV2;

            printf("第二次分配地址=%x/n",pC2);

            MEMORYSTATUS memStatus5;

            GlobalMemoryStatus(&memStatus5);

            cout<<"第二次堆分配后:"<<endl;

cout<<"减少物理内存="<<memStatus4.dwAvailPhys-memStatus5.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="<<memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;

            for(int i=0;i<200*1024;i++)

                        pC2[i]=9;

 

            MEMORYSTATUS memStatus10;

            GlobalMemoryStatus(&memStatus10);

            cout<<"第二次堆使用一半后:"<<endl;

cout<<"减少物理内存="<<memStatus5.dwAvailPhys-memStatus10.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus5.dwAvailPageFile-memStatus10.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="

<<memStatus5.dwAvailVirtual-memStatus10.dwAvailVirtual<<endl<<endl;

结果如下:

 

可以看出,第一次分配507K的地址为0x04ad d650<0x04bc 0000,它是在堆中分配的;第二次分配508K的地址为0x055c 0020>0x04bc 0000,它是在堆外分配的;无论在多大的堆中,只要分配内存块大于507K时,都会在堆外分配,但是,它像在堆中一样,存在堆的链接表中,受堆管理。分配时,系统使用的是虚拟页文件;只有在真正使用时,才会分配物理内存。

至于为什么分配大于507K会在堆外分配而不直接使用堆中的内存,目前仍然不清楚。

         改变大小:

PVOID HeapReAlloc(HANDLE 堆句柄,DWORD 选项,PVOID 旧内存块地址,SIZE_T 新内存块大小)

“选项”除了以上三个外,还有HEAP_REALLOC_IN_PLACE_ONLY,指定不能移动原有内存块的地址。

C++程序如下:

GlobalMemoryStatus(&memStatus4);

            PVOID pV2New=HeapReAlloc(hHeap,0,pV2,1024*1024*2);

            if(pV2New!=NULL)

            {

            char * pC2New=(char*)pV2New;

            printf("改变分配地址=%x/n",pC2New);

            cout<<pC2New[0]<<endl;

            //cout<<pC2[0]<<endl;出现访问违规

            SIZE_T lenNew=HeapSize(hHeap,0,pV2New);

            cout<<"改变后大小="<<lenNew<<endl;

            }

            GlobalMemoryStatus(&memStatus5);

            cout<<"改变分配后:"<<endl;

cout<<"减少物理内存="<<memStatus4.dwAvailPhys-memStatus5.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus4.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="

<<memStatus4.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;

结果如下:

 

可以看出,新内存块紧接着原来内存块结束的地方开始创建,大小为2M;原来的内存块的内容被销毁和释放,所以新内存块只减少了增加的内存量。一个缺点就是,新内存块居然不保留原来内存的内容!另外,如果采用HEAP_REALLOC_IN_PLACE_ONLY的话,出现Not Enough Quote异常。也就是说,当前内存的状况是,必须移动才可以扩大此内存块。

         查询内存:

            可以查询堆中一个内存块的大小。

            SIZE_T HeapSize(HANDLE 堆句柄,DWORD 选项,LPVOID 内存块地址)

            “选项”可为0或HEAP_NO_SERIALIZE。

            参考以上例子。

         释放内存块:

            BOOL HeapFree(HANDLE 堆句柄,DWORD 选项,PVOID 内存块地址)

            “选项”可为0或HEAP_NO_SERIALIZE。

            C++程序如下:

            GlobalMemoryStatus(&memStatus5);

            HeapFree(hHeap,0,pV2New);

            MEMORYSTATUS memStatus6;

            GlobalMemoryStatus(&memStatus6);

            cout<<"第二次堆分配释放后:"<<endl;

cout<<"增加物理内存="<<memStatus6.dwAvailPhys-memStatus5.dwAvailPhys<<endl;

cout<<"增加可用页文件="<<memStatus6.dwAvailPageFile-memStatus5.dwAvailPageFile<<endl;

cout<<"增加可用进程空间="<<memStatus6.dwAvailVirtual-memStatus5.dwAvailVirtual<<endl<<endl;

结果如下:

 

            内存空间释放了原来的2M空间。

         释放堆:

            BOOL HeapDestroy(HANDLE 堆句柄)

            不能用它释放默认堆,系统忽略它的处理。

这一次,我们先在堆1中分配了70M的内存,由于它很大,所以,堆在堆外给它分配了内存,所以,堆1一共有50M+70M=120M。释放程序如下:

PVOID pV4=HeapAlloc(hHeap,HEAP_ZERO_MEMORY|HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS)

,1024*1024*70);

            if(pV4==NULL)

            {

                        cout<<"分配堆内存失败!"<<endl;

            }

            char * pC4=(char*)pV4;

            printf("第四次堆分配=%x/n",pC4);

            MEMORYSTATUS memStatus9;

            GlobalMemoryStatus(&memStatus9);

            cout<<"分配堆内存后:"<<endl;

cout<<"减少物理内存="<<memStatus7.dwAvailPhys-memStatus9.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus7.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="<<memStatus7.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;

 

            SIZE_T len=HeapSize(hHeap,0,pV4);

            cout<<"len="<<len<<endl;

            bool re=HeapDestroy(hHeap);

            if(re==false)

            {

                        cout<<"释放堆失败!"<<endl;

}

MEMORYSTATUS memStatus8;

            GlobalMemoryStatus(&memStatus8);

            cout<<"释放堆后:"<<endl;

cout<<"增加物理内存="<<memStatus8.dwAvailPhys-memStatus9.dwAvailPhys<<endl;

cout<<"增加可用页文件="<<memStatus8.dwAvailPageFile-memStatus9.dwAvailPageFile<<endl;

cout<<"增加可用进程空间="<<memStatus8.dwAvailVirtual-memStatus9.dwAvailVirtual<<endl<<endl;

 

结果如下:

 

如所猜想一样,释放了120M内存。

 

         获取所有堆:

            DWORD GetProcessHeaps(DWORD 数量,PHANDLE 句柄数组)

            “数量”是你想获取的堆数目;

            “句柄数组”是获得的堆句柄。

            默认堆也可以获取。

            HANDLE        handles[10];

            memset(handles,0,sizeof(handles));

            GetProcessHeaps(10,handles);

            for(int i=0;i<10;i++)

                        cout<<"堆"<<i+1<<"="<<handles[i]<<endl;

            结果如下:

            

可以看见,一共有8个堆,堆1是默认堆,堆7和堆8是本文建立的堆。另外5个不知来源。

         验证堆:

            BOOL HeapValidate(HANDLE 堆句柄,DWORD 选项,LPVOID 内存块地址)

            “选项” 可为0或HEAP_NO_SERIALIZE;

            “内存块地址”为NULL时,验证所有内存块。

            C++程序如下:

HANDLE        handles[10];

            memset(handles,0,sizeof(handles));

            GetProcessHeaps(10,handles);

            for(int i=0;i<10;i++)

            {

                        cout<<"堆"<<i+1<<"="<<handles[i]<<"   ";

                        if(HeapValidate(handles[i],0,NULL))

                                    cout<<"验证堆成功!"<<endl;

                        else

                                    cout<<endl;

 

            }

结果如下:

 

        合并内存块:

            UINT HeapCompact(HANDLE 堆句柄,DWORD 选项)

            “选项” 可为0或HEAP_NO_SERIALIZE;

            此函数可以合并空闲内存块。

       

        其他函数:

            HeapLock和HeapUnlock 通常是系统使用的;

            HeapWalk可以遍历堆内存,需要以上两个函数。

           

·        C++内存函数

Malloc和Free

这是C语言使用的函数,只能从默认堆中分配内存,并且只是分配内存,不能调用构造函数,且只是按字节分配,不能按类型分配。

New 和Delete

这是C++语言使用的函数,默认情况下从默认堆中分配内存,但是也可以通过重载New函数,从自建堆中按类型分配;同时可以执行构造函数和析构函数。它底层是通过HeapAlloc和HeapFree实现的。 依赖于编译器的实现。

GlobalAlloc 和GlobalFree

这是比HeapAlloc和HeapFree更慢的函数,但是也没有比它们更好的优点,只能在默认堆中分配;16位操作系统下利用它们分配内存。

LocalAlloc和LocalFree

在WindowsNT 内核里,和GlobalAlloc、GlobalFree是一样的。

 

·        一个例子

默认情况下,New关键字是利用HeapAlloc在默认堆上建立对象。本文重载了类的New方法,使得类在自己的堆中存放,这样可以与外面的对象隔离,以免重要的数据结构被意外破坏。由于类中的成员变量是在堆中存放,因此不局限于线程堆栈的1M空间。

C++程序如下:

class AllocateInOtherHeap

{

public:

            AllocateInOtherHeap(void);

            ~AllocateInOtherHeap(void);

            voidoperator new(size_t size);

            static HANDLE heap;

         public:

            //类对象唯一所需的空间

            int iArray[1024*1024*10];

            AllocateInOtherHeap::AllocateInOtherHeap(void)

{

            cout<<"AllocateInOtherHeap()"<<endl;

            //如果New函数没有分配够空间,那么此处会出现访问违规

            memset(iArray,0,sizeof(AllocateInOtherHeap));

            iArray[1024]=8;

}

void* AllocateInOtherHeap::operator new(size_t size)

{         

            if(heap==NULL)

heap=HeapCreate(HEAP_NO_SERIALIZE|HEAP_GENERATE_EXCEPTIONS,1024*1024*10,0);

            //分配足够这个类对象的空间

            void* p=HeapAlloc(heap,0,sizeof(AllocateInOtherHeap));

            cout<<"堆的大小="<<HeapSize(heap,0,p)<<endl;

            printf("AllocateInOtherHeap堆地址=%x/n",heap);

            printf("AllocateInOtherHeap返回地址=%x/n",p);

            return p;

}

AllocateInOtherHeap::~AllocateInOtherHeap(void)

{

            cout<<"~AllocateInOtherHeap"<<endl;

}

void AllocateInOtherHeap::operator delete(void* p)

{         

            HeapFree(heap,0,p);

            HeapDestroy(heap);

            cout<<"delete()"<<endl;          

}

};

结果如下:

可见,new函数先分配够空间,然后才能初始化对象变量;而delete函数得先做析构,才能释放空间。对象保存在堆外,因为大于512K;对象大小刚好是iArray变量的大小。

注意,如果没有分配足够的空间,虽然你可以得到对象指针,但是你访问数据时可能会出现访问违规,如果没出现,那更惨,意味着你读写了别人的数据。


0 0