堆和栈

介绍堆和栈以前先介绍些内存存储预备知识:java

1)静态的:静态存储分配指在编译时就能肯定每一个数据目标在运行时刻的存储空间需求,于是在编译时就能够给它们分配固定的内存空间,这种分配策略要求程序代码中不容许有可变数据结构的存在,也不容许有嵌套或者递归的结构出现,由于它们都会致使编译程序没法计算准确的存储空间需求。程序员

2)栈式的:栈式存储分配也可称为动态存储分配,是由一个相似于堆栈的运行栈类实现的,和静态存储分配相反,在栈式存储方案中,程序对数据区的需求在编译时是彻底未知的,只有到运行的时候才可以知道,但规定在运行中进入一个程序模块时,必须知道该程序模块所需的数据区大小才可以为其分配内存,和数据结构中的栈同样,按照后进先出的原则进行分配。编程

3)堆式的:静态存储分配要求在编译时能肯定全部变量的存储要求,栈式存储分配要求在过程的入口处必须知道全部的存储要求,而堆式存储分配则专门负责在编译时或运行时模块入口都没法肯定存储要求的数据结构的内存分配。好比可变长度字符串、对象实例。堆由大片的可利用块或空闲块组成,堆中的内存能够按照任意顺序分配和释放。数组

 

 堆栈有两种解释,一种是数据结构中的堆栈,一种是操做系统内存存储方式的堆栈。数据结构

数据结构的栈和堆:多线程

首先在数据结构上要知道堆栈,尽管咱们这么称呼它,但实际上堆栈是两种数据结构:堆和栈。函数

堆和栈都是一种数据项按序排列的数据结构。性能

栈做为一种数据结构,是一种只能在一端进行插入和删除的特殊线性表,这一端称为栈顶,按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,须要读数据的时候从栈顶开始弹出数据。经常使用到的两个操做出栈POP和入栈PUSH。操作系统

栈就像装数据的桶或箱子,它是一种具备后进先出性质的数据结构,也就是说后存放的先取,先存放的后取。这就如同咱们要取出放在箱子里面底下的东西(放入的比较早的物体),咱们首先要移开压在它上面的物体(放入的比较晚的物体)。线程

 

堆像一棵倒过来的树

堆是一种通过排序的树形数据结构,每一个结点都有一个值。

一般咱们所说的堆的数据结构,是指二叉堆。

堆的特色是根结点的值最小(或最大),且根结点的两个子树也是一个堆。因为堆的这个特性,经常使用来实现优先队列,堆的存取是随意,这就如同咱们在图书馆的书架上取书,虽然书的摆放是有顺序的,可是咱们想取任意一本时没必要像栈同样,先取出前面全部的书,书架这种机制不一样于箱子,咱们能够直接取出咱们想要的书。

 

操做系统内存分配的堆和栈:

栈:

1)栈常常与 sp 寄存器(译者注:”stack pointer”,了解汇编的朋友应该都知道)一块儿工做,最初 sp 指向栈顶(栈的高地址)。

2)CPU 用 push 指令来将数据压栈,用 pop 指令来弹栈。当用 push 压栈时,sp 值减小(向低地址扩展)。当用 pop 弹栈时,sp 值增大。存储和获取数据都是 CPU 寄存器的值。

3)当函数被调用时,CPU使用特定的指令把当前的 IP (译者注:“instruction pointer”,是一个寄存器,用来记录 CPU 指令的位置)压栈。即执行代码的地址。CPU 接下来将调用函数地址赋给 IP ,进行调用。当函数返回时,旧的 IP 被弹栈,CPU 继续去函数调用以前的代码。

4)当进入函数时,sp 向下扩展,扩展到确保为函数的局部变量留足够大小的空间。若是函数中有一个 32-bit 的局部变量会在栈中留够四字节的空间。当函数返回时,sp 经过返回原来的位置来释放空间。

5)若是函数有参数的话,在函数调用以前,会将参数压栈。函数中的代码经过 sp 的当前位置来定位参数并访问它们。

6)函数嵌套调用和使用魔法同样,每一次新调用的函数都会分配函数参数,返回值地址、局部变量空间、嵌套调用的活动记录都要被压入栈中。函数返回时,按照正确方式的撤销。

