有半个多月没有写博客了,应该是工作和生活比较忙吧。技术积累应该是不断的,今天就整理一下window的内存管理相关的知识吧。最近公司的其他同事分享了关于windows的内存管理的一些内在机制,主要集中在堆的分配和管理,这些知识点主要是侧重windows内核的。当时就觉得自己对windows的内存管理这块儿不是很熟,虽然大概的知识都了解,包括堆、栈等,但是由于工作中用到的不多,实际的工作中不会去自己写一些底层的内存分配和释放机制。我们工作是用C++进行开发,分配内存的途径就是new 和malloc两种方法,很少会用到win32的底层函数heapcreate,virtualCreate等。看一些开源的代码,比如v8和stl,他的里面就是自己进行heap的创建等。为什么c++已经提供了new函数,这些开源的代码还要自己管理复杂的内存分配呢?回过头来又将window核心编程里的内存管理这一部分看了一遍,记录学到的东西于此。
window中提供了三种进行内存操控的机制:
- 虚拟内存:最适合管理大型对象数组和大型结构数组
- 堆:最适合管理大量的小型对象
- 内存映射文件:最适合管理大型数据流(通常是文件)。
上面的每种机制都说明了其适用的场合。
堆
进程在启动的时候,windows会在进程的地址空间中创建一个默认的堆,这个默认堆是一些windows函数进行内存分配的地方。
我们一般使用的new所分配的内存都存在于C运行时内建的堆中,这一点我们可以通过char *c = new char这个语句进行单步调试,就可以看到最终的内存分配位于malloc.c中的代码,从crt的默认堆中进行heapalloc的操作来分配内存,这个堆是在c运行时初始化时进行创建的。而v8中的ast节点的对象都分配在自己创建的堆上。这两个例子都是堆内存管理的典型应用场景,进行大量的小型对象的内存管理。那么用堆有哪些具体的优点呢?
- 对组件进行保护
- 更有效的内存管理:堆中存放大小相同的数据对象,能够避免出现内存碎片。
- 局部访问:相关对象存放在同一个堆中,可以讲数据的访问逻辑限制在一个局部的内存块上,提高效率
- 避免线程同步的开销:对堆的访问时顺序进行的,也就是多线程访问堆时,会依次的进行堆的访问,不会同时访问,所以会影响效率。如果每个线程有自己的堆,就可以省去系统进行堆访问同步的开销。
- 快速释放:我们可以释放整个堆而不是一个对象一个对象的去释放,方便且快速。v8中的ast节点就有这样的用法。
知道了堆的这些好处,就可以用win32api来进行堆的使用了。
HANDLE WINAPI HeapCreate( __in DWORD flOptions, __in SIZE_T dwInitialSize, __in SIZE_T dwMaximumSize);
这个函数进行堆的创建, flOptions参数可选的参数如下:HEAP_CREATE_ENABLE_EXECUTE: 堆中的代码可以执行HEAP_NO_SERIALIZE: 堆不需要 顺序访问,如果只有单一的线程访问此堆,通过这个选项可以提高性能。dwInitialSize 参数: 初始大小。 dwMaximumSize 堆的最大尺寸,如果未指定,则紧着计算机内存增长。
LPVOID WINAPI HeapAlloc( __in HANDLE hHeap, __in DWORD dwFlags, __in SIZE_T dwBytes);
从堆上分配内存。
BOOL WINAPI HeapFree( __in HANDLE hHeap, __in DWORD dwFlags, __in LPVOID lpMem);
释放一块在对堆上分配的内存。
BOOL WINAPI HeapDestroy( __in HANDLE hHeap);
销毁一个堆。
其他有用的堆函数还有:HeapSetInformation, HeapReAlloc,HeapLock, HeapUnlock, HeapSize等等
我自己对堆的理解是操作系统为我们在虚拟内存的基础之上做了一层封装,方便我们的使用。当分配一个堆时,对应的后台有地址空间的保留和物理内存的提交,通过操作系统为我们所做的工作,可以比较简单的进行堆内存的管理,不需要对后台的虚拟内存很清楚也可以进行。
在C++中进行堆的使用时需要进行对象的new操作符的重载,将对象分配在指定的堆上。
虚拟内存
这部分的内容较多,不会仔细的进行描述,大概记录一下。
window下的每个进程都有自己独立的内存地址空间,对于32位的系统来说,内存空间大小为4GB, 低2GB的空间的应用程序的地址空间,高2GB的空间是操作系统的内核运行的空间。当系统创建一个进程时,地址空间的大部分区域都是free的,为了使用这部分,我们需要用VirtualAlloc来进行地址空间的预订和物理内存的调拨。当进行地址空间预订时,系统会确保所分配的区域起始地址是系统分配粒度的整数倍,而区域大小则是系统页面大小的整数倍。我们可以给每个分配的页面指定不同的页面保护属性,比如:PAGE_NOACCESS, PAGE_EXECUTE, PAGE_READWRITE等符号,来限制在这一个内存区域上所能进行的操作类型。
虚拟内存的函数主要有:VirtualAlloc, VirtualFree, VirtualProtect, VirtualQuery 等等。