delphi 内存管理[2-3]


本文整理自网络,侵删。

 
内存管理[2]
系统给程序的地址数是 4G, 为什么不是 3G 或 5G? 因为 32 位的指针的最大值就是 $FFFFFFFF, 它不能表示更多了, 究其根源这要回到 CPU 的寻址能力、地址总线等等.

在 Win64 下, 系统给程序的地址数达到了 16EB(0 - $FFFFFFFFFFFFFFFF), 也就是 18446744073709551616 个. 不过 Win64 还没有普及, 我们还得回到实际的 Win32.

就这 4G 的地址, 系统还要留下一半($80000000 - $FFFFFFFF, 这 2G 是各进程共享的)用作宏观管理; 只给程序 2G(0 - $7FFFFFFF).
就这 2G 的地址, 也不是全给用户的, 低端的 0 - $FFFF 是用于空指针分配, 禁止访问; 高端的 $7FFF0000 - $7FFFFFFF 也留出来作为进程的临界区, 也禁止访问. 其实进程的私有空间地址只有 $10000 - $7FEFFFF.

内存管理[3]
VirtualAlloc 分配的内存是以 4K 为最小单位、连续的内存地址(但映射到真实的内存时它不一定是连续的), 前面说了, 它不适合分配小内存(譬如只有几个字节的变量); 局部的变量在 "栈" 中有程序自动管理, 那么那些全局的小变量怎么办呢? 这就要用到 "堆".

这样看来, VirtualAlloc 分配的内存既不是 "栈" 也不是 "堆"; VirtualAlloc 分配的内存地址是连续的, "堆" 中内容一般是不连续的, 所以管理 "堆" 比较麻烦, 它是通过双线链表的结构方式管理的; 程序可以拥有若干个 "堆", 每一个 "堆" 都会有一个句柄, 访问 "堆" 中的内容时先要找到这个 "堆", 然后再遍历链表, 这可能就是 "堆" 比 "栈" 慢的根本原因.

在 "堆" 中分配内存(HeapAlloc)前先要建立 "堆"(HeapCreate), 就像程序有默认的 "栈" 一样, 每一个程序都有一个默认建立的 "堆"(可以用 GetProcessHeap 获取这个 "默认堆" 的句柄), 我们在 Delphi 中用到 "堆" 时, 使用的就是这个 "默认堆". 如果让程序更灵活地拥有多个 "堆", 必须要用到 API 函数.

建立 "堆" 时会同时提交真实内存的, 这在申请大内存时会很慢, 所以默认堆也只有 1M, 但 "默认堆" 并没有限制大小, 它会根据需要动态增长.

有了 "默认堆" 还有必要申请其他的 "堆" 吗? 这只有在多线程中才能体现出来, 和 "栈" 不一样, 程序会给每个线程分配一个 "栈区"; 而 "默认堆" 是进程中的所有线程公用的, 当一个线程使用 "默认堆" 时, 另一个需要使用 "堆" 的线程就要先挂起等待, 也就是它们不能同时使用; 只有通过 API 函数重新建立的私有堆才是互不干涉、最有效率的.

先了解一下 "堆" 相关的函数.
//建立堆; 注意建立时指定的尺寸也是按页大小(PageSize)对齐的, 譬如指定 15k, 实际会分配 16K.
HeapCreate(
  flOptions: DWORD;     {堆属性选项, 见下表}
  dwInitialSize: DWORD; {初始尺寸, 单位是字节; 该大小会被直接提交到实际的内存}
  dwMaximumSize: DWORD  {最大尺寸, 如果不限定最大值就设为 0}
): THandle;             {返回堆句柄; 失败返回 0, 但如果参数 flOptions 允许了异常, 失败会返回异常标识}

//flOptions 参数可选值:
HEAP_NO_SERIALIZE        = 1; {非互斥, 此标记可允许多个线程同时访问此堆}
HEAP_GENERATE_EXCEPTIONS = 4; {当建立堆出错时, 此标记可激发一个异常并返回异常标识}
HEAP_ZERO_MEMORY         = 8; {把分配的内存初始化为 0}

//flOptions 参数指定有 HEAP_GENERATE_EXCEPTIONS 时, 可能返回的异常:
STATUS_ACCESS_VIOLATION = DWORD($C0000005); {参数错误}
STATUS_NO_MEMORY        = DWORD($C0000017); {内存不足}


//销毁堆
HeapDestroy(
hHeap: THandle {堆句柄}
): BOOL;       {}


//从堆中申请内存
HeapAlloc(
  hHeap: THandle; {堆句柄}
  dwFlags: DWORD; {内存属性选项, 见下表}
  dwBytes: DWORD  {申请内存的大小, 单位是字节}
): Pointer;       {返回内存指针; 失败返回 0 或异常, 情况和建立堆是一样}

//dwFlags 参数可选值:
HEAP_NO_SERIALIZE        = 1; {非互斥, 此标记可允许多个线程同时访问此堆}
HEAP_GENERATE_EXCEPTIONS = 4; {当建立堆出错时, 此标记可激发一个异常并返回异常标识}
HEAP_ZERO_MEMORY         = 8; {把分配的内存初始化为 0}

{能看出这和堆的属性选项是一样的; 如果 dwFlags 参数设为 0, 将使用堆的属性; 如果重新指定将覆盖堆的属性}
{另外: 如果堆是默认堆, 也就是堆句柄来自 GetProcessHeap, dwFlags 参数会被忽略}