7)栈要受到内存块的限制,不断的函数嵌套/为局部变量分配太多的空间,可能会致使栈溢出。当栈中的内存区域都已经被使用完以后继续向下写(低地址),会触发一个 CPU 异常。这个异常接下来会经过语言的运行时转成各类类型的栈溢出异常。(译者注:“不一样语言的异常提示不一样,所以经过语言运行时来转换”我想他表达的是这个含义)。

 

堆:

1)堆包含一个链表来维护已用和空闲的内存块。在堆上新分配(用 new 或者 malloc)内存是从空闲的内存块中找到一些知足要求的合适块。这个操做会更新堆中的块链表。这些元信息也存储在堆上,常常在每一个块的头部一个很小区域。

2)堆的增长新块一般从低地址向高地址扩展。所以你能够认为堆随着内存分配而不断的增长大小。若是申请的内存大小很小的话,一般从底层操做系统中获得比申请大小要多的内存。

3)申请和释放许多小的块可能会产生以下状态:在已用块之间存在不少小的空闲块。进而申请大块内存失败,虽然空闲块的总和足够,可是空闲的小块是零散的,不能知足申请的大小,。这叫作“堆碎片”。

4)当旁边有空闲块的已用块被释放时,新的空闲块可能会与相邻的空闲块合并为一个大的空闲块,这样能够有效的减小“堆碎片”的产生。

 

在编程中,例如C/C++中,全部的方法调用都是经过栈来进行的,全部的局部变量、形式参数都是从栈中分配内存空间的。实际上也不是什么分配,只是从栈顶向上用就行,就好像工厂中的传送带(conveyor belt)同样,Stack Pointer会自动指引你到放东西的位置,你所要作的只是把东西放下来就行,退出函数的时候,修改栈指针就能够把栈中的内容销毁,这样的模式速度最快,,所以固然要用来运行程序了。须要注意的是,在分配的时候,好比为一个即将要调用的程序模块分配数据区时,应事先知道这个数据区的大小,也就说是虽然分配是在程序运行时进行的,可是分配的大小多少是肯定的,不变的,而这个"大小多少"是在编译时肯定的,不是在运行时。

 

堆是应用程序在运行的时候请求操做系统分配给本身内存,因为从操做系统管理的内存分配,因此在分配和销毁时都要占用时间,所以用堆的效率很是低,可是堆的优势在于,编译器没必要知道要从堆里分配多少存储空间,也没必要知道存储的数据要在堆里停留多长的时间,所以,用堆保存数据时会获得更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,由于多态变量所需的存储空间只有在运行时建立了对象以后才能肯定,在C++中,要求建立一个对象时,只需用 new命令编制相关的代码便可。执行这些代码时,会在堆里自动进行数据的保存。固然,为达到这种灵活性,必然会付出必定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是致使效率低的缘由。

 

区别:

1 申请方式和回收方式不一样

堆和栈的第一个区别就是申请方式不一样:栈(英文名称是stack)是系统自动分配空间的,例如咱们定义一个 char a;系统会自动在栈上为其开辟空间。而堆(英文名称是heap)则是程序员根据须要本身申请的空间,例如malloc(10);开辟十个字节的空间。

因为栈上的空间是自动分配自动回收的,因此栈上的数据的生存周期只是在函数的运行过程当中,运行后就释放掉,不能够再访问。而堆上的数据只要程序员不释放空间,就一直能够访问到,不过缺点是一旦忘记释放会形成内存泄露。

 

2 申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,不然将报异常提示栈溢出。

堆:首先应该知道操做系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,而后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的 delete语句才能正确的释放本内存空间。另外,因为找到的堆结点的大小不必定正好等于申请的大小,系统会自动的将多余的那部分从新放入空闲链表中。也就是说堆会在申请后还要作一些后续的工做,同时这也会引出申请效率的问题。

 

3 申请效率的比较

根据第0点和第1点可知。

栈:由系统自动分配,速度较快。但程序员是没法控制的。

堆:是由new分配的内存,通常速度比较慢,并且容易产生内存碎片,不过用起来最方便。

 

4 申请大小的限制

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就肯定的常数),若是申请的空间超过栈的剩余空间时,将提示overflow。所以,能从栈得到的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是因为系统是用链表来存储的空闲内存地址的,天然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。因而可知,堆得到的空间比较灵活,也比较大。

 

5 堆和栈中的存储内容

