一、操作系统上的堆、栈
引用:数组名大小为什么必须是常量?
/*报错代码*/
int a;
scanf("%d",&a);
int array[a];
答:
- int array[a]这是在栈上分配的,大小在编译时就要决定下来
- 在堆上就没问题: int *array = new int[a];
一、预备知识
1.C/C++编译的程序占用的内存分配部分
区域 | 储存对象 | 分配释放 | tips | 例子: |
---|---|---|---|---|
栈区/堆栈(stack)① | 编译期静态分配的,也能动态分配⑧,存放函数的参数值,局部变量等,一般放基本的数据类型,对象的地址。 | 由编译器自动分配、释放;一般在函数/作用域结束后自动销毁; | 其操作方式类似于数据结构中的栈;由系统自动分配。 | int b; |
堆区(heap) | 运行时动态分配的,程序员用malloc的方式,自己申请并指明大小的空间。 | 一般由程序员分配释放⑥,若程序员不释放,程序结束时可能由OS回收。一般一个malloc对应一个free | 与数据结构中的堆不同,分配方式类似于链表; | p1 = (char *)malloc(10); 注意p1本身在栈中。 |
自由存储区(free store)(C++的一个抽象概念,实际上仍然是在堆上,但不等价于堆) | 与堆类似,通过new的方式,申请并指明大小的空间。一般放对象⑦ | 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。一般一个new对应一个delete | p2 = new char[10]; 注意p2本身在栈中。 |
|
全局/静态存储区)(static) | 全局变量和静态变量的存储放在一块。 | 程序结束后释放 | ||
常量存储区 | 放常量字符串,不允许修改(非正当手段也可以修改,《const的思考》中给出了6种方法) | 程序结束后释放 | ||
程序代码区 | 存放函数体的二进制代码 |
tips:
①堆、栈合起来说——堆栈指的是栈
②栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高,更快。
③堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多,但堆更灵活。
④推荐尽量用栈,而不是用堆。 但是由于和堆相比栈不是那么灵活;有时候分配大量的内存空间,还是用堆好一些。
⑤基本上,所有的C++编译器默认使用堆来实现自由存储,运算符new和delete内部默认是使用malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。
说明:new和delete是运算符。
运算符可以重载,当new被程序员重载,且内部实现并非只有malloc()时,此时的内存空间就和堆不同了,这是一块组合的内存空间,C++中称为“自由存储区”,这是一个抽象的概念。
⑥在堆中创建的对象会一直存在,这也是java为什么需要垃圾回收的原因(C++没有垃圾回收,new出来的对象,要自己delete)。
⑦C++中,对象既可以建在堆上,也可以建在堆栈中,但是在堆上的对象如果不用了,程序员需要自己回收空间;java中,对象都建在堆上,系统会自动回收不再使用的空间;
⑧栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。堆都是动态分配(没有静态分配的堆)。
2.例子
int a = 0;//全局初始化区
char *p1;//全局未初始化区
main()
{
int b;//栈
char s[] = "abc";//栈
char *p2;//栈
char *p3 = "123456";//123456/0在常量区,p3在栈上
static int c = 0;//全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
//分配得来的10和20字节的区域在堆区
strcpy(p1,"123456");//123456/0放在常量区,编译器可能会将它与p3所指向的”123456”优化成一个地方。
}
void f(){
int* p = new int[5];
}
这条短短的一句话就包含了堆与栈,看到new,我们首先就应该想到,我们分配了一块堆内存,那么指针p呢?他分配的是一块栈内存,所以这句话的意思就是:
在栈内存中存放了一个指向一块堆内存的指针p。
该程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,然后返回这块内存的首地址,放入栈中,他在VC6下的汇编代码如下:
00401028 push 14h // 传入分配的字节数 4*5
0040102A call operator new (00401060) // 调用new过程分配程序
0040102F add esp,4 // 平衡载指针
00401032 mov dword ptr [ebp-8],eax
00401035 mov eax,dword ptr [ebp-8]
00401038 mov dword ptr [ebp-4],eax
二、理论知识
2.1 申请方式
栈区(stack)
由系统自动分配。
堆区(heap)
需要程序员自己申请,并指明大小//在c中malloc函数 p1 = (char *)malloc(10); //在C++中用new运算符 p2 = new char[10]; //但是注意p1、p2本身是在栈中的。
2.2 系统对申请的响应
申请栈区空间:
只要栈的剩余空间大于申请空间,系统将为程序提供内存,否则将报异常:提示栈溢出。
申请堆区空间:
①操作系统有一个记录空闲内存地址的链表,
②当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点。
③然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。(大多数系统,会在这块内存空间中的首地址处,记录本次分配的大小,这样也便于delete语句释放)
④由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
2.3 申请大小的限制
区域 | 定义 | 解释 |
---|---|---|
申请栈区 | 在Windows下,栈向下,是向低地址扩展的数据结构,是一块连续的内存的区域。 | 栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数), 如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。 |
申请堆区 | 堆向上,是向高地址扩展的数据结构,是不连续的内存区域。 | 由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 |
2.4 申请效率与碎片问题的比较
栈由系统自动分配,进出一一对应,不会产生碎片,速度较快。但程序员是无法控制的。
堆由new分配的内存,频繁的new/delete会造成大量碎片,使程序效率降低。 不过用起来最方便。
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,它不是在堆,也不是在栈,是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。
2.5 堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中的下一条指令的地址(函数调用语句的下一条可执行语句),然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
2.6 存取效率的比较
char s1[] = “aaaaaaaaaaaaaaa”;//aaaaaaaaaaa是在运行时刻赋值的;用数组比用指针速度要快一些,因为指针在底层汇编中需要用edx寄存器中转一下,而数组在栈上直接读取。
char *s2 = “bbbbbbbbbbbbbbbbb”;//而bbbbbbbbbbb是在编译时就确定的;
//但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。
//比如:
int main()
{
char a = 1;
char c[] = “1234567890”;
char *p = ”1234567890”;
a = c[1];
a = p[1];
return 0;
}
/* 对应的汇编代码 */
10: a = c[1];
00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
0040106A 88 4D FC mov byte ptr [ebp-4],cl
11: a = p[1];
0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
00401070 8A 42 01 mov al,byte ptr [edx+1]
00401073 88 45 FC mov byte ptr [ebp-4],al
//第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。
二、数据结构中的堆、栈
堆(数据结构):(满足堆性质的)优先队列的一种数据结构,第1个元素有最高的优先权;
它也可以被看成是一棵树,如:堆排序;
栈(数据结构):一种足先进后出的数据结构。
三、JAVA内存划分上的堆与栈
一、JVM内存的划分有五片:
- 寄存器;
- 本地方法区;
- 方法区;
- 栈内存;
- 堆内存。
二、Java中的堆内存与栈内存
2.1 堆内存与栈内存的定义
栈内存:栈内存首先是一片内存区域,存储的都是局部变量,凡是定义在方法中的都是局部变量(方法外的是全局变量),for循环内部定义的也是局部变量,是先加载函数才能进行局部变量的定义,所以方法先进栈,然后再定义变量,变量有自己的作用域,一旦离开作用域,变量就会被释放。栈内存的更新速度很快,因为局部变量的生命周期都很短。
堆内存:存储的是数组和对象(其实数组就是对象),凡是new建立的都是在堆中,堆中存放的都是实体(对象),实体用于封装数据,而且是封装多个(实体的多个属性),如果一个数据消失,这个实体也没有消失,还可以用,所以堆是不会随时释放的,但是栈不一样,栈里存放的都是单个变量,变量被释放了,那就没有了。堆里的实体虽然不会被释放,但是会被当成垃圾,Java有垃圾回收机制不定时的收取。
2.2 java中堆、栈的实例
比如主函数里的语句 int [] arr=new int [3];在内存中是怎么被定义的:
主函数先进栈,在栈中定义一个变量arr,接下来为arr赋值,但是右边不是一个具体值,是一个实体。实体创建在堆里,在堆里首先通过new关键字开辟一个空间,内存在存储数据的时候都是通过地址来体现的,地址是一块连续的二进制,然后给这个实体分配一个内存地址。数组都是有一个索引,数组这个实体在堆内存中产生之后每一个空间都会进行默认的初始化(这是堆内存的特点,未初始化的数据是不能用的,但在堆里是可以用的,因为初始化过了,但是在栈里没有),不同的类型初始化的值不一样。所以堆和栈里就创建了变量和实体:
那么堆和栈是怎么联系起来的呢?
我们刚刚说过给堆分配了一个地址,把堆的地址赋给arr,arr就通过地址指向了数组。所以arr想操纵数组时,就通过地址,而不是直接把实体都赋给它。这种我们不再叫他基本数据类型,而叫引用数据类型。称为arr引用了堆内存当中的实体。(可以理解为c或c++的指针,Java成长自c++和c++很像,优化了c++)
如果当int [] arr=null;
arr不做任何指向,null的作用就是取消引用数据类型的指向。
当一个实体,没有引用数据类型指向的时候,它在堆内存中不会被释放,而被当做一个垃圾,在不定时的时间内自动回收,因为Java有一个自动回收机制,(而c++没有,需要程序员手动回收,如果不回收就越堆越多,直到撑满内存溢出,所以Java在内存管理上优于c++)。自动回收机制(程序)自动监测堆里是否有垃圾,如果有,就会自动的做垃圾回收的动作,但是什么时候收不一定。
所以堆与栈的区别很明显:
1.栈内存存储的是局部变量而堆内存存储的是实体;
2.栈内存的更新速度要快于堆内存,因为局部变量的生命周期很短;
3.栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的实体会被垃圾回收机制不定时的回收。