DELPHI栈和堆

SQLDebug_Fan 2010-07-15 03:53:41
加精
2.1 栈
栈是由操作系统在创建线程的时候,系统自动创建,栈是由顶像下分配的,DELPHI中默认的栈大小是1M,这个可以通过Project->Options->Linker->Max Stack size来改变其大小。
栈是线程执行代码的地方,操作系统根据系统调度算法来加载执行的代码,另外栈还存放函数的参数值,局部变量。栈的存取是按4字节偏移,不会根据需要动态增长,因此超出范围会报栈溢出。
2.2 堆
我们把在栈之外的分配内存都叫在堆上分配内存,堆是由程序员分配释放。在DELPHI中是用GetMem.inc中的代码来管理堆的,堆中包含许多大小不确定的块。初始状态下,堆仅有一个块,即堆本身。经过一段时间地取用和回收以后,堆中将可能只剩下一些“切割”后残余的“碎片”,且这些碎片可能已经无法再合并。此时,如果一个新的请求大于任何一个碎片,那么就必须再申请一个新的、大的块放在堆中。堆的使用永远是一个“拆东墙补西墙”的过程。
堆的大小是2G,在扩展内存模式下能达到3G。注意它与数据结构中的堆是两回事,它的分配方式类似于链表,访问“堆”的内容的时候需要先找到这个“堆”,然后再遍历链表,因此“堆”访问会比“栈”慢。
2.3 哪些在栈中
2.3.1 获取栈的首尾地址
获取通常情况下的栈地址
在写汇编的时候,我们知道esp存放栈顶指针,ebp存放栈底指针
procedure GetStackAddress(var AStackTop, AStackBottom: Cardinal);
begin
asm
mov [eax], esp; //栈顶,eax接收第一个参数
mov [edx], ebp; //栈底,edx接收第二个参数
end;
end;
获取异常发生时的栈地址
在Windows下,FS:[4]存放发生异常时的栈顶指针。
procedure GetStackAddress(var AStackTop, AStackBottom: Cardinal);
begin
asm
mov ecx, FS:[4]; //FS:[4]放置发生异常时的栈信息
sub ecx, 3;
mov [eax], eax; //栈顶,eax接收第一个参数
mov [edx], ebp; //栈低,edx接收第二个参数
end;
end;
知道了栈的首尾地址之后,我们就可以取出变量地址,然后和栈的地址比较,如果超出栈的范围,则表示变量在堆中。
2.3.2 基本数据类型(Integer、Cardinal、Shortint、Smallint、Longint、Int64、Byte、Word、LongWord、Char)都是在栈中的
基本类型在函数体内分配是在栈中的,如果在类中分配则是在堆中的。另外Int64也是在栈中分配的,它具体的分配是偏移8字节。我们写下如下测试代码:
procedure TestInt64;
var
Value: Int64;
StackTop, StackBottom: Cardinal;
begin
Value := 10;
GetStackAddress(StackTop, StackBottom);
ShowMessage(Format('StackTop: %s, StackBottom: %s; Int64 Address: %s',
[IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@Value), 8)]));
end;
我电脑测试显示的信息为StackTop: 0012F5E0, StackBottom: 0012F628; Int64 Address: 0012F620,从上面信息我们可以看出栈底偏8字节就是Value的地址。
2.3.3 指针类型是指针在栈中,指针所指向的地址在堆中
指针在函数体内分配,指针的地址是在栈中的,指针的内容是在堆中的。指针如果在类中分配则,指针地址和指针内容都是在堆中的。我们写下如下测试代码:
procedure TestPointer;
var
APoint: Pointer;
StackTop, StackBottom: Cardinal;
begin
GetMem(APoint, 1000);
GetStackAddress(StackTop, StackBottom);
ShowMessage(Format('StackTop: %s, StackBottom: %s; Pointer Address: %s; Pointer Content Address: %s',
[IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@APoint), 8),
IntToHex(Integer(APoint), 8)]));
end;
我的电脑测试显示的信息为StackTop: 0012F568, StackBottom: 0012F5B8; Pointer Address: 0012F5B4; Pointer Content Address: 00A3FD10,从上面的信息我们可以栈底偏4字节就是指针的地址。
2.3.4 固定数组在栈中
固定数组在函数体内分配是在栈中的,如果在类中分配则是在堆中的。因此不能函数体内分配超过1M大小的固定数组,否则会造成栈溢出。我们写下如下测试代码:
type
TFixArray = array[0..9] of Integer;
procedure TestFixArray;
var
FixArray: TFixArray;
StackTop, StackBottom: Cardinal;
begin
FixArray[0] := 10;
GetStackAddress(StackTop, StackBottom);
ShowMessage(Format('StackTop: %s, StackBottom: %s; Int64 Address: %s',
[IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@FixArray[0]), 8)]));
end;
我的电脑测试显示的信息为StackTop: 0012F550, StackBottom: 0012F5B8; Fix Array Address: 0012F588,从上面的信息我们可以看出固定数组是在栈中的,动态数组类似指针,只是动态数组的指针在栈中,动态数组的内容是在堆中的。另外我们从汇编代码也可以看出相同的信息,FixArray[0] := 10对应的汇编代码是mov [ebp-$30],$0000000a,ebp指向栈底,因此我们可以看出动态数组的内存是在栈中的。
2.3.5 结构体在栈中
结构体在函数体内分配是在栈中的,在类中分配则是在堆中的,如果结构体内含有string等指针类型,则指针的地址在站内,指针的内容是在堆中的。在函数体内分配超过1M大小的结构体也会造成栈溢出。我们写下如下测试代码:
type
TRecord = record
Value: string;
Len: Integer;
end;
procedure TestRecord;
var
PntRecord: TRecord;
StackTop, StackBottom: Cardinal;
begin
PntRecord.Value := 'Test';
GetStackAddress(StackTop, StackBottom);
ShowMessage(Format('StackTop: %s, StackBottom: %s; Record Address: %s; Record Pointer Address: %s',
[IntToHex(StackTop, 8), IntToHex(StackBottom, 8), IntToHex(Integer(@PntRecord), 8),
IntToHex(Integer(PChar(PntRecord.Value)), 8)]));
end;
我的电脑测试显示的信息为StackTop: 0012F564, StackBottom: 0012F5B8; Record Address: 0012F5B0; Record Pointer Address: 0045F4B4,从上面的信息我们可以看出结构体是在栈中的,结构体指针和指针一样。
2.4 哪些在堆中
用内存申请函数申请的内存都是在堆中的,如用New、GetMem、StrAlloc、AllocMem、SysGetMem,哪些自管理类型string、动态数组的内容都是在堆中的,下面我们给出结论,测试代码大家可以仿照上面的判断变量是否在栈中的代码编写。
2.4.1 指针指向的内容是在堆中
2.4.2 动态数组的内容是在堆中
2.4.3 String、ShortString、WideString的内容是在堆中
2.4.4 变体Variant、OleVariant的内容是在堆中
变体类型是一个结构体,它的定义是:
TVarData = packed record
case Integer of
0: (VType: TVarType;
case Integer of
0: (Reserved1: Word;
case Integer of
0: (Reserved2, Reserved3: Word;
case Integer of
varSmallInt: (VSmallInt: SmallInt);
varInteger: (VInteger: Integer);
varSingle: (VSingle: Single);
varDouble: (VDouble: Double);
varCurrency: (VCurrency: Currency);
varDate: (VDate: TDateTime);
varOleStr: (VOleStr: PWideChar);
varDispatch: (VDispatch: Pointer);
varError: (VError: HRESULT);
varBoolean: (VBoolean: WordBool);
varUnknown: (VUnknown: Pointer);
varShortInt: (VShortInt: ShortInt);
varByte: (VByte: Byte);
varWord: (VWord: Word);
varLongWord: (VLongWord: LongWord);
varInt64: (VInt64: Int64);
varString: (VString: Pointer);
varAny: (VAny: Pointer);
varArray: (VArray: PVarArray);
varByRef: (VPointer: Pointer);
);
1: (VLongs: array[0..2] of LongInt);
);
2: (VWords: array [0..6] of Word);
3: (VBytes: array [0..13] of Byte);
);
1: (RawData: array [0..3] of LongInt);
end;
从定义中我们可以看出varOleStr、varString、varArray、varByRef都是在堆中的。
2.5 全局变量在堆中
全局变量的指针地址和指针内容都是在栈中的,我们把他归类到堆中。
2.6 栈和堆比较
2.6.1 栈和堆的管理方式比较
栈:由操作系统自动分配,而且在栈上分配内存是由编译器自动完成的,栈不需要编译器管理,操作系统自动实现申请释放;
堆:由操作系统提供接口,各个编译器实现管理方式,由外部程序申请释放,如果外部程序在程序结束时没有释放,由操作系统强行释放,在DELPHI中是用GetMem.inc来实现内存管理;
2.6.2 栈和堆的初始化比较
栈:分配的内存不会初始化,是一个垃圾值;
堆:分配的内存不会初始化,是一个垃圾值,但是DELPHI默认初始化类变量和全局变量;
2.6.3 栈和堆的申请方式比较
栈:由系统自动分配,如在函数申明一个局部变量i: Integer;编译器会自动在栈中分配内存;
堆:由程序自己管理,需要程序员自己申请,并指明大小;
2.6.4 堆和栈的效率比较
栈:在栈上分配空间是直接用add指令,对esp进行移位,例如add esp,-$44,可以在一个指令周期内完成;
堆:操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码中的FreeMem语句才能正确的释放本内存空间。另外由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。 在堆中分配内存的时候会用HeapLock和HeapUnlock加锁,因此在多线程中分配内存是线性的,效率低下;
2.6.5 栈和堆的大小限制比较
栈:在Windows下栈默认大小是1M, 栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是固定的(是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
堆:在Windows下默认堆大小是2GB,堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2.6.6 栈和堆的超出范围比较
栈:超出栈大小会报栈溢出;
堆:超出堆大小会报Out Of Memory;

原帖地址:http://blog.csdn.net/SQLDebug_Fan/archive/2010/07/15/5737329.aspx
...全文
3213 135 打赏 收藏 转发到动态 举报
写回复
用AI写文章
135 条回复
切换为时间正序
请发表友善的回复…
发表回复
hhj0301 2010-09-29
  • 打赏
  • 举报
回复
长进了,谢谢分享!
cq_mark 2010-09-13
  • 打赏
  • 举报
回复
又学习了新知识了
yyzzzh 2010-08-12
  • 打赏
  • 举报
回复
栈和堆的理解 讲的很详细,支持一下!
hucaiyu2010 2010-08-12
  • 打赏
  • 举报
回复
堆和栈其实很简单;
比如局部变量和stdcall的参数就在栈里面,new或create的就在内存堆里面
AsheBin 2010-08-11
  • 打赏
  • 举报
回复
好人啊,送知识还送分!
fenshm 2010-08-11
  • 打赏
  • 举报
回复
接分生娃开宝马
modney 2010-08-11
  • 打赏
  • 举报
回复
呵呵,沙发。。。。。
IVC2009 2010-07-26
  • 打赏
  • 举报
回复
有点不明白
qiaoyan1981 2010-07-26
  • 打赏
  • 举报
回复
还是JAVA的垃圾回收机制自动一些,其他面向对象语言都没做这个
SQLDebug_Fan 2010-07-23
  • 打赏
  • 举报
回复
[Quote=引用 125 楼 rainychan2009 的回复:]

引用 44 楼 lhylhy 的回复:
这文章看不出Delphi的特色呀?

windows上的内存管理!
[/Quote]
在Windows上各个语言基本相同,这篇文档除了示例代码是DELPHI的,其它都可以用于别的语言。
rainychan2009 2010-07-23
  • 打赏
  • 举报
回复
[Quote=引用 44 楼 lhylhy 的回复:]
这文章看不出Delphi的特色呀?
[/Quote]
windows上的内存管理!
好兄弟好兄弟 2010-07-21
  • 打赏
  • 举报
回复
不错,不错,进来学习了。
xiaoxiangqing 2010-07-21
  • 打赏
  • 举报
回复
说得太详细了,谢谢分享
ximenziplc 2010-07-21
  • 打赏
  • 举报
回复
Delphi
peacewong 2010-07-21
  • 打赏
  • 举报
回复
个人感觉delphi是个不错的编译环境,很喜欢
yaolei007 2010-07-21
  • 打赏
  • 举报
回复
很不错~~谢谢分享
seastars 2010-07-21
  • 打赏
  • 举报
回复
相当高级深入的知识
jornye 2010-07-20
  • 打赏
  • 举报
回复
[Quote=引用 95 楼 tan124 的回复:]
学习下....
[/Quote]
只有引用的内容不允许回复!
haibuku 2010-07-20
  • 打赏
  • 举报
回复
学一下
llwlz 2010-07-20
  • 打赏
  • 举报
回复
路过,接分
加载更多回复(107)

1,183

社区成员

发帖
与我相关
我的任务
社区描述
Delphi Windows SDK/API
社区管理员
  • Windows SDK/API社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