因为栈的大小有限,因此用子函数仍是有物理意义的,而不只仅是逻辑意义。

栈: 在函数调用时,第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址,而后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,而后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,而后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:通常是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

 

6 存取效率的比较

char s1[] = "aaaaaaaaaaaaaaa";

char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在运行时刻赋值的;放在栈中。

而bbbbbbbbbbb是在编译时就肯定的;放在堆中。

可是,在之后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

 

好比:

#include

void main()

{

  char a = 1;

  char c[] = "1234567890";

  char *p ="1234567890";

  a = c[1];

  a = p[1];

  return;

}

对应的汇编代码

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

 

关于堆和栈区别的比喻

堆和栈的区别能够引用一位前辈的比喻来看出:

使用栈就象咱们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,没必要理会切菜、洗菜等准备工做和洗碗、刷锅等扫尾工做,他的好处是快捷,可是自由度小。

使用堆就象是本身动手作喜欢吃的菜肴,比较麻烦,可是比较符合本身的口味,并且自由度大。

 

JVM中的堆和栈 

JVM是基于堆栈的虚拟机,JVM为每一个新建立的线程都分配一个堆栈,也就是说,对于一个Java程序来讲,它的运行就是经过对堆栈的操做来完成的。堆栈以帧为单位保存线程的状态。JVM对堆栈只进行两种操做:以帧为单位的压栈和出栈操做。 咱们知道,某个线程正在执行的方法称为此线程的当前方法,咱们可能不知道,当前方法使用的帧称为当前帧。当线程激活一个Java方法,JVM就会在线程的 Java堆栈里新压入一个帧。这个帧天然成为了当前帧,在此方法执行期间,这个帧将用来保存参数、局部变量、中间计算过程和其余数据,这个帧在这里和编译原理中的活动纪录的概念是差很少的。

从Java的这种分配机制来看,堆栈又能够这样理解:堆栈(Stack)是操做系统在创建某个进程时或者线程(在支持多线程的操做系统中是线程)为这个线程创建的存储区域,该区域具备先进后出的特性。 每个Java应用都惟一对应一个JVM实例,每个实例惟一对应一个堆。应用程序在运行中所建立的全部类实例或数组都放在这个堆中,并由应用全部的线程共享。跟C/C++不一样,Java中分配堆内存是自动初始化的。Java中全部对象的存储空间都是在堆中分配的,可是这个对象的引用倒是在堆栈中分配,也就是说在创建一个对象时从两个地方都分配内存,在堆中分配的内存实际创建这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已。

 

在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的做用域后,java会自动释放掉为该变量分配的内存空间,该内存空间能够马上被另做他用。堆内存用于存放由new建立的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还能够在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,之后就能够在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量至关于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到做用域外释放。而数组&对象自己在堆中分配,即便程序运行到使用new产生数组和对象的语句所在地代码块以外,数组和对象自己占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,可是仍然占着内存,在随后的一个不肯定的时间被垃圾回收器释放掉。这个也是java比较占内存的主要缘由,实际上,栈中的变量指向堆内存中的变量,这就是 Java 中的指针!

 

Java 中的堆和栈

Java把内存划分红两种:一种是栈内存,一种是堆内存。

在函数中定义的一些基本类型的变量和对象的引用变量都在函数的栈内存中分配。当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的做用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间能够当即被另做他用。

堆内存用来存放由new建立的对象和数组。 在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还能够在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。 引用变量就至关因而为数组或对象起的一个名称,之后就能够在程序中使用栈中的引用变量来访问堆中的数组或对象。

 

具体的说:

栈与堆都是Java用来在Ram中存放数据的地方。与C++不一样,Java自动管理栈和堆,程序员不能直接地设置栈或堆。

Java的堆是一个运行时数据区,类的对象从中分配空间。这些对象经过new、newarray、anewarray和multianewarray等指令创建,它们不须要程序代码来显式的释放。堆是由垃圾回收来负责的,堆的优点是能够动态地分配内存大小,生存期也没必要事先告诉编译器,由于它是在运行时动态分配内存的,Java的垃圾收集器会自动收走这些再也不使用的数据。但缺点是,因为要在运行时动态分配内存,存取速度较慢。

 

栈的优点是,存取速度比堆要快,仅次于寄存器,栈数据能够共享。但缺点是,存在栈中的数据大小与生存期必须是肯定的,缺少灵活性。栈中主要存放一些基本类型的变量(,int, short, long, byte, float, double, boolean, char)和对象句柄

