关于堆结构:前面在内存跟踪的时候,可能你已经发现,加入我们申请100个字节的堆内存,char* p = (char*)malloc(100);堆管理器实际上会分配给我们比100字节更大的一块内存给我们,这里多余的字节不只是堆结构的头,还有根据分配算法而多出来的那一部分。管理器返回给我们结构头之后的空闲块开始处的地址。当调用free(p)时,管理器假定p是动态分配返回的可用内存首地址,然后它会使p回跳堆结构头那么多字节,并将此时p指向的位置作为结构头的格式来读取,得到堆实际分配的内存大小,将其插入空闲内存块链表。因此,你也该很容易明白如果你分配了100个字节,你只想用60个字节,因此你调用free(p+60) 这样所造成的后果。
| | |
关于堆更智能化的调整,想象我们现在有下面这种现状的堆内存:
已分配A | 未分配30Byte | 已分配B | 未分配50Byte | 已分配C |
| | |
pA pB pC
如果我们现在想要分配60字节的内存,显然我们并不能直接得到,只有在通过一定手段的空闲内存块压缩才能够得到。然而这里的压缩并不像操作系统所说的直接移动那么简单,因为这周围还有三段已经分配的内存块,并且这些指针已经分配给用户了。如果你将内存块移成如下形式
已分配A | 未分配80字节 | 已分配B | 已分配C |
| | |
| | |
pA pB pC
那么实际上你已经改变了已分配内存块指针pB所指的内容。这样用户将不能再使用pB。因此操作系统采用了一种更合理的利用堆内存管理器的方法:使用二级指针---句柄 在用户分配的时候,比如调用和GlobalAlloc 返回给用户一个HGLOBAL类型的句柄,这个句柄并不能直接当成指针来使用,因为这时候得到的是二跳指针,这时候堆内存管理器是可以对内存块进行合理移动的,因为在移动内存块时,返回给用户的句柄是不变的,而是句柄在进程的对象表中所指的数据(堆内存实际的指针)变了。这里还需要解决的一个问题是,操作系统如何知道用户什么时候会使用那块堆内存以保证在用户使用的时候不移动该内存块?在Windows API中 我们使用GlobalLock传入我们得到的句柄值来通知操作系统我们即将使用这块内存,请不要再移动这个内存块了。OK,这时候操作系统就会帮我们锁定这块内存,并且返回一个可以直接转换使用的void*一级指针。当我们此次使用完这块内存后,如果又暂时不想释放这块内存,我们可以对内存块解锁:GlobalUnLock 这样操作系统又可以智能的管理你的这块内存了。注意,在这种分配方案中,内存块和指向内存块的指针都是属于堆管理器的,我们只能通过这几个API来与管理器交互。