//改变堆内存的大小, 也就是重新分配
HeapReAlloc(
  hHeap: THandle; {句柄}
  dwFlags: DWORD; {内存属性选项; 该参数比 HeapAlloc 多出一个选项, 见下表}
  lpMem: Pointer; {原内存指针}
  dwBytes: DWORD  {新的尺寸}
): Pointer;       {同 HeapAlloc}

//dwFlags 参数可选值:
HEAP_NO_SERIALIZE          = 1;  {非互斥, 此标记可允许多个线程同时访问此堆}
HEAP_GENERATE_EXCEPTIONS   = 4;  {当建立堆出错时, 此标记可激发一个异常并返回异常标识}
HEAP_ZERO_MEMORY           = 8;  {把分配的内存初始化为 0}
HEAP_REALLOC_IN_PLACE_ONLY = 16; {此标记不允许改变原来的内存位置}


//获取堆中某块内存的大小
HeapSize(
  hHeap: THandle; {堆句柄}
  dwFlags: DWORD; {内存属性; 可选值是 0 或 HEAP_NO_SERIALIZE, 后者可确保同步访问}
  lpMem: Pointer  {内存指针}
): DWORD;         {成功返回字节为单位的大小; 失败返回 $FFFFFFFF}


//释放堆中指定的内存块
HeapFree(
  hHeap: THandle; {堆句柄}
  dwFlags: DWORD; {内存属性; 可选值是 0 或 HEAP_NO_SERIALIZE}
  lpMem: Pointer  {内存指针}
): BOOL;          {}


//验证堆
HeapValidate(
  hHeap: THandle; {}
  dwFlags: DWORD; {}
  lpMem: Pointer  {}
): BOOL;          {}


//整理堆
HeapCompact(
  hHeap: THandle; {}
  dwFlags: DWORD  {}
): UINT;          {}


//锁定堆
HeapLock(
  hHeap: THandle {}
): BOOL;         {}


//锁定后的解锁
HeapUnlock(
  hHeap: THandle {}
): BOOL;         {}


//列举堆中的内存块
HeapWalk(
  hHeap: THandle;                {}
  var lpEntry: TProcessHeapEntry {}
): BOOL;                         {}

举例放下篇吧.
上面这个结果, 我们可以通过 GetSystemInfo 函数得到证实, 通过 GetSystemInfo 函数能获取一个 TSystemInfo 结构, 结构中的 lpMinimumApplicationAddress 和 lpMaximumApplicationAddress 分别表示程序(或动态链接库)可以访问的最低与最高的内存地址.
var
  si: TSystemInfo;
begin
  GetSystemInfo(si);
  ShowMessageFmt('%p-%p', [si.lpMinimumApplicationAddress, si.lpMaximumApplicationAddress]);
  {结果是: 00010000-7FFEFFFF}
end;

通过 GetSystemInfo 还能得到一个内存相关的重要参数: 页大小(PageSize)
var
  si: TSystemInfo;
begin
  GetSystemInfo(si);
  ShowMessage(IntToStr(si.dwPageSize)); {4096; 4096 字节也就是 4K}
end;

PageSize 是系统管理内存的基本单位, 之所以需要用 GetSystemInfo 获取不同系统的 PageSize 也会有区别.

我们需要知道的是, 用 VirtualAlloc 函数分配的内存就是以 PageSize(4K) 为最小单位的; 假如我们用 VirtualAlloc 给一个整数(4个字节)分配内存, 将会浪费 4092 个字节, 也就是说 VirtualAlloc 不适合分配小内存, 因而也有了多种分配内存的函数.

暂时放下这个话题, 先了解一下 "栈".

说到 "栈", 就想到 "堆", 还有 "堆栈" 指的也是 "栈"; "栈" 与 "堆" 都是程序可操作的内存区域($10000 - $FFEFFFF)中的某一小段.

系统函数中有 HeapReAlloc、GlobalAlloc 等分配 "堆" 的函数, 却没有分配 "栈" 的函数, 这是因为 "栈" 是程序自动管理的; 每个程序都从自己的可用地址范围内留出一块作为 "栈", 程序根据需要可以自动调节它的大小, 但咱们可以设置它的最大值与最小值. 在Delphi 中可以从这里设置:
Project -> Options -> Linker -> [Min stack size 和 Max stack size]

"栈" 用来暂存局部变量和函数参数, 由程序在需要时申请, 用完就释放.

因为 "栈" 的空间一般不是很大, 所以咱们一般不要把局部变量弄得太大(特别是在使用数组的时候);
因为访问 "栈" 比访问 "堆" 来的简洁, 速度快, 所以要尽量多用局部变量、少用全局变量.

相关阅读 >>

Delphi中用拼音首字符序列来实现检索功能

Delphi webbroker standalone 模式下当作一个普通的 webserver 响应静态网页或其它文件的请求

Delphi webbrowser获取页面全部链接

Delphi fdmemtable内存表操作

Delphi 锁定鼠标移动范围

Delphi d10.x 安卓app开发中按返回键后程序不退出程序的方法

Delphi fastreport 直接列印

Delphi调用游戏call代码

Delphi opendialog1 savedialog1 默认路径

Delphi中format与formatdatetime函数详解

更多相关阅读请进入《Delphi》频道 >>



打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,您说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

分享从这里开始,精彩与您同在

评论

管理员已关闭评论功能...