栈有一个很重要的特殊性,就是存在栈中的数据能够共享。假设咱们同时定义:

  int a = 3;

  int b = 3;

编译器先处理int a = 3,首先它会在栈中建立一个变量为a的引用,而后查找栈中是否有3这个值,若是没找到,就将3存放进来,而后将a指向3。接着处理int b = 3;在建立完b的引用变量后,由于在栈中已经有3这个值,便将b直接指向3。这样,就出现了a与b同时均指向3的状况。这时,若是再令a=4,那么编译器会从新搜索栈中是否有4值,若是没有,则将4存放进来,并令a指向4,若是已经有了,则直接将a指向这个地址。所以a值的改变不会影响到b的值。要注意这种数据的共享与两个对象的引用同时指向一个对象的这种共享是不一样的,由于这种状况a的修改并不会影响到b, 它是由编译器完成的,它有利于节省空间。而一个对象引用变量修改了这个对象的内部状态,会影响到另外一个对象引用变量。

 

Java为何慢?

JVM的存在固然是一个缘由,但有人说,在Java中,除了简单类型(int,char等)的数据结构,其它都是在堆中分配内存(因此说Java的一切都是对象),这也是程序慢的缘由之一。 个人想法是(应该说表明TIJ的观点),若是没有Garbage Collector(GC),上面的说法就是成立的。

堆不象栈是连续的空间,没有办法期望堆自己的内存分配可以象堆栈同样拥有传送带般的速度。由于,没有谁会为你整理庞大的堆空间,让你几乎没有延迟的从堆中获取新的空间,这个时候,GC站出来解决问题,咱们都知道GC用来清除内存垃圾,为堆腾出空间供程序使用,但GC同时也担负了另一个重要的任务,就是要让Java中堆的内存分配和其余语言中堆栈的内存分配同样快,由于速度的问题几乎是众口一词的对Java的诟病。要达到这样的目的,就必须使堆的分配也可以作到象传送带同样,不用本身操心去找空闲空间,这样,GC除了负责清除Garbage外,还要负责整理堆中的对象,把它们转移到一个远离Garbage的纯净空间中无间隔的排列起来,就象堆栈中同样紧凑,这样Heap Pointer就能够方便的指向传送带的起始位置,或者说一个未使用的空间,为下一个须要分配内存的对象"指引方向"。所以能够这样说,垃圾收集影响了对象的建立速度,听起来很怪,对不对? 还有GC怎样在堆中找到全部存活的对象呢?前面说了,在创建一个对象时,在堆中分配实际创建这个对象的内存,而在堆栈中分配一个指向这个堆对象的指针(引用),那么只要在堆栈(也有可能在静态存储区)找到这个引用,就能够跟踪到全部存活的对象。找到以后。GC将它们从一个堆的块中移到另一个堆的块中,并将它们一个挨一个的排列起来,就象咱们上面说的那样,模拟出了一个栈的结构,但又不是先进后出的分配,而是能够任意分配的,在速度能够保证的状况下,但GC()的运行要占用一个线程,这自己就是一个下降程序运行性能的缺陷,更况且这个线程还要在堆中把内存翻来覆去的折腾,不只如此,如上面所说,堆中存活的对象被搬移了位置,那么全部对这些对象的引用都要从新赋值,这些开销都会致使性能的下降。此消彼长,GC()的优势带来的效益是否盖过了它的缺点致使的损失,我也没有太多的体会。

 

注:

名称解释:

IP(Instruction Pointer):指令指针寄存器存储的是下一个时钟周期将要执行的指令所在的程序寄存器地址。

stack pointer堆栈指针:老是指向栈顶元素。堆栈的实现是往上长的(就是说往顶的方向长,其实质是栈底是定死的不能动,入栈的东西只能不断往上叠,这就像在书桌上放书同样,桌底是定死的,因此书只能一本一本地往上堆,往上长),计算机内部的堆栈的实现采起的就是这种模式,因此就得“先修改指针,而后插入数 据,出栈时恰好相反”,由于堆栈指针指向的老是栈顶元素,栈底不能动,因此数据入栈前要先修改指针使它指向新的空余空间而后再把数据存进去,出栈的时候天然相反。

相关文章
相关标签/搜索