文章目录
一、数据结构中的堆和栈
1. 栈
是一种连续储存的数据结构,具有先进后出的性质。
通常的操作有入栈(压栈),出栈和栈顶元素。想要读取栈中的某个元素,就是将其之间的所有元素出栈才能完成。
2. 堆
是一种非连续的树形储存数据结构,每个节点有一个值,整棵树是经过排序的。特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。常用来实现优先队列,存取随意。
二、内存中的栈区与堆区
1. 内存中的栈区与堆区比较
栈区 | 堆区 |
---|---|
Stack memory内存空间由操作系统自动分配和释放。 | Heap Memory内存空间手动申请和释放的,Heap Memory内存常用new关键字来分配。 |
Stack Memory内存空间有限。 | Heap Memor的空间是很大的自由区几乎没有空间限制。 |
2. 计算机内存的大致划分
一般说到内存,指的是计算机的随机存储器(RAM),程序都是在这里面运行。
三、栈内存与栈溢出
由程序自动向操作系统申请分配以及回收,速度快,使用方便,但程序员无法控制。只要栈的剩余空间大于所申请的空间,系统将为程序提供内存,否则将报异常提示栈溢出,即若分配失败,则提示栈溢出错误。
在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。
因此,能从栈获得的空间较小。
😒 注意,const局部变量也储存在栈区内,栈区向地址减小的方向增长。
#include <iostream>
int main()
{
int i = 10; //变量i储存在栈区中
const int i2 = 20;
int i3 = 30;
std::cout << &i << " " << &i2 << " " << &i3 << std::endl;
return 0;
}
&i3 < &i2 < &i,证明地址是减小的。
四、堆内存与内存泄露
程序员向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
- 分配的速度较慢,地址不连续,容易碎片化。
- 由程序员申请,同时也必须由程序员负责销毁,否则导致内存泄露。
堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
//测试堆内存和栈内存的区别
#include <iostream>
int main()
{
int i = 10; //变量i储存在栈区中
char pc[] = "hello!"; //储存在栈区
const double cd = 99.2; //储存在栈区
static long si = 99; //si储存在可读写区,专门用来储存全局变量和静态变量的内存
int* pi = new int(100); //指针pi指向的内存是在堆区,专门储存程序运行时分配的内存
std::cout << &i << " " << &pc << " " << &cd << " " << &si << " " << pi << std::endl;
delete pi; //需程序员自己释放
return 0;
}
五、JAVA
1. Java中的堆与栈
在Java中:
- 声明的对象是先在栈内存中为其分配地址空间,
- 在对其进行实例化后则在堆内存中为其分配地址。
例如:
Person p = null ; 只在Stack Memory中为其分配地址空间
p = new Person(); 则在Heap Memory中为其分配内存地址
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,
- 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,
- 当超过变量的作用域后,Java 会自动释放掉为该变量分配的内存空间,该内存空间可以立即被另作它用。
堆内存用来存放由 new 创建的对象和数组,在堆中分配的内存,由 Java 虚拟机的自动垃圾回收器来管理。
2. 引用变量
在堆中产生了一个数组或者对象之后,还可以在栈中定义一个特殊的变量,让栈中的这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或者对象,引用变量就相当于是为数组或者对象起的一个名称。
引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。
而数组和对象本身在堆中分配,即使程序运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。
3. 堆和非堆内存
按照官方的说法:“Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配。 堆是在 Java 虚拟机启动时创建的。在JVM中,堆之外的内存称为非堆内存(Non-heap memory)”。
可以看出JVM主要管理两种类型的内存:堆和非堆。
简单来说:
- 堆就是Java代码可及的内存,是留给开发人员使用的;
- 非堆就是JVM留给自己用的,所以
- 方法区、
- JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、
- 每个类结构(如运行时常数池、字段和方法数据)
- 方法和构造方法 的代码
都在非堆内存中。
六、《C++内存管理技术内幕》
1. C++中,内存分成5个区
根据《C++内存管理技术内幕》一书,在C++中,内存分成5个区,分别是:堆、栈、自由存储区、全局/静态存储区、常量存储区。
a)栈
内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数。
为运行函数而分配的局部变量、函数参数、返回地址等存放在栈区。
栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
b)堆
内存使用new进行分配,使用delete或delete[]释放。
如果未能对内存进行正确的释放,会造成内存泄漏。
但在程序结束时,会由操作系统自动回收。
c)自由存储区
使用malloc进行分配,使用free进行回收。
和堆类似。
d)全局/静态存储区
全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了。
全局变量、静态数据、常量存放在全局数据区;使用静态关键字static声明,在静态存储区申请一个静态变量。
e)常量存储区
存储常量,不允许被修改。
2. C++中,内存分成3个区
这里,在一些资料中是这样定义C++内存分配的,可编程内存在基本上分为这样的几大部分:静态存储区、堆区和栈区。他们的功能不同,对他们使用方式也就不同。
a)静态存储区
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。它主要存放静态数据、全局数据和常量。
b)栈区
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
c)堆区
亦称动态内存分配。
程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或 delete释放内存。
动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存。
但是,良好的编程习惯是:如果某动态内存不再使用,需要将其释放掉,否则,我们认为发生了内存泄漏现象。
七、堆和栈究竟有什么区别?
管理方式不同:
- 对于栈来讲,是由编译器自动管理,无需我们手工控制;
- 对于堆来说,释放工作由程序员控制,容易产生memory leak。
空间大小不同:
- 一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。
- 对于栈来讲,一般都是有一定的空间大小的。默认的栈空间大小是1M了。不过可以修改其大小。
能否产生碎片不同:
- 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
- 对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
生长方向不同:
- 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;
- 对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式不同:
- 堆都是动态分配的,没有静态分配的堆。
- 栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由allocal 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
分配效率不同:
- 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
- 堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
总结
可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,推荐大家尽量用栈,而不是用堆。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好。
内存使用规则:
【规则1】用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
【规则2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
【规则3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
【规则4】动态内存的申请与释放必须配对,防止内存泄漏。
【规则5】用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
C++与C语言的内存分配存在一些不同,但是整体上就一致的,不会影响程序分析。就C++而言,不管是5部分还是3大部分,只是分法不一致,将5部分中的c)d)e)合在一起则是3部分的a)。
下面几段代码,则会让你有豁然明白的感觉:
void fn(){
int* p = new int[5];
}
看到new,首先应该想到,我们分配了一块堆内存,那么指针p呢?
它分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。程序会先确定在堆中分配内存的大小,然后调用 operator new分配内存,然后返回这块内存的首地址,放入栈中。
注意:这里为了简单并没有释放内存,那么该怎么去释放呢? 是delete p
么? NO,错了,应该是delete [ ]p
,这是告诉编译器:删除的是一个数组。
//main.cpp
int a = 0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[] = "abc"; //栈
char *p2; //栈
char *p3 = "123456"; //123456\0在常量区,p3在栈上。
static int c =0; // 全局(静态)初始化区
// 分配得来得10和20字节的区域就在堆区。
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
strcpy(p1, "123456"); // 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}
学习:
https://www.cnblogs.com/jiudianren/p/5671992.html
https://msd.misuland.com/pd/2884250137616452012