什么是堆栈和堆?
编程语言书籍解释了值类型是在堆栈上创建的,而引用类型是在堆上创建的,而没有说明这两个是什么。我还没有阅读清楚的解释。我了解堆栈是什么。但,
- 它们在哪里和在哪里(物理上在真实计算机的内存中)?
- 它们在多大程度上受操作系统或者语言运行时的控制?
- 他们的范围是什么?
- 什么决定了它们的大小?
- 是什么使速度更快?
解决方案
回答
堆:
- 就像堆一样存储在计算机RAM中。
- 在堆栈上创建的变量将超出范围并自动释放。
- 与堆中的变量相比,分配要快得多。
- 用实际的堆栈数据结构实现。
- 存储用于参数传递的本地数据,返回地址。
- 当使用过多的堆栈时(可能来自无限或者太深的递归,非常大的分配),可能会导致堆栈溢出。
- 可以在没有指针的情况下使用在堆栈上创建的数据。
- 如果我们确切地知道在编译之前需要分配多少数据并且它不会太大,则可以使用堆栈。
- 通常,程序启动时已经确定了最大大小。
堆:
- 像堆栈一样存储在计算机RAM中。
- 在C ++中,必须手动销毁堆上的变量,并且切勿超出范围。可以通过
delete
,delete []
或者free
释放数据。 - 与堆栈上的变量相比,分配速度较慢。
- 按需使用以分配数据块以供程序使用。
- 当有很多分配和释放时,可能会产生碎片。
- 在C ++或者C语言中,在堆上创建的数据将由指针指向,并分别通过
new
或者malloc
进行分配。 - 如果请求分配过多的缓冲区,可能会导致分配失败。
- 如果我们不确切知道运行时需要多少数据或者需要分配大量数据,则可以使用堆。
- 负责内存泄漏。
例子:
int foo() { char *pBuffer; //<--nothing allocated yet (excluding the pointer itself, which is allocated here on the stack). bool b = true; // Allocated on the stack. if(b) { //Create 500 bytes on the stack char buffer[500]; //Create 500 bytes on the heap pBuffer = new char[500]; }//<-- buffer is deallocated here, pBuffer is not }//<--- oops there's a memory leak, I should have called delete[] pBuffer;
回答
堆栈
当我们调用一个函数时,该函数的参数以及其他一些开销将被放入堆栈中。一些信息(例如返回目的地)也存储在此处。
当我们在函数内声明变量时,该变量也分配在堆栈上。
取消分配栈非常简单,因为我们总是以相反的顺序进行分配。输入函数时会添加堆栈内容,退出函数时会删除相应的数据。这意味着除非我们调用许多调用许多其他函数的函数(或者创建递归解决方案),否则我们通常会停留在堆栈的较小区域内。
堆
堆是用于将创建的数据动态放置的位置的通用名称。如果我们不知道程序将要创建多少个太空飞船,则很可能使用新的(或者malloc或者等效的)运算符来创建每个太空飞船。这种分配将持续一段时间,因此很可能我们将以与创建它们不同的顺序释放事物。
因此,堆要复杂得多,因为最终会出现未使用的内存区域与内存碎片化的块交错的情况。寻找所需大小的可用内存是一个难题。这就是为什么应该避免使用堆的原因(尽管仍然经常使用堆)。
执行
堆栈和堆的实现通常取决于运行时/ OS。通常,性能至关重要的游戏和其他应用程序会创建自己的内存解决方案,这些解决方案会从堆中获取大量内存,然后在内部进行分配,以避免依赖OS来获取内存。
仅当内存使用量与正常情况(即我们在一个大型操作中加载一个关卡并可以在另一个大型操作中将所有内容丢掉)的游戏使用的标准大不相同时,这才实用。
内存中的物理位置
这与我们想像的不太相关,因为有一种称为虚拟内存的技术,该技术使程序认为我们可以访问物理数据在其他地方(甚至在硬盘上)的某个地址。随着调用树的深入,我们为堆栈获取的地址将按升序排列。堆的地址是不可预测的(即特定于隐含的),并且坦率地说并不重要。
回答
堆栈是内存的一部分,可以通过一些关键的汇编语言指令进行操作,例如" pop"(从堆栈中删除并返回值)和" push"(将值推入堆栈),但也可以调用(调用子例程,这会将地址压入堆栈,然后返回(从子例程返回,这会将地址从堆栈中弹出并跳转至该堆栈)。它是堆栈指针寄存器下方的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子例程,还用于在调用子例程之前将值保留在寄存器中。
堆是操作系统通常通过诸如malloc之类的syscall分配给应用程序的一部分内存。在现代OS上,此内存是仅呼叫进程有权访问的一组页面。
堆栈的大小是在运行时确定的,通常在程序启动后不会增长。在C程序中,堆栈必须足够大以容纳每个函数中声明的每个变量。堆将根据需要动态增长,但是操作系统最终会进行调用(它通常会使堆增长超过malloc请求的值,因此至少将来的某些malloc不需要返回内核来完成)。获得更多的内存。这种行为通常可以自定义)
因为在启动程序之前已经分配了堆栈,所以我们不需要使用malloc就可以使用堆栈,因此那是一个小优点。在实践中,很难预测在具有虚拟内存子系统的现代操作系统中哪些将是快的,哪些将是慢的,因为页面的实现方式和存储位置是实现细节。
回答
其他人已经很好地回答了大笔画,所以我将介绍一些细节。
- 堆栈和堆不必是单个的。我们拥有多个堆栈的常见情况是,如果一个进程中有多个线程。在这种情况下,每个线程都有自己的堆栈。我们也可以有多个堆,例如某些DLL配置可能导致从不同堆分配不同的DLL,这就是为什么释放由不同库分配的内存通常不是一个好主意的原因。
- 在C语言中,我们可以通过使用alloca来获得可变长度分配的好处,该分配在堆栈上进行分配,而不是在堆上进行分配。此内存无法在return语句中保留下来,但对于暂存缓冲区很有用。
- 在Windows上使用不多的巨大临时缓冲区并不是免费的。这是因为编译器将生成一个堆栈探测循环,每次进入函数时都会调用该循环以确保堆栈存在(因为Windows在堆栈末尾使用单个保护页来检测何时需要增大堆栈。如果我们访问的内存超出了堆栈末尾的一页,则会崩溃。例子:
void myfunction() { char big[10000000]; // Do something that only uses for first 1K of big 99% of the time. }
回答
堆栈是为执行线程预留的暂存空间。调用函数时,在堆栈的顶部保留了一个块,用于存放局部变量和一些簿记数据。当该函数返回时,该块将变为未使用状态,并且可以在下次调用该函数时使用。堆栈始终按LIFO(后进先出)顺序保留;最近保留的块始终是要释放的下一个块。这使得跟踪堆栈非常简单。从堆栈中释放一个块无非就是调整一个指针。
堆是为动态分配预留的内存。与堆栈不同,堆中的块分配和释放没有强制的模式。我们可以随时分配一个块并随时释放它。这使跟踪在任何给定时间分配或者释放堆的哪些部分变得更加复杂。有许多自定义堆分配器可用于针对不同的使用模式调整堆性能。
每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管对于不同类型的分配有多个堆并不少见)。
要直接回答问题:
To what extent are they controlled by the OS or language runtime?
创建线程时,操作系统会为每个系统级线程分配堆栈。通常,语言运行库会调用OS来为应用程序分配堆。
What is their scope?
堆栈连接到线程,因此当线程退出时,堆栈将被回收。通常,堆是在运行时在应用程序启动时分配的,并在应用程序(技术上已退出)退出时回收。
What determines the size of each of them?
创建线程时设置堆栈的大小。堆的大小是在应用程序启动时设置的,但是可以随需要的空间而增加(分配器从操作系统请求更多的内存)。
What makes one faster?
堆栈速度更快,因为访问模式使从中分配内存和取消分配内存变得微不足道(指针/整数只是递增或者递减),而堆的分配或者释放则涉及到更为复杂的簿记。另外,堆栈中的每个字节都倾向于非常频繁地重用,这意味着它倾向于被映射到处理器的高速缓存中,从而使其非常快。堆的另一个性能问题是,堆(通常是全局资源)通常必须是多线程安全的,即,通常每个分配和释放都必须与程序中"所有"其他堆访问同步。
清晰的演示:
图片来源:vikashazrati.wordpress.com
回答
我认为许多其他人在这个问题上给了我们大多数正确的答案。
但是,遗漏的一个细节是"堆"实际上应该称为"免费存储"。区别的原因是原始的免费存储区是通过称为"二项式堆"的数据结构实现的。因此,从早期实现的malloc()/ free()进行分配是从堆进行分配。但是,在当今的今天,大多数免费存储都是使用非常复杂的数据结构实现的,这些数据结构不是二项式堆。
回答
其他人直接回答了问题,但是当试图了解堆栈和堆时,我认为考虑传统UNIX进程(没有线程和基于mmap()的分配器)的内存布局会有所帮助。内存管理词汇表网页上有此内存布局的图表。
传统上,堆栈和堆位于进程的虚拟地址空间的相对两端。堆栈在访问时会自动增长,直至达到内核设置的大小(可以通过setrlimit(RLIMIT_STACK,...)
进行调整)。当内存分配器调用" brk()"或者" sbrk()"系统调用,将更多的物理内存页面映射到进程的虚拟地址空间时,堆就会增长。
在没有虚拟内存的系统(例如某些嵌入式系统)中,通常使用相同的基本布局,只是堆栈和堆的大小固定不变。但是,在其他嵌入式系统(例如基于Microchip PIC微控制器的系统)中,程序堆栈是单独的存储器块,数据移动指令无法对其进行寻址,并且只能通过程序流指令(调用返回等)。其他体系结构(例如Intel Itanium处理器)具有多个堆栈。从这个意义上讲,堆栈是CPU体系结构的组成部分。
回答
最重要的一点是,堆和堆栈是内存分配方式的通用术语。它们可以以许多不同的方式实现,并且这些术语适用于基本概念。
- 在一堆物品中,物品按照放置在另一个物品上的顺序排列在另一个物品的顶部,并且我们只能移除顶部的物品(不能将整个物品翻倒)。堆栈的简单性在于,我们无需维护一个表,该表包含分配的内存的每个部分的记录。我们唯一需要的状态信息是指向堆栈末尾的单个指针。要分配和取消分配,我们只需递增和递减该单个指针。注意:有时可以将堆栈实现为从内存部分的顶部开始,然后向下扩展而不是向上扩展。
- 在堆中,项的放置方式没有特定的顺序。因为没有清晰的"顶部"项目,所以我们可以按任意顺序进入和移除项目。堆分配需要完整记录分配的内存和未分配的内存,并进行一些开销维护以减少碎片,找到足够大以适合请求的大小的连续内存段,依此类推。可以随时释放内存以留下可用空间。有时,内存分配器将执行维护任务,例如通过移动分配的内存来对内存进行碎片整理或者垃圾回收-在运行时识别内存不再在作用域中并对其进行分配。
这些映像应该在描述堆栈和堆中分配和释放内存的两种方式方面做得相当好。好吃!
- 它们在多大程度上受操作系统或者语言运行时的控制?如前所述,堆和堆栈是通用术语,可以通过多种方式实现。计算机程序通常具有称为调用堆栈的堆栈,该堆栈存储与当前功能有关的信息,例如指向从哪个函数调用的指针以及任何局部变量。因为函数会先调用其他函数然后返回,所以堆栈会不断增长和缩小,以保存来自函数的信息,这些信息将在调用堆栈的更下方。一个程序实际上并没有对它的运行时控制。它由编程语言,操作系统甚至系统架构决定。堆是用于动态随机分配的任何内存的通用术语。即出现故障。内存通常由OS分配,应用程序调用API函数进行此分配。管理动态分配的内存需要一定的开销,通常由OS处理。
- 他们的范围是什么?调用栈是一个低级概念,从编程的意义上讲它与"作用域"无关。如果我们分解一些代码,则会看到指向堆栈部分的相对指针样式引用,但是就高级语言而言,该语言强加了自己的作用域规则。但是,堆栈的一个重要方面是,一旦函数返回,该函数本地的所有内容都会立即从堆栈中释放出来。鉴于编程语言是如何工作的,因此可以按照我们期望的方式工作。在堆中,也很难定义。范围是操作系统公开的任何内容,但是编程语言可能会添加有关其在应用程序中的"作用域"的规则。处理器体系结构和操作系统使用虚拟寻址,虚拟寻址将处理器转换为物理地址,并且存在页面错误等。它们跟踪哪些页面属于哪些应用程序。但是,我们根本不需要担心这一点,因为我们只需使用编程语言用来分配和释放内存的任何方法,并检查错误(如果分配/释放由于任何原因而失败)。
- 什么决定了它们的大小?同样,它取决于语言,编译器,操作系统和体系结构。堆栈通常是预先分配的,因为根据定义,它必须是连续的内存(有关最后一段的更多信息)。语言编译器或者操作系统确定其大小。我们不会在堆栈上存储大量数据,因此它将足够大,以至于永远不要完全使用它,除非发生不必要的无穷递归(因此,"堆栈溢出")或者其他异常编程决策。堆是可以动态分配的任何事物的通用术语。根据我们看待它的方式,它会不断变化的大小。在现代处理器和操作系统中,它的确切工作方式无论如何都是非常抽象的,因此我们通常不必担心它的内在工作方式,除非(在允许它的语言中)不得使用我们尚未分配或者已释放的内存。
- 是什么使速度更快?堆栈速度更快,因为所有可用内存始终是连续的。无需维护所有可用内存段的列表,只需一个指向堆栈当前顶部的指针即可。为此,编译器通常将此指针存储在特殊的快速寄存器中。更重要的是,堆栈上的后续操作通常集中在内存的非常近的区域内,这在非常低的级别上有利于通过处理器片上缓存进行优化。
回答
简单来说,堆栈是在其中创建局部变量的地方。同样,每次调用子例程时,程序计数器(指向下一条机器指令的指针)和任何重要的寄存器,有时参数都被压入堆栈。然后,子例程中的所有局部变量都被压入堆栈(并从那里使用)。子例程完成后,所有东西将从堆栈中弹出。 PC和注册数据将被获取并放回弹出位置,因此程序可以继续进行。
堆是进行动态内存分配的内存区域(显式"新"或者"分配"调用)。它是一种特殊的数据结构,可以跟踪大小不同的内存块及其分配状态。
在"经典"系统中,RAM的布局使得堆栈指针从内存的底部开始,堆指针从内存的顶部开始,并且它们相互靠近。如果它们重叠,则说明内存不足。但是,这不适用于现代的多线程OS。每个线程都必须具有自己的堆栈,并且可以动态创建这些堆栈。
回答
我们可以使用堆栈做一些有趣的事情。例如,我们具有诸如alloca之类的功能(假设我们可以绕过有关其用法的大量警告),这是一种malloc的形式,专门使用堆栈而不是堆作为内存。
也就是说,基于堆栈的内存错误是我遇到的最严重的错误。如果使用堆内存,并且超出了分配的块的边界,则触发分段错误的机会就很大。 (不是100%:块可能偶然与我们先前分配的另一个相邻。)但是,由于在堆栈上创建的变量始终彼此相邻,因此越界写入可以更改另一个变量的值。我了解到,每当我感到我的程序停止遵守逻辑规则时,它就有可能是缓冲区溢出。
回答
来自WikiAnwser。
堆
当一个函数或者方法调用另一个函数,依次调用另一个函数等时,所有这些函数的执行将保持挂起状态,直到最后一个函数返回其值为止。
挂起的函数调用链是堆栈,因为堆栈中的元素(函数调用)相互依赖。
在异常处理和线程执行中,堆栈是重要的考虑因素。
堆
堆只是程序用于存储变量的内存。
堆的元素(变量)彼此之间没有依赖性,并且始终可以随时随地进行随机访问。
回答
(我将此答案从另一个或者多或者少是这个问题的重复的问题上移开了。)
我们问题的答案是特定于实现的,并且可能会因编译器和处理器体系结构而异。但是,这是一个简化的说明。
- 堆栈和堆都是从底层操作系统分配的内存区域(通常是按需映射到物理内存的虚拟内存)。
- 在多线程环境中,每个线程将具有其自己的完全独立的堆栈,但它们将共享堆。并发访问必须在堆上进行控制,而在堆栈上则不可能。
堆
- 堆包含已用和可用块的链接列表。通过从一个空闲块中创建一个合适的块,可以满足堆上的新分配(通过" new"或者" malloc")。这需要更新堆上的块列表。有关堆上块的元信息通常也存储在堆上每个块前面的小区域中。
- 随着堆的增长,通常将新块从低地址分配到高地址。因此,我们可以将堆视为存储块的堆,随着分配的内存,存储块的大小会增加。如果堆对于分配而言太小,则通常可以通过从底层操作系统获取更多内存来增加大小。
- 分配和释放许多小块可能会使堆处于这样一种状态,即在已使用的块之间散布着许多小空闲块。分配大块的请求可能会失败,因为即使空闲块的组合大小可能足够大,也没有一个空闲块足以满足分配请求。这称为堆碎片。
- 当与空闲块相邻的已用块被重新分配时,新的空闲块可以与相邻的空闲块合并以创建更大的空闲块,从而有效地减少了堆的碎片。
堆栈
- 堆栈通常与CPU上一个名为堆栈指针的特殊寄存器紧密配合使用。最初,堆栈指针指向堆栈的顶部(堆栈中的最高地址)。
- CPU具有用于将值压入堆栈并将其从堆栈弹出的特殊指令。每次推送都会将值存储在堆栈指针的当前位置,并减少堆栈指针。 pop检索堆栈指针所指向的值,然后增加堆栈指针(不要因向堆栈中添加值会减少堆栈指针,而删除值会增加堆栈指针这一事实而混淆。底端)。存储和检索的值是CPU寄存器的值。
- 调用函数时,CPU使用特殊指令来推送当前指令指针,即在堆栈上执行的代码的地址。然后,CPU通过将指令指针设置为所调用函数的地址来跳转至该函数。稍后,当函数返回时,旧的指令指针会从堆栈中弹出,并在调用函数后立即在代码处恢复执行。
- 输入函数后,将减少堆栈指针以在堆栈上为本地(自动)变量分配更多空间。如果函数具有一个局部32位变量,则在堆栈上预留4个字节。函数返回时,将堆栈指针移回以释放分配的区域。
- 如果函数具有参数,则在调用函数之前将它们压入堆栈。然后,函数中的代码可以从当前堆栈指针向上浏览堆栈以找到这些值。
- 嵌套函数调用的工作方式就像一种魅力。每个新的调用将分配函数参数,局部变量的返回地址和空间,并且这些激活记录可以堆叠用于嵌套调用,并在函数返回时以正确的方式展开。
- 由于堆栈是有限的内存块,因此我们可能会通过调用过多的嵌套函数和/或者为局部变量分配过多的空间而导致堆栈溢出。通常,用于堆栈的存储区的设置方式是,在堆栈的底部(最低地址)以下进行写入将触发CPU中的陷阱或者异常。然后,运行时可以捕获这种异常情况,并将其转换为某种堆栈溢出异常。
Can a function be allocated on the heap instead of a stack?
否,函数(即本地或者自动变量)的激活记录分配在堆栈上,该记录不仅用于存储这些变量,还用于跟踪嵌套的函数调用。
堆的管理方式实际上取决于运行时环境。 C使用malloc
,而C ++使用new
,但是许多其他语言具有垃圾回收。
但是,堆栈是与处理器体系结构紧密相关的更底层的功能。在没有足够空间的情况下增长堆并不难,因为可以在处理堆的库调用中实现堆。但是,堆栈的增长通常是不可能的,因为只有在为时已晚时才发现堆栈溢出。并关闭执行线程是唯一可行的选择。