DELPHI内存详解内存组成、栈、堆、内存管理器、三方内存管理器、内存越界、内存泄露作者樊升:sheng.
fan@dinglicom.
com陆元会:Kingron@dinglicom.
com文档历史日期版本作者描述说明2009-12-111.
0樊升完成初始版2010-6-31.
1樊升增加了一些内容2010-06-041.
2陆元会审核修改了一些内容目录1应用程序的内存组成52栈和堆52.
1栈和堆的内存分配比较62.
2栈和堆的申请方式方面62.
3栈和堆的系统响应方面62.
4栈和堆的大小限制方面63DELPHI的内存实现64变量初始化75用const来提高应用程序在多核多线程下的性能76函数返回值77内存申请和释放97.
1内存分配常见函数97.
2GetMem和FreeMem、GetMemory和FreeMemory97.
3New和Dispose97.
4StrAlloc和StrDispose97.
5AllocMem97.
6SysGetMem和SysFreeMem108String和Pchar108.
1String的结构108.
2直接给常量赋值,系统会自动分配内存,自动释放109数组和动态数组109.
1数组到底是在栈中还是在堆中109.
1.
1固定数组在函数体内分配是在栈中的109.
1.
2固定数组在类中分配是在堆中的119.
1.
3固定数组全局变量是在堆中的119.
1.
4动态数组不管在函数体中、类中、全局变量都是在堆中的119.
2DELPHI的数组和动态数组都是自动管理的,不需要手动释放1110结构体1210.
1结构体是在栈中还是堆中1210.
1.
1在函数体中定义的结构体是在栈中的1210.
1.
2结构体在类中、全局变量都是在堆中1210.
1.
3结构体指针,指针地址在栈中,结构体内容在堆中1210.
2结构体的申请和释放1311内存操作:复制、清空、填充1311.
1内存复制1311.
2内存清空与填充1612内存映射1713FastMM内存管理器1713.
1用FastMM加快DELPHIIDE的速度1713.
2用FastMM加快我们自己应用程序的速度1713.
3用FastMM检测内存泄漏1714常见内存错误、代码审查1814.
1PCHAR指针末尾没有赋#01814.
2没初始化内存出现错误,用FillChar、ZeroMemory1814.
3函数返回值没有初始化1814.
4PwideChar(string)内存格式不一致,强制转换,导致内存错乱1814.
5申请的函数和释放的函数不一致1815附录:内存函数1816感谢人员名单2117参考资料21应用程序的内存组成对于Windows32来说,系统会给每个进程4GB的地址空间,低端2GB($00000000-$7FFFFFFF)给用户支配;高端2GB($80000000-$FFFFFFFF)留给系统使用,这个4G的地址空间叫"虚拟地址表",虚拟地址表不是真实的内存.
这个"虚拟地址表"上有4GB的空间,每个程序都有这样的一个表,但他们并不会冲突,这样就阻断了一个进程对另一个进程的访问,系统在需要的时候会把他们映射成具体的真实内存地址.
可以使用GlobalMemoryStatus来获取内存信息,获取后的信息放在TMemoryStatus结构体中.
_MEMORYSTATUS=recorddwLength:DWORD;结构长度}dwMemoryLoad:DWORD;{表示可用内存比例的一个整数,100表示内存都可用}dwTotalPhys:DWORD;{物理内存总数}dwAvailPhys:DWORD;{可用物理内存总数}dwTotalPageFile:DWORD;{虚拟内存总数}dwAvailPageFile:DWORD;{可用虚拟内存总数}dwTotalVirtual:DWORD;{虚地址表中的地址总数}dwAvailVirtual:DWORD;{虚地址表中可用的地址总数}end;TMemoryStatus=_MEMORYSTATUS;在用户的2GB地址空间中,低端的0-$FFFF是用于空指针分配,高端的$7FFF0000-$7FFFFFFF用于进程的临界区,这两个地址都是禁止访问的.
进程真正的私有空间地址是:$10000-$FFEFFFF.
我们可以通过GetSystemInfo函数得到证实,函数返回TSystemInfo结构,结构中的lpMinimumApplicationAddress和lpMaximumApplicationAddress分别标识程序可以访问的最低与最高地址,通过GetSystemInfo还能得到一个内存相关的重要参数:页大小(PageSize),通常大小是4K,我们需要知道的是,用VirtualAlloc函数分配的内存就是PageSize(4K)为最小单位,加入我们用VirtualAlloc给一个整数分配内存,就会浪费4092个字节.
栈和堆栈是编译器自动分配释放,存放函数的参数值,局部变量的值,存取偏移是4字节,不会根据需要动态增长,超出范围会报异常;堆是由程序员分配释放,编译器都会默认建一个"堆",建立"堆"时会同时提交真实内存,这在申请大内存时会很慢,所以默认"堆"也只有1M,但是"堆"没有限制大小,会根据需要动态增长,注意它与数据结构中的堆是两回事,它的分配方式类似于链表,访问"堆"的内容的时候需要先找到这个"堆",然后再遍历链表,因此"堆"访问会比"栈"慢.
栈和堆的内存分配比较栈:有编译器自动分配释放,存放函数的参数值,局部变量的值等,存取偏移是4字节,申请的变量不会初始化,是一个垃圾值.
堆:有程序员分配释放,若不释放,程序结束时由系统回收,分配的内存不会初始化,是一个垃圾值.
栈和堆的申请方式方面栈:由系统自动分配,如在函数申明一个局部变量i:Integer;编译器会自动在栈中分配内存.
堆:需要程序员自己申请,并指明大小.
如在函数中申请一个结构体New(A),A指向的内容是在堆中的,但是A的地址却是在栈中的.
栈和堆的系统响应方面栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出.
堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的FreeMem语句才能正确的释放本内存空间.
另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中.
在堆中分配内存的时候会用HeapLock和HeapUnlock加锁,因此在多线程中分配内存是线性的,效率低下.
栈和堆的大小限制方面栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域.
这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow.
因此,能从栈获得的空间较小.
堆:是向高地址扩展的数据结构,是不连续的内存区域.
这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址.
堆的大小受限于计算机系统中有效的虚拟内存.
由此可见,堆获得的空间比较灵活,也比较大.
Delphi改变程序栈大小设置是:Project->Options->Linker->Minstacksize/Maxstacksize.
获取栈的首地址代码procedureGetStackAddress(varAStackTop,AStackBottom:Cardinal);beginasmmoveax,FS:[4];subeax,3;movAStackTop,eax;movAStackBottom,ebp;end;end;DELPHI的内存实现DELPHI是在Windows内存管理的基础上,通过GetMem.
inc来实现自己的内存管理器,它封装了操作系统API,使得用户可以直接分配内存,而无须考虑内存具体在哪个虚地址空间.
这样的好处就是对于开发者来说:内存分配是透明,分配到的任何一块(既定长度的)内存都是连续的,可以通过字节序遍历一个数据结构,或者在长度边界相同的情况下进行强制转换.
DELPHI的内存管理为了考虑多线程环境,使用了临界区变量heapLock,因此在多线程环境中,DELPHI缺省的内存管理器性能会相对较差一些.
变量初始化DELPHI默认初始的变量是全局变量和类变量,初始化的规则,是内存块,内存内容全部初始化为#0,指针初始化为nil,别的(包括函数体内变量)都需要手动初始化.
用const来提高应用程序在多核多线程下的性能我们经常在DELPHI中用const来定义常量,用const来保护函数参数,其实在用const保护函数参数还有另一个更为重要的作用,提高应用程序的执行效率,尤其是在多线程多核下效果更明显.
原因是:普通的函数参数如Add(AValue:string),编译器在传入参数的时候先把变量复制一份,然后当成AValue传入Add,函数结束的时候进行销毁,你在参数上加了const,编译器在传入参数的时候不会进行复制,而是直接传地址,并在编译期间检查不能修改AValue值,我们知道DELPHI的内存管理在申请内存的时候是会加锁的,因此如果调用函数频繁,而且没有加const,这样会造成线程排队等候,性能会不如单线程,const只是对string、结构体等非基本类型有提高效率的作用,对Integer等基本类型(栈变量)不起作用.
函数返回值Delphi函数在返回值的处理上,对于32简单数据类型(不包括浮点数类型),是使用eax返回的,如Integer等;对于64位简单数据类型,是使用edx:eax返回的,如int64类型,而对于结构体返回值来说,长度在32位及以下是通过eax返回的,32位以上返回有点区别,不是"32位以上用寄存器返回指针",而是在调用前传递了一个默认指针参数.
如下面例子的functionIncX:TTest;函数,在TTest类型长度大于32位时,实际上等同于于procedureIncX(varv:TTest)或者procedureIncX(v:^TTest).
下面例子非常有意思:下面的代码中,定义了一个记录类型,定义了2个同名的方法(同不同名无所谓),v1和v2的x字段都初始化为10,分别调用2个方法后.
提问:1、v1.
x与v2.
x的值分别为多少(要求不要在机上测试)2、假如从记录定义中去掉y字段,v1.
x与v2.
x的值又分别为多少(要求不要在机上测试)typeTTest=recordx:Integer;y:Integer;end;procedureIncX(varv:TTest);overload;beginInc(v.
x);end;functionIncX:TTest;overload;beginInc(Result.
x);end;varv1,v2:TTest;beginv1.
x:=10;v2.
x:=10;v1:=IncX;IncX(v2);end;试题的结果是:两个都是11DELPHI的函数返回值一定要初始化,如果没有初始化,则返回是一个随机值(没有初始化返回值,DELPHI编译器会告警).
内存申请和释放内存分配常见函数GetMem和FreeMem、GetMemory和FreeMemory、New和Dispose、StrAlloc和StrDispose、AllocMem、SysGetMem和SysFreeMem.
GetMem和FreeMem、GetMemory和FreeMemory由于DELPHI的内存管理都知道分配内存的大小,因此在释放内存的时候,只要给指针地址不用给出长度就可以了.
另外提倡用GetMemory和FreeMemory来代替GetMem和FreeMem,因为FreeMemory会判断指针是否为空.
New和DisposeNew和Dispose是用来管理变体类型内存分配,如变体结构体:TRecord=recordText:string;Value:Integer;end;PRecord=^TRecord;如果用GetMem和FreeMem、GetMemory和FreeMemory来释放,会造成Text的内存没有释放,造成内存泄漏.
如果用Dispose来释放指针,要加上定义信息,否则造成内存泄漏,正确写法Dispose(PRecord(Point)).
StrAlloc和StrDispose这个函数也是一对,他们分配PChar加一个Cardinal长度,因此一定要用StrDispose释放,否则容易造成4字节的内存泄漏.
StrAlloc分配的指针可以使用StrBufSize来获得大小.
AllocMemAllocMem是调用GetMem来分配内存,但是它会把内存全部初始化为#0,因此推荐AllocMem代替GetMem和GetMemory.
以下写法都是错误的,都会造成字符串没有结尾符.
functionSystemPath:string;beginSetLength(Result,GetSystemDirectory(nil,0));GetSystemDirectory(PChar(Result),Length(Result));Result:=PChar(Result);end;functionSystemPath:string;beginSetLength(Result,GetSystemDirectory(nil,0));GetSystemDirectory(PChar(Result),Length(Result));Result:=PChar(Result);end;varSystemPath:PChar;Len:Cardinal;beginLen:=GetSystemDirectory(nil,0);SystemPath:=GetMemory(Len);GetSystemDirectory(SystemPath,Len)FreeMem(SystemPath);end;SysGetMem和SysFreeMemSysGetMem和SysFreeMem是上面函数的底层实现,申请的内存不通过DELPHI内存管理器管理,一般不直接使用它们.
String和PcharString的结构String结构为Cardinal(引用计数)Cardinal(长度)内容直接给常量赋值,系统会自动分配内存,自动释放varp:PChar;beginp:='常量赋值会自动分配内存';ShowMessage(p);end;数组和动态数组数组到底是在栈中还是在堆中数组单一说在栈中还是在堆中都是错误,分为几种情况:固定数组在函数体内分配是在栈中的我们做一个试验,一般DELPHI程序线程的栈大小是1M,如果我们函数体中申请4M大小的数组,报栈溢出,则表示数据的大小是在栈中的.
constCArrayCount=1024*1024*4;procedureTForm1.
btnMainThreadClick(Sender:TObject);varMainThreadArray:array[1.
.
CArrayCount]ofChar;i:Integer;beginfori:=Low(MainThreadArray)toHigh(MainThreadArray)doMainThreadArray[i]:=#0;end;我把以上代码在主线程中测试时,确实报了栈溢出,如果这时你把DELPHI程序的栈调大为6M则正确,表示在函数体中申请的数组是在栈中的.
固定数组在类中分配是在堆中的我们在类中加一下定义语句FFixArray:array[1.
.
CArrayCount]ofChar;程序正常,表示在类中分配固定数组是在堆中的.
固定数组全局变量是在堆中的我们在程序定义全部数组GFixArray:array[1.
.
CArrayCount]ofChar;程序也正常,表示全局固定长度是在堆中的.
动态数组不管在函数体中、类中、全局变量都是在堆中的如果你会汇编,看一下汇编就明白了.
DELPHI这么实现是合理的,在函数里中分配的固定长度数组放在栈中可以加快运行效率,而且在多线程的情况下,不用像堆分配有加锁.
只是大家在写程序的过程中注意在函数里定义太长的数组需要注意,否则栈溢出,程序就崩溃了.
DELPHI的数组和动态数组都是自动管理的,不需要手动释放动态数组用SetLength来分配内容,手动释放可以直接把动态数组长度设为0.
结构体结构体是在栈中还是堆中在函数体中定义的结构体是在栈中的以下代码会直接报堆栈溢出constCMaxStackSize=4*1024*1024;typeTStackRecord=recordValue:Integer;ValueInteger:array[0.
.
CMaxStackSize]ofInteger;end;PStackRecord=^TStackRecord;varForm1:TForm1;implementation{$R*.
dfm}procedureTForm1.
FormCreate(Sender:TObject);varStackRecord:TStackRecord;beginShowMessage('此处会报栈溢出'+IntToStr(Integer(@StackRecord)));end;结构体在类中、全局变量都是在堆中结构体指针,指针地址在栈中,结构体内容在堆中以下代码不会报堆栈溢出constCMaxStackSize=4*1024*1024;typeTStackRecord=recordValue:Integer;ValueInteger:array[0.
.
CMaxStackSize]ofInteger;end;PStackRecord=^TStackRecord;varForm1:TForm1;implementation{$R*.
dfm}procedureTForm1.
FormCreate(Sender:TObject);varStackRecord:PStackRecord;beginNew(StackRecord);ShowMessage('此处不会报栈溢出'+IntToStr(Integer(@StackRecord)));end;结构体的申请和释放结构体申请和释放建议使用New和Dispose,不提倡使用GetMem和FreeMem,这是因为如果结果体中存在String和动态数组,用GetMem和FreeMem会造成内存泄漏.
内存操作:复制、清空、填充内存复制内存复制是用以下三个函数:MoveMemory、CopyMemory、Move,其中MoveMemory和CopyMemory是同样功能的函数,都是调用Move函数操作,这个我们可以通过查看它们的代码看到:procedureMoveMemory(Destination:Pointer;Source:Pointer;Length:DWORD);beginMove(Source^,Destination^,Length);end;procedureCopyMemory(Destination:Pointer;Source:Pointer;Length:DWORD);beginMove(Source^,Destination^,Length);end;Move函数会自动寻址,因此我们调用的时候不能直接给地址,如复制两个整数,正确写法是:varAValue1,AValue2:Integer;beginMove(AValue1,AValue2,SizeOf(Integer));end;在DELPHI中,Move函数是汇编实现的,效率非常高,为了方便大家查看我把下面的代码加了注释,图片效果更好看一点.
procedureMove(constSource;varDest;count:Integer);{$IFDEFPUREPASCAL}varS,D:PChar;I:Integer;beginS:=PChar(@Source);D:=PChar(@Dest);ifS=DthenExit;ifCardinal(D)>Cardinal(S)thenforI:=count-1downto0doD[I]:=S[I]elseforI:=0tocount-1doD[I]:=S[I];end;{$ELSE}asm{->EAXPointertosource}{EDXPointertodestination}{ECXCount}PUSHESI//在DELPHI的内嵌汇编中,只有EAX、EDX、ECX三个寄存器可以自由使用PUSHEDI//别的都需要在使用之后还原,一般采用的是压栈出栈方式保护MOVESI,EAX//EAX接收第一个参数,因此EAX指向Source,ESI=SourceMOVEDI,EDX//EDX接收第二个参数,因此EDX指向DEST,EDI=DestMOVEAX,ECX//保存ECX到EAX中,利于后面还原CMPEDI,ESI//比较Source和DestJA@@down//DEST高于Source,则跳转到downJE@@exit//内存地址相同则退出SARECX,2//ECX:=ECXDIV4JS@@exit//负号跳转REPMOVSD//按每次4字节进行循环拷贝MOVECX,EAX//还原ECXANDECX,03H//ECX:=ECXmod4REPMOVSB//按单字节拷贝循环拷贝剩余字节JMP@@exit//退出@@down:LEAESI,[ESI+ECX-4]//ESI指向Source的最后不足4字节的地址LEAEDI,[EDI+ECX-4]//EDI指向Source的最后不足4字节的地址SARECX,2//ECX:=ECXDIV4JS@@exit//负号跳转STD//方向设置REPMOVSD//按每次4字节进行循环拷贝MOVECX,EAX//还原ECXANDECX,03H//ECX:=ECXmod4ADDESI,4-1//不足4字节的地址ADDEDI,4-1//不足4字节的地址REPMOVSB//按单字节拷贝循环拷贝剩余字节CLD@@exit:POPEDI//还原EDIPOPESI//还原ESIend;{$ENDIF}我们知道32位机器按4字节拷贝是最快的,从上面的代码可以看出,它采用也是这种做法,先循环按4字节拷贝,然后循环按1字节拷贝剩下的.
内存清空与填充内容清空与填充是用到以下三个函数:FillMemory、ZeroMemory、FillChar,其中FillMemory和ZeroMemory都是调用FillChar函数,这个我们可以从它们的源代码中得出.
procedureFillMemory(Destination:Pointer;Length:DWORD;Fill:Byte);beginFillChar(Destination^,Length,Fill);end;procedureZeroMemory(Destination:Pointer;Length:DWORD);beginFillChar(Destination^,Length,0);end;DELPHI中的FillChar也是用汇编代码实现的,其实现的思路和Move函数相同,都是按4字节赋值.
内存映射内存映射是Windows提供可以在进程之间共享内存的方法,主要是由以下四个函数:CreateFileMapping、OpenFileMapping、MapViewOfFile、UnmapViewOfFile组成,这方面的资料非常多,很多DELPHI的书籍都有介绍.
我们这给出一个在系统服务和IIS之间(不通系统账户、不通权限)实现共享内存的例子.
FastMM内存管理器FastMM是一个由法国开发者发起的开源DELPHI内存管理器,它针对Borland内存管理器的很多弊端进行改善,采用纯汇编编写,效率比Borlan内存管理器提高2-4倍,在多线程下表现会更好.
从DELPHI2006开始,DELPHI都采用FastMM来提高其IDE的速度.
用FastMM加快DELPHIIDE的速度找到里面的FastMm\ReplacementBorlndMMDLL\Precompiled\forDelphiIDE\Performance目录下的BorlndMM.
dll替换DELPHI的Bin目录下BorlandMM.
dll即可.
用FastMM加快我们自己应用程序的速度找到里面的FastMM\ReplacementBorlndMMDLL\Delphi\Precompiled\forApplications\Performance目录下的BorlandMM.
dll放到我们应用程序目录下即可.
如果你的程序只是一个单独的EXE,则在工程的第一句加上FastMM4即可.
用FastMM检测内存泄漏用FastMM检测内存泄漏,需要在工程的第一个文件引用FastMM4和FastMM4Messages,在FastMM4Options.
inc打开FullDebugMode模式,并把FastMM\FullDebugModeDLL\Precompiled\FastMM_FullDebugMode.
dll拷贝应用程序目录下,重新编译程序,这样如果程序有内存泄漏,在退出程序的时候,FastMM会生成一个内存泄漏文档,里面有详细的堆栈信息,非常利于查找.
racknerd怎么样?racknerd美国便宜vps又开启促销模式了,机房优秀,有洛杉矶DC-02、纽约、芝加哥机房可选,最低配置4TB月流量套餐16.55美元/年,此外商家之前推出的最便宜的9.49美元/年套餐也补货上架,同时RackNerd美国AMD VPS套餐最低才14.18美元/年,是全网最便宜的AMD VPS套餐!RackNerd主要经营美国圣何塞、洛杉矶、达拉斯、芝加哥、亚特兰大、新...
DiyVM是一家成立于2009年的国人主机商,提供的产品包括VPS主机、独立服务器租用等,产品数据中心包括中国香港、日本大阪和美国洛杉矶等,其中VPS主机基于XEN架构,支持异地备份与自定义镜像,VPS和独立服务器均可提供内网IP功能。商家VPS主机均2GB内存起步,三个地区机房可选,使用优惠码后每月69元起;独立服务器开设在香港沙田电信机房,CN2线路,自动化开通上架,最低499元/月起。下面以...
轻云互联怎么样?轻云互联,广州轻云网络科技有限公司旗下品牌,2018年5月成立以来,轻云互联以性价比的价格一直为提供个人,中大小型企业/团队云上解决方案。本次轻云互联送上的是美国圣何塞cn2 vps(免费50G集群防御)及香港沙田cn2 vps(免费10G集群防御)促销活动,促销产品均为cn2直连中国大陆线路、采用kvm虚拟技术架构及静态内存。目前,轻云互联推出美国硅谷、圣何塞CN2GIA云服务器...