堆栈和堆是什么以及在哪里?

memory-management stack language-agnostic dynamic-memory-allocation heap-memory

什么是栈和堆?

它们在计算机内存中的物理位置?

它们在多大程度上受操作系统或语言运行时的控制?

他们的范围是什么?

是什么决定了它们的大小?

是什么让一个更快?

在这里可以找到一个非常好的解释What’s the difference between a stack and a heap?

也(真的)好:codeproject.com/Articles/76153/…(堆栈/堆部分)

youtube.com/watch?v=clOUdVDDzIM&spfreload=5

相关,参见Stack Clash。 Stack Clash 补救措施影响了系统变量和行为的某些方面,例如 rlimit_stack。另请参阅红帽 Issue 1463241

@mattshane 堆栈和堆的定义不依赖于值和引用类型。换句话说,即使值和引用类型从不存在,堆栈和堆也可以完全定义。此外,在理解值和引用类型时,堆栈只是一个实现细节。根据 Eric Lippert:The Stack Is An Implementation Detail, Part One

u
user1666620

堆栈是为执行线程预留的内存空间。调用函数时,堆栈顶部会保留一个块用于局部变量和一些簿记数据。当该函数返回时,该块将变为未使用,并且可以在下次调用函数时使用。堆栈始终以 LIFO(后进先出)顺序保留;最近保留的块总是下一个要释放的块。这使得跟踪堆栈变得非常简单;从堆栈中释放一个块只不过是调整一个指针。

堆是为动态分配预留的内存。与堆栈不同,堆中块的分配和释放没有强制模式;您可以随时分配块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分已分配或空闲变得更加复杂。有许多自定义堆分配器可用于针对不同的使用模式调整堆性能。

每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管有多个堆用于不同类型的分配并不少见)。

直接回答您的问题:

它们在多大程度上受操作系统或语言运行时的控制?

操作系统在创建线程时为每个系统级线程分配堆栈。通常,语言运行时调用操作系统来为应用程序分配堆。

他们的范围是什么?

堆栈附加到线程,因此当线程退出时,堆栈被回收。堆通常由运行时在应用程序启动时分配,并在应用程序(技术进程)退出时回收。

是什么决定了它们每个的大小?

堆栈的大小在创建线程时设置。堆的大小在应用程序启动时设置,但可以随着空间的需要而增长(分配器从操作系统请求更多内存)。

是什么让一个更快?

堆栈更快,因为访问模式使得从中分配和释放内存变得微不足道(指针/整数只是增加或减少),而堆在分配或释放中涉及更复杂的簿记。此外,堆栈中的每个字节往往被非常频繁地重用,这意味着它往往被映射到处理器的缓存,从而使其非常快。堆的另一个性能损失是堆,主要是全局资源,通常必须是多线程安全的,即每个分配和释放需要 - 通常 - 与程序中的“所有”其他堆访问同步。

https://i.stack.imgur.com/i6k0Z.png

好的答案 - 但我认为你应该补充一点,虽然堆栈是在进程启动时由操作系统分配的(假设存在操作系统),但它由程序内联维护。这也是堆栈更快的另一个原因 - 推送和弹出操作通常是一条机器指令,现代机器可以在一个周期内完成其中至少 3 条,而分配或释放堆涉及调用 OS 代码。

我真的被最后的图表弄糊涂了。我以为我明白了,直到我看到那张照片。

@Anarelle 处理器在有或没有操作系统的情况下运行指令。一个贴近我心的例子是 SNES,它没有 API 调用,没有我们今天所知的操作系统——但它有一个堆栈。在堆栈上分配是这些系统上的加法和减法,这对于通过从创建它们的函数返回而弹出时销毁的变量很好,但是将其与构造函数进行对比,其结果不能只是扔掉了。为此,我们需要堆,它与调用和返回无关。大多数操作系统都有一堆 API,没有理由自己做

“堆栈是留作暂存空间的内存”。凉爽的。但就 Java 内存结构而言,它实际上“搁置”在哪里?是堆内存/非堆内存/其他(Java 内存结构按照 betsol.com/2017/06/…

@JatinShashoo Java 运行时,作为字节码解释器,增加了一层虚拟化,所以你提到的只是 Java 应用程序的观点。从操作系统的角度来看,这只是一个堆,Java 运行时进程在其中分配一些空间作为已处理字节码的“非堆”内存。该操作系统级堆的其余部分用作存储对象数据的应用程序级堆。

R
Rob

堆:

像堆一样存储在计算机 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;

指针 pBuffer 和 b 的值位于堆栈上,并且很可能分配在函数的入口处。根据编译器的不同,缓冲区也可能在函数入口处分配。

一种常见的误解是,由 C99 语言标准(可在 open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf 获得)定义的 C 语言需要一个“堆栈”。事实上,“堆栈”这个词甚至没有出现在标准中。这回答 wrt/ 到 C 的堆栈使用的陈述通常是正确的,但语言绝不要求。请参阅 knosof.co.uk/cbook/cbook.html 了解更多信息,特别是如何在 en.wikipedia.org/wiki/Burroughs_large_systems 等奇数架构上实现 C

@Brian您应该解释为什么在堆栈上创建缓冲区[]和pBuffer指针以及为什么在堆上创建pBuffer的数据。我认为有些人可能会对您的回答感到困惑,因为他们可能认为程序专门指示在堆栈和堆上分配内存,但事实并非如此。是因为 Buffer 是值类型而 pBuffer 是引用类型吗?

“负责内存泄漏” - 堆不负责内存泄漏!懒惰/健忘/ex-java 编码员/不废话的编码员是!

此外,关于范围和分配的评论是错误的 - 范围根本没有连接到堆栈或堆。堆上的变量必须手动销毁,并且永远不会超出范围。不正确;更正确的说法是“当引用它们的变量超出范围时,不会释放堆上的数据。释放它们取决于您(或垃圾收集器)。

t
thomasrutter

最重要的一点是,堆和栈是内存分配方式的通用术语。它们可以以许多不同的方式实现,并且这些术语适用于基本概念。

在一堆物品中,物品按照它们放置在那里的顺序一个放在另一个上面,您只能移除最上面的一个(不会将整个东西翻倒)。堆栈的简单性在于您不需要维护一个包含每个已分配内存部分的记录的表;您需要的唯一状态信息是指向堆栈末尾的单个指针。要分配和取消分配,您只需增加和减少单个指针。注意:有时可以实现堆栈以从一段内存的顶部开始并向下扩展而不是向上增长。

在堆中,物品的放置方式没有特定的顺序。您可以按任何顺序进入和移除项目,因为没有明确的“顶部”项目。堆分配需要维护已分配和未分配内存的完整记录,以及一些开销维护以减少碎片,找到足够大以适应请求大小的连续内存段,等等。内存可以随时释放,留下可用空间。有时,内存分配器将执行维护任务,例如通过移动分配的内存或垃圾收集来对内存进行碎片整理 - 在运行时识别内存不再在范围内并释放它。

这些图像应该很好地描述了在堆栈和堆中分配和释放内存的两种方式。嗯!

它们在多大程度上受操作系统或语言运行时的控制?如前所述,堆和堆栈是通用术语,可以通过多种方式实现。计算机程序通常有一个称为调用堆栈的堆栈,它存储与当前函数相关的信息,例如指向它被调用的任何函数的指针,以及任何局部变量。因为函数调用其他函数然后返回,所以堆栈会增长和缩小以保存来自调用堆栈更下方的函数的信息。程序实际上并没有对其进行运行时控制。它由编程语言、操作系统甚至系统架构决定。堆是用于任何动态随机分配的内存的通用术语;即乱序。内存通常由操作系统分配,应用程序调用 API 函数来执行此分配。管理动态分配的内存需要相当多的开销,这通常由所使用的编程语言或环境的运行时代码处理。

他们的范围是什么?调用堆栈是一个如此低级的概念,它与编程意义上的“范围”无关。如果您反汇编一些代码,您将看到对堆栈部分的相对指针样式引用,但就更高级别的语言而言,该语言强加了自己的范围规则。然而,堆栈的一个重要方面是,一旦函数返回,该函数本地的任何内容都会立即从堆栈中释放。考虑到您的编程语言的工作方式,这将按照您期望的方式工作。在堆中,也很难定义。范围是操作系统公开的任何内容,但是您的编程语言可能会添加有关应用程序中“范围”是什么的规则。处理器架构和操作系统使用虚拟寻址,处理器将其转换为物理地址,并且存在页面错误等。它们跟踪哪些页面属于哪些应用程序。不过,您永远不需要担心这一点,因为您只需使用您的编程语言用来分配和释放内存的任何方法,并检查错误(如果分配/释放因任何原因失败)。

是什么决定了它们每个的大小?同样,它取决于语言、编译器、操作系统和架构。堆栈通常是预先分配的,因为根据定义它必须是连续的内存。语言编译器或操作系统决定了它的大小。您不会在堆栈上存储大量数据,因此它将足够大以至于永远不会完全使用它,除非在不需要的无限递归(因此,“堆栈溢出”)或其他不寻常的编程决策的情况下。堆是可以动态分配的任何东西的总称。根据你看待它的方式,它的大小会不断变化。在现代处理器和操作系统中,它的确切工作方式无论如何都是非常抽象的,所以你通常不需要太担心它是如何工作的,除了(在它允许你的语言中)你不能使用内存您尚未分配或已释放的内存。

是什么让一个更快?堆栈更快,因为所有空闲内存始终是连续的。不需要维护所有空闲内存段的列表,只需一个指向当前堆栈顶部的指针。为此,编译器通常将此指针存储在一个特殊的快速寄存器中。更重要的是,堆栈上的后续操作通常集中在非常接近的内存区域中,这在非常低的级别有利于处理器片上缓存的优化。

大卫 我不同意这是一个好的形象,或者“下推堆栈”是一个很好的术语来说明这个概念。当您将某些内容添加到堆栈时,堆栈的其他内容不会被下推,它们保持在原处。

这个答案包含一个很大的错误。静态变量不在堆栈上分配。请参阅我的回答 [链接] stackoverflow.com/a/13326916/1763801 进行说明。您将“自动”变量等同于“静态”变量,但它们完全不同

具体来说,您说“静态分配的局部变量”是在堆栈上分配的。实际上它们是在数据段中分配的。只有自动分配的变量(包括大多数但不是所有的局部变量,以及通过值而不是通过引用传递的函数参数)在堆栈上分配。

我刚刚意识到你是对的 - 在 C 中,静态分配是它自己独立的东西,而不是任何非动态的术语。我已经编辑了我的答案,谢谢。

不仅仅是 C。Java、Pascal、Python 和许多其他人都有静态分配、自动分配和动态分配的概念。说“静态分配”在任何地方都意味着同样的事情。在任何语言中,静态分配都不意味着“非动态”。您想要为您所描述的内容(即堆栈上的内容)使用术语“自动”分配。

M
Maggyero

(我已将这个答案从另一个或多或少是这个问题的欺骗的问题中移出。)

您的问题的答案是特定于实现的,并且可能因编译器和处理器架构而异。但是,这里有一个简化的解释。

堆栈和堆都是从底层操作系统分配的内存区域(通常是按需映射到物理内存的虚拟内存)。

在多线程环境中,每个线程都有自己完全独立的堆栈,但它们将共享堆。并发访问必须在堆上进行控制,而在堆栈上是不可能的。

堆包含已用块和空闲块的链表。通过从空闲块之一创建合适的块来满足堆上的新分配(通过 new 或 malloc)。这需要更新堆上的块列表。有关堆上块的元信息也存储在堆上,通常位于每个块前面的一小块区域中。

随着堆的增长,新块通常从低地址分配到高地址。因此,您可以将堆视为内存块的堆,随着内存的分配,其大小会增加。如果堆对于分配来说太小,通常可以通过从底层操作系统获取更多内存来增加大小。

分配和释放许多小块可能会使堆处于这样一种状态,即在已使用的块之间散布着许多小的空闲块。分配大块的请求可能会失败,因为没有一个空闲块大到足以满足分配请求,即使空闲块的组合大小可能足够大。这称为堆碎片。

当与空闲块相邻的已用块被释放时,新的空闲块可以与相邻的空闲块合并以创建更大的空闲块,从而有效地减少堆的碎片。

https://i.stack.imgur.com/0Obi0.png

堆栈

堆栈通常与 CPU 上名为堆栈指针的特殊寄存器密切配合工作。最初,堆栈指针指向堆栈的顶部(堆栈上的最高地址)。

CPU 有特殊的指令,用于将值压入堆栈并将它们从堆栈中弹出。每次推送都将值存储在堆栈指针的当前位置并减少堆栈指针。 pop 检索堆栈指针指向的值,然后增加堆栈指针(不要被以下事实混淆:向堆栈添加值会减少堆栈指针,而删除值会增加堆栈指针。记住堆栈增长到底部)。存储和检索的值是 CPU 寄存器的值。

如果函数有参数,则在调用该函数之前将这些参数压入堆栈。然后,函数中的代码能够从当前堆栈指针向上导航堆栈以定位这些值。

当调用函数时,CPU 使用特殊指令将当前指令指针压入堆栈,即堆栈上执行的代码的地址。 CPU 然后通过将指令指针设置为被调用函数的地址来跳转到该函数。稍后,当函数返回时,旧指令指针从堆栈中弹出,并在调用函数后的代码处继续执行。

进入函数时,堆栈指针会减小,以便在堆栈上为本地(自动)变量分配更多空间。如果函数有一个本地 32 位变量,则在堆栈上留出四个字节。当函数返回时,堆栈指针被移回以释放分配的区域。

嵌套函数调用就像一个魅力。每次新调用都会分配函数参数、局部变量的返回地址和空间,这些激活记录可以为嵌套调用堆叠,并在函数返回时以正确的方式展开。

由于堆栈是有限的内存块,调用太多嵌套函数和/或为局部变量分配太多空间可能会导致堆栈溢出。通常,用于堆栈的内存区域的设置方式是,在堆栈底部(最低地址)下方写入将触发 CPU 中的陷阱或异常。这种异常情况可以被运行时捕获并转换为某种堆栈溢出异常。

https://i.stack.imgur.com/9UshP.png

可以在堆而不是堆栈上分配函数吗?

不,函数(即本地或自动变量)的激活记录分配在堆栈上,该堆栈不仅用于存储这些变量,还用于跟踪嵌套函数调用。

如何管理堆实际上取决于运行时环境。 C 使用 malloc,C++ 使用 new,但许多其他语言都有垃圾收集。

但是,堆栈是与处理器架构密切相关的更底层的功能。当没有足够的空间时增加堆并不太难,因为它可以在处理堆的库调用中实现。然而,增加堆栈通常是不可能的,因为堆栈溢出只有在为时已晚时才被发现;关闭执行线程是唯一可行的选择。

@Martin - 一个非常好的答案/解释,而不是更抽象的接受答案。一个显示堆栈指针/寄存器用于函数调用的示例汇编程序将更具说明性。

每个引用类型都是值类型(int、string 等)的组合。据说,值类型存储在堆栈中,而不是当它们是引用类型的一部分时它是如何工作的。

这个答案在我看来是最好的,因为它帮助我理解了 return 语句的真正含义以及它与我不时遇到的这个“返回地址”的关系,将函数压入堆栈意味着什么,以及为什么将函数压入堆栈。很好的答案!

我认为这是最好的,即提到堆/堆栈是非常特定于实现的。其他答案假设了很多关于语言和环境/操作系统的事情。 +1

你是什么意思“然后函数中的代码能够从当前堆栈指针向上导航堆栈以找到这些值。” ?你能详细说明一下吗?

t
tagurit

在以下 C# 代码中

public void Method1()
{
    int i = 4;
    int y = 2;
    class1 cls1 = new class1();
}

这是内存的管理方式

https://i.stack.imgur.com/NS0k7.jpg

Local Variables 只需要在函数调用进入堆栈时持续。堆用于存放我们事先并不真正知道其生命周期但我们希望它们能持续一段时间的变量。在大多数语言中,如果我们想将变量存储在堆栈中,我们必须在编译时知道变量有多大。

对象(随着我们更新它们的大小会有所不同)在堆上,因为我们在创建时不知道它们会持续多久。在许多语言中,堆被垃圾收集以查找不再有任何引用的对象(例如 cls1 对象)。

在 Java 中,大多数对象直接进入堆。在 C / C++ 等语言中,当您不处理指针时,结构和类通常会保留在堆栈上。

更多信息可以在这里找到:

The difference between stack and heap memory allocation « timmurphy.org

和这里:

Creating Objects on the Stack and Heap

本文为上图来源:Six important .NET concepts: Stack, heap, value types, reference types, boxing, and unboxing - CodeProject

但请注意,它可能包含一些不准确之处。

这是不正确的。 i 和 cls 不是“静态”变量。它们被称为“本地”或“自动”变量。这是一个非常重要的区别。有关说明,请参阅 [链接] stackoverflow.com/a/13326916/1763801

我没有说它们是静态变量。我说 int 和 cls1 是静态项。它们的内存是静态分配的,因此它们进入堆栈。这与需要动态内存分配的对象形成对比,因此在堆上进行。

我引用“静态项目......继续堆栈”。这完全是错误的。静态项进入数据段,自动项进入堆栈。

写那篇代码项目文章的人也不知道他在说什么。例如,他说“原始人需要静态类型内存”这完全不正确。没有什么能阻止您在堆中动态分配基元,只需编写类似“int array[] = new int[num]”之类的东西,瞧,在 .NET 中动态分配的基元。这只是几个不准确之处之一。

@SnowCrash 关于您的图片的一个问题 - 分配 y 后如何访问 i?我必须弹出y吗?交换它们?如果有很多局部变量将它们分开怎么办?

T
Tom Leys

堆栈当您调用一个函数时,该函数的参数加上一些其他开销都放在堆栈上。一些信息(例如去哪里返回)也存储在那里。当您在函数中声明一个变量时,该变量也会在堆栈上分配。

释放堆栈非常简单,因为您总是以与分配相反的顺序释放。当您输入函数时会添加堆栈内容,退出函数时会删除相应的数据。这意味着您倾向于停留在堆栈的一个小区域内,除非您调用许多调用许多其他函数的函数(或创建递归解决方案)。

堆堆是放置动态创建的数据的通用名称。如果您不知道您的程序将创建多少艘飞船,您可能会使用 new(或 malloc 或等效的)运算符来创建每艘飞船。这种分配会持续一段时间,所以我们很可能会以不同于我们创建它们的顺序释放它们。

因此,堆要复杂得多,因为最终会出现未使用的内存区域与块交错 - 内存变得碎片化。找到所需大小的空闲内存是一个难题。这就是为什么应该避免使用堆的原因(尽管它仍然经常使用)。

堆栈和堆的实现通常取决于运行时/操作系统。通常,对性能至关重要的游戏和其他应用程序会创建自己的内存解决方案,这些解决方案会从堆中获取大量内存,然后在内部将其分发出去,以避免依赖操作系统来获取内存。

这仅在您的内存使用与标准完全不同的情况下才实用 - 即对于您在一个巨大的操作中加载一个关卡并且可以在另一个巨大的操作中丢弃全部内容的游戏。

内存中的物理位置 这并不像您想象的那么相关,因为一种称为 Virtual Memory 的技术使您的程序认为您可以访问物理数据位于其他位置的某个地址(即使在硬盘!)。随着调用树的深入,您获得的堆栈地址将按递增顺序排列。堆的地址是不可预测的(即特定于实现的)并且坦率地说并不重要。

避免使用堆的建议非常强烈。现代系统具有良好的堆管理器,并且现代动态语言广泛使用堆(程序员无需真正担心它)。我会说使用堆,但是使用手动分配器,不要忘记释放!

如果您可以使用堆栈或堆,请使用堆栈。如果不能使用堆栈,真的别无选择。我经常使用这两种方法,当然使用 std::vector 或类似的东西会命中堆。对于新手来说,你可以避免堆,因为堆栈就是这么简单!!

如果您的语言没有实现垃圾收集,智能指针(围绕指针的单独分配的对象,对动态分配的内存块进行引用计数)与垃圾收集密切相关,并且是一种在安全中管理堆的好方法并且无泄漏的方式。它们在各种框架中实现,但对于您自己的程序也并不难实现。

“这就是为什么应该避免使用堆的原因(尽管它仍然经常使用)。”我不确定这实际上意味着什么,尤其是在许多高级语言中以不同方式管理内存时。由于这个问题被标记为与语言无关,我想说这个特定的评论/行不合适且不适用。

好点@JonnoHampson - 虽然你提出了一个有效的观点,但我认为如果你使用 GC 使用“高级语言”,你可能根本不关心内存分配机制 - 所以不要甚至关心堆栈和堆是什么。

2
22 revs, 7 users 72%

其他答案只是避免解释静态分配的含义。因此,我将在下面解释三种主要的分配形式以及它们通常与堆、堆栈和数据段的关系。我还将展示一些 C/C++ 和 Python 中的示例,以帮助人们理解。

“静态”(AKA 静态分配)变量不在堆栈上分配。不要这么假设——很多人这样做只是因为“静态”听起来很像“堆栈”。它们实际上既不存在于堆栈中,也不存在于堆中。它们是所谓的 data segment 的一部分。

但是,通常最好考虑“范围”和“生命周期”而不是“堆栈”和“堆”。

范围是指代码的哪些部分可以访问变量。通常我们会考虑局部作用域(只能由当前函数访问)与全局作用域(可以在任何地方访问),尽管作用域会变得更加复杂。

生命周期是指在程序执行期间分配和释放变量的时间。通常我们会想到静态分配(变量将在程序的整个过程中持续存在,这对于在多个函数调用中存储相同的信息很有用)与自动分配(变量仅在对函数的单个调用期间持续存在,使其有用存储仅在函数期间使用并且一旦完成就可以丢弃的信息)与动态分配(其持续时间在运行时定义的变量,而不是像静态或自动的编译时间)。

尽管大多数编译器和解释器在使用堆栈、堆等方面类似地实现此行为,但只要行为正确,编译器有时可能会破坏这些约定。例如,由于优化,局部变量可能只存在于寄存器中或被完全删除,即使大多数局部变量存在于堆栈中。正如一些评论中所指出的,您可以自由地实现一个编译器,它甚至不使用堆栈或堆,而是使用其他一些存储机制(很少这样做,因为堆栈和堆非常适合此)。

我将提供一些简单的带注释的 C 代码来说明所有这些。最好的学习方法是在调试器下运行程序并观察其行为。如果您更喜欢阅读python,请跳到答案的末尾:)

// Statically allocated in the data segment when the program/DLL is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in the code
int someGlobalVariable;

// Statically allocated in the data segment when the program is first loaded
// Deallocated when the program/DLL exits
// scope - can be accessed from anywhere in this particular code file
static int someStaticVariable;

// "someArgument" is allocated on the stack each time MyFunction is called
// "someArgument" is deallocated when MyFunction returns
// scope - can be accessed only within MyFunction()
void MyFunction(int someArgument) {

    // Statically allocated in the data segment when the program is first loaded
    // Deallocated when the program/DLL exits
    // scope - can be accessed only within MyFunction()
    static int someLocalStaticVariable;

    // Allocated on the stack each time MyFunction is called
    // Deallocated when MyFunction returns
    // scope - can be accessed only within MyFunction()
    int someLocalVariable;

    // A *pointer* is allocated on the stack each time MyFunction is called
    // This pointer is deallocated when MyFunction returns
    // scope - the pointer can be accessed only within MyFunction()
    int* someDynamicVariable;

    // This line causes space for an integer to be allocated in the heap
    // when this line is executed. Note this is not at the beginning of
    // the call to MyFunction(), like the automatic variables
    // scope - only code within MyFunction() can access this space
    // *through this particular variable*.
    // However, if you pass the address somewhere else, that code
    // can access it too
    someDynamicVariable = new int;


    // This line deallocates the space for the integer in the heap.
    // If we did not write it, the memory would be "leaked".
    // Note a fundamental difference between the stack and heap
    // the heap must be managed. The stack is managed for us.
    delete someDynamicVariable;

    // In other cases, instead of deallocating this heap space you
    // might store the address somewhere more permanent to use later.
    // Some languages even take care of deallocation for you... but
    // always it needs to be taken care of at runtime by some mechanism.

    // When the function returns, someArgument, someLocalVariable
    // and the pointer someDynamicVariable are deallocated.
    // The space pointed to by someDynamicVariable was already
    // deallocated prior to returning.
    return;
}

// Note that someGlobalVariable, someStaticVariable and
// someLocalStaticVariable continue to exist, and are not
// deallocated until the program exits.

为什么区分生命周期和范围很重要的一个特别尖锐的例子是变量可以具有局部范围但静态生命周期 - 例如,上面代码示例中的“someLocalStaticVariable”。这样的变量会使我们常见但非正式的命名习惯变得非常混乱。例如,当我们说“本地”时,我们通常指的是“局部范围内自动分配的变量”,而当我们说全局时,我们通常指的是“全局范围内静态分配的变量”。不幸的是,当谈到“文件范围内的静态分配变量”之类的事情时,很多人只会说......“嗯???”。

C/C++ 中的一些语法选择加剧了这个问题——例如,许多人认为全局变量不是“静态的”,因为下面显示的语法。

int var1; // Has global scope and static allocation
static int var2; // Has file scope and static allocation

int main() {return 0;}

请注意,在上面的声明中放置关键字“static”会阻止 var2 具有全局范围。然而,全局 var1 具有静态分配。这不直观!出于这个原因,我在描述范围时尽量不要使用“静态”这个词,而是说“文件”或“文件受限”范围之类的东西。然而,许多人使用短语“静态”或“静态范围”来描述只能从一个代码文件访问的变量。在生命周期的上下文中,“静态”始终意味着变量在程序启动时分配并在程序退出时释放。

有些人认为这些概念是特定于 C/C++ 的。他们不是。例如,下面的 Python 示例说明了所有三种类型的分配(在解释语言中可能存在一些细微的差异,我不会在这里讨论)。

from datetime import datetime

class Animal:
    _FavoriteFood = 'Undefined' # _FavoriteFood is statically allocated

    def PetAnimal(self):
        curTime = datetime.time(datetime.now()) # curTime is automatically allocatedion
        print("Thank you for petting me. But it's " + str(curTime) + ", you should feed me. My favorite food is " + self._FavoriteFood)

class Cat(Animal):
    _FavoriteFood = 'tuna' # Note since we override, Cat class has its own statically allocated _FavoriteFood variable, different from Animal's

class Dog(Animal):
    _FavoriteFood = 'steak' # Likewise, the Dog class gets its own static variable. Important to note - this one static variable is shared among all instances of Dog, hence it is not dynamic!


if __name__ == "__main__":
    whiskers = Cat() # Dynamically allocated
    fido = Dog() # Dynamically allocated
    rinTinTin = Dog() # Dynamically allocated

    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

    Dog._FavoriteFood = 'milkbones'
    whiskers.PetAnimal()
    fido.PetAnimal()
    rinTinTin.PetAnimal()

# Output is:
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is steak
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is tuna
# Thank you for petting me. But it's 13:05:02.255000, you should feed me. My favorite food is milkbones
# Thank you for petting me. But it's 13:05:02.256000, you should feed me. My favorite food is milkbones

我将在函数中声明的静态变量称为仅具有本地可访问性,但通常不会使用术语“作用域”。此外,可能值得注意的是,语言的一个堆栈/堆方面基本上具有零灵活性:将执行上下文保存在堆栈上的语言不能使用相同的堆栈来保存需要比创建它们的上下文更长的东西.像 PostScript 这样的一些语言有多个堆栈,但有一个更像堆栈的“堆”。

@supercat 这一切都说得通。我将范围定义为“代码的哪些部分可以访问变量”(并且觉得这是最标准的定义)所以我认为我们同意:)

你一定在开玩笑。你真的可以在函数中定义静态变量吗?

@zaeemsatar 绝对,这在 C 代码中并不常见

@ZaeemSattar 将静态函数变量想象为隐藏的全局变量或私有静态成员变量。

P
Peter Mortensen

其他人已经很好地回答了粗略的笔画,所以我将提供一些细节。

堆栈和堆不必是单数。拥有多个堆栈的常见情况是进程中有多个线程。在这种情况下,每个线程都有自己的堆栈。您还可以拥有多个堆,例如,某些 DLL 配置可能会导致从不同堆分配不同的 DLL,这就是为什么释放由不同库分配的内存通常不是一个好主意的原因。在 C 中,您可以通过使用在堆栈上分配的 alloca 来获得可变长度分配的好处,而不是在堆上分配的 alloc。此内存不会在您的 return 语句中保留下来,但它对于暂存缓冲区很有用。在 Windows 上创建一个您不经常使用的巨大临时缓冲区并不是免费的。这是因为编译器会生成一个堆栈探测循环,每次输入函数时都会调用该循环以确保堆栈存在(因为 Windows 在堆栈末尾使用单个保护页来检测何时需要增加堆栈。如果您访问的内存超过堆栈末尾的一页,您将崩溃)。例子:

void myfunction()
{
   char big[10000000];
   // Do something that only uses for first 1K of big 99% of the time.
}

Re“与alloc相对”:您的意思是“与malloc相对”吗?

@PeterMortensen 这不是 POSIX,不能保证可移植性。

t
tagurit

其他人已经直接回答了您的问题,但是在尝试理解堆栈和堆时,我认为考虑传统 UNIX 进程的内存布局(没有线程和基于 mmap() 的分配器)是有帮助的。 Memory Management Glossary 网页包含此内存布局的图表。

堆栈和堆传统上位于进程的虚拟地址空间的两端。堆栈在访问时自动增长,达到内核设置的大小(可以使用 setrlimit(RLIMIT_STACK, ...) 调整)。当内存分配器调用 brk()sbrk() 系统调用时,堆会增长,将更多物理内存页面映射到进程的虚拟地址空间。

在没有虚拟内存的系统中,例如一些嵌入式系统,通常采用相同的基本布局,只是堆栈和堆的大小是固定的。但是,在其他嵌入式系统中(例如基于 Microchip PIC 单片机的系统),程序堆栈是一个单独的内存块,无法通过数据移动指令寻址,只能通过程序流指令(调用、返回等)。其他体系结构(例如 Intel Itanium 处理器)具有 multiple stacks。从这个意义上说,堆栈是 CPU 架构的一个元素。

C
Community

什么是堆栈?

堆栈是一堆对象,通常是整齐排列的对象。

https://i.stack.imgur.com/GTa97.jpg

计算架构中的堆栈是以后进先出的方式添加或删除数据的内存区域。在多线程应用程序中,每个线程都有自己的堆栈。

什么是堆?

堆是杂乱无章的东西堆积起来的不规则集合。

https://i.stack.imgur.com/rz43z.jpg

在计算架构中,堆是动态分配的内存区域,由操作系统或内存管理器库自动管理。在程序执行期间,堆上的内存会被定期分配、释放和调整大小,这可能会导致称为碎片的问题。当分配的内存对象之间的小空间太小而无法容纳额外的内存对象时,就会发生碎片。最终结果是不能用于进一步内存分配的堆空间百分比。

两者一起

在多线程应用程序中,每个线程都有自己的堆栈。但是,所有不同的线程将共享堆。因为在多线程应用程序中不同的线程共享堆,这也意味着线程之间必须进行一些协调,以便它们不会尝试访问和操作堆中的相同内存块同时。

哪个更快——堆栈还是堆?为什么?

堆栈比堆快得多。这是因为在堆栈上分配内存的方式。在堆栈上分配内存就像向上移动堆栈指针一样简单。

对于刚接触编程的人来说,使用堆栈可能是一个好主意,因为它更容易。因为堆栈很小,所以当您确切知道数据需要多少内存时,或者如果您知道数据的大小非常小,您会想要使用它。当您知道需要大量内存来存储数据时,最好使用堆,或者您只是不确定需要多少内存(例如使用动态数组)。

Java 内存模型

https://i.stack.imgur.com/yZK6t.png

堆栈是存储局部变量(包括方法参数)的内存区域。当涉及到对象变量时,这些仅仅是堆上实际对象的引用(指针)。每次实例化一个对象时,都会留出一块堆内存来保存该对象的数据(状态)。由于对象可以包含其他对象,因此其中一些数据实际上可以保存对这些嵌套对象的引用。

D
Daniel Papasian

堆栈是内存的一部分,可以通过几个关键的汇编语言指令进行操作,例如“pop”(从堆栈中删除并返回一个值)和“push”(将一个值推送到堆栈),但也可以调用 (调用子程序 - 这会将地址推送到堆栈中)并返回(从子程序返回 - 这会将地址从堆栈中弹出并跳转到它)。它是堆栈指针寄存器下方的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子程序,也用于在调用子程序之前保存寄存器中的值。

堆是由操作系统提供给应用程序的内存的一部分,通常通过诸如 malloc 之类的系统调用。在现代操作系统上,这个内存是一组只有调用进程才能访问的页面。

堆栈的大小是在运行时确定的,一般在程序启动后不会增长。在 C 程序中,堆栈需要足够大以容纳每个函数中声明的每个变量。堆将根据需要动态增长,但操作系统最终会进行调用(它通常会使堆增长超过 malloc 请求的值,因此至少一些未来的 malloc 不需要返回内核来获得更多内存。这种行为通常是可定制的)

因为您在启动程序之前已经分配了堆栈,所以在使用堆栈之前您永远不需要 malloc,所以这是一个轻微的优势。在实践中,很难预测在具有虚拟内存子系统的现代操作系统中什么会快什么会慢,因为页面是如何实现的以及它们存储在哪里是一个实现细节。

在这里还值得一提的是,英特尔对堆栈访问进行了大量优化,尤其是诸如预测从函数返回的位置之类的事情。

佚名

我想很多其他人在这个问题上给了你大部分正确的答案。

然而,一个被遗漏的细节是“堆”实际上应该被称为“自由存储”。这种区别的原因是最初的自由存储是用一种称为“二项式堆”的数据结构实现的。出于这个原因,从 malloc()/free() 的早期实现中分配是从堆中分配的。然而,在现代,大多数免费商店都是用非常复杂的数据结构实现的,这些数据结构不是二项式堆。

另一个挑剔 - 大多数答案(轻微地)暗示C语言需要使用“堆栈”。这是一个常见的误解,尽管它是(到目前为止)实现 C99 6.2.4 automatic storage duration objects(变量)的主导范式。事实上,“堆栈”这个词甚至没有出现在 C99 语言标准中:open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf

[@Heath] 我对你的回答有点意见。查看 this question 的已接受答案。它说 free store 最可能heap 相同,但不一定如此。

P
Peter

你可以用堆栈做一些有趣的事情。例如,您有像 alloca 之类的函数(假设您可以通过关于其使用的大量警告),这是一种 malloc 形式,专门使用堆栈而不是堆来存储内存。

也就是说,基于堆栈的内存错误是我经历过的最糟糕的一些错误。如果您使用堆内存,并且超出了分配块的范围,那么您很有可能触发段错误。 (不是 100%:您的块可能偶然与您之前分配的另一个块相邻。)但是由于在堆栈上创建的变量始终彼此相邻,因此越界写入可能会更改另一个变量的值。我了解到,每当我觉得我的程序不再遵守逻辑规律时,很可能是缓冲区溢出。

alloca 的便携性如何?例如,它可以在 Windows 上运行吗?它仅适用于类 Unix 操作系统吗?

T
T.E.D.

简单地说,堆栈是创建局部变量的地方。此外,每次调用子程序时,程序计数器(指向下一条机器指令的指针)和任何重要的寄存器,有时参数都会被压入堆栈。然后子例程内的任何局部变量都被压入堆栈(并从那里使用)。当子例程完成时,所有东西都会从堆栈中弹出。 PC 和寄存器数据在弹出时得到并放回原处,因此您的程序可以继续进行。

堆是内存动态内存分配的区域(显式“新”或“分配”调用)。它是一种特殊的数据结构,可以跟踪不同大小的内存块及其分配状态。

在“经典”系统中,RAM 的布局使得堆栈指针从内存底部开始,堆指针从顶部开始,它们彼此相向增长。如果它们重叠,则说明您的 RAM 不足。但是,这不适用于现代多线程操作系统。每个线程都必须有自己的堆栈,并且这些堆栈可以动态创建。

P
Peter Mortensen

来自 WikiAnwser。

当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数等时,所有这些函数的执行将保持挂起,直到最后一个函数返回其值。

这个挂起的函数调用链就是堆栈,因为堆栈中的元素(函数调用)相互依赖。

堆栈在异常处理和线程执行中很重要。

堆只是程序用来存储变量的内存。堆的元素(变量)相互之间没有依赖关系,可以随时随机访问。

P
Peter Mortensen

访问速度非常快

不必显式取消分配变量

空间由CPU高效管理,内存不会碎片化

仅局部变量

堆栈大小限制(取决于操作系统)

变量无法调整大小

变量可以全局访问

内存大小没有限制

(相对)较慢的访问

无法保证有效地使用空间,随着内存块的分配和释放,内存可能会随着时间的推移而变得碎片化

您必须管理内存(您负责分配和释放变量)

可以使用 realloc() 调整变量的大小

z
zgue

简而言之

堆栈用于静态内存分配和堆用于动态内存分配,两者都存储在计算机的 RAM 中。

详细地

堆栈

堆栈是一种“LIFO”(后进先出)数据结构,由 CPU 密切管理和优化。每次函数声明一个新变量时,它都会被“推入”堆栈。然后每次函数退出时,该函数压入堆栈的所有变量都被释放(也就是说,它们被删除)。释放堆栈变量后,该内存区域可用于其他堆栈变量。

使用堆栈存储变量的优点是内存是为您管理的。您不必手动分配内存,也不必在不再需要时释放它。更重要的是,由于 CPU 如此高效地组织堆栈内存,读取和写入堆栈变量非常快。

here 可以找到更多信息。

堆是计算机内存中的一个区域,它不会自动为您管理,也不是由 CPU 严格管理。它是一个更自由浮动的内存区域(并且更大)。要在堆上分配内存,您必须使用 malloc() 或 calloc(),它们是 C 的内置函数。一旦你在堆上分配了内存,你有责任在你不再需要它时使用 free() 来释放该内存。

如果你不这样做,你的程序就会发生所谓的内存泄漏。也就是说,堆上的内存仍将被留出(并且不会对其他进程可用)。正如我们将在调试部分看到的,有一个名为 Valgrind 的工具可以帮助您检测内存泄漏。

与堆栈不同,堆对可变大小没有大小限制(除了计算机明显的物理限制)。堆内存的读取和写入速度稍慢,因为必须使用指针来访问堆上的内存。我们将很快讨论指针。

与堆栈不同,在堆上创建的变量可以被程序中任何位置的任何函数访问。堆变量在范围内本质上是全局的。

here 可以找到更多信息。

分配在栈上的变量直接存储到内存中,访问这块内存非常快,它的分配是在程序编译的时候处理的。当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数等时,所有这些函数的执行将保持挂起,直到最后一个函数返回其值。堆栈始终以 LIFO 顺序保留,最近保留的块始终是下一个要释放的块。这使得跟踪堆栈变得非常简单,从堆栈中释放一个块只不过是调整一个指针。

在堆上分配的变量在运行时分配它们的内存,访问这块内存有点慢,但堆大小仅受虚拟内存大小的限制。堆的元素彼此之间没有依赖关系,并且总是可以随时随机访问。您可以随时分配块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分已分配或空闲变得更加复杂。

https://i.stack.imgur.com/KdBPf.png

如果您确切知道在编译时需要分配多少数据,并且不会太大,则可以使用堆栈。如果您不知道运行时需要多少数据或者需要分配大量数据,则可以使用堆。

在多线程情况下,每个线程都有自己完全独立的堆栈,但它们将共享堆。堆栈是特定于线程的,而堆是特定于应用程序的。堆栈在异常处理和线程执行中很重要。

每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管有多个堆用于不同类型的分配并不少见)。

https://i.stack.imgur.com/J0teH.gif

在运行时,如果应用程序需要更多的堆,它可以从空闲内存中分配内存,如果堆栈需要内存,它可以从为应用程序分配的空闲内存中分配内存。

甚至,herehere 给出了更多细节。

现在来回答你的问题。

它们在多大程度上受操作系统或语言运行时的控制?

操作系统在创建线程时为每个系统级线程分配堆栈。通常,语言运行时调用操作系统来为应用程序分配堆。

here 可以找到更多信息。

他们的范围是什么?

已经在上面给出了。

“如果您确切知道在编译时需要分配多少数据,并且它不是太大,您可以使用堆栈。如果您不知道运行时需要多少数据或者如果您不知道在运行时需要多少数据,您可以使用堆。你需要分配大量数据。”

更多信息可在 here 中找到。

是什么决定了它们每个的大小?

创建线程时,堆栈的大小由 OS 设置。堆的大小在应用程序启动时设置,但它可以随着空间的需要而增长(分配器从操作系统请求更多内存)。

是什么让一个更快?

堆栈分配要快得多,因为它真正所做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得相当的性能,但这会稍微增加复杂性并带来一些麻烦。

此外,堆栈与堆不仅是性能考虑因素;它还告诉你很多关于对象的预期生命周期的信息。

详情请参阅 here

A
Alireza

好的,简而言之,它们的意思是订购和未订购......!

堆栈:在堆栈项目中,事物相互叠加,意味着处理速度更快,效率更高!...

所以总是有一个索引来指向特定的项目,处理也会更快,项目之间也有关系!...

堆:没有顺序,处理速度会变慢,值会乱七八糟,没有特定的顺序或索引……有随机性,它们之间没有关系……所以执行和使用时间可能会有所不同……

我还创建了下面的图像以显示它们的外观:

https://i.stack.imgur.com/9c2VH.png

Y
Yousha Aleayoub

每个进程在虚拟内存中的栈、堆和数据:

https://bayanbox.ir/view/581244719208138556/virtual-memory.jpg

j
jlettvin

在 1980 年代,UNIX 像兔子一样传播开来,大公司纷纷推出自己的产品。埃克森美孚拥有一个,数十个品牌名称已被历史遗忘。内存的布局方式由许多实现者自行决定。

一个典型的 C 程序在内存中被平放,并有机会通过更改 brk() 值来增加。通常,HEAP 刚好低于这个 brk 值,增加 brk 会增加可用堆的数量。

单个堆栈通常是 HEAP 下方的区域,它是一段内存,在下一个固定内存块的顶部之前不包含任何价值。下一个块通常是 CODE,它可以被当时著名的黑客攻击中的堆栈数据覆盖。

一个典型的内存块是 BSS(零值块),它在一家制造商的产品中意外地没有归零。另一个是包含初始化值的 DATA,包括字符串和数字。第三种是包含 CRT(C 运行时)、main、函数和库的 CODE。

UNIX 中虚拟内存的出现改变了许多限制。没有客观的理由为什么这些块需要是连续的、大小固定的,或者现在以特定的方式排序。当然,在 UNIX 之前是不受这些限制的 Multics。这是一个示意图,显示了那个时代的一种内存布局。

https://i.stack.imgur.com/eeefv.png

P
Peter Mortensen

几分钱:我认为,绘制内存图形会更好,更简单:

https://i.stack.imgur.com/nlb7A.jpg

箭头 - 显示增长堆栈和堆的位置,进程堆栈大小有限制,在操作系统中定义,线程堆栈大小限制通常由线程创建 API 中的参数。堆通常受进程最大虚拟内存大小的限制,例如 32 位 2-4 GB。

如此简单的方法:进程堆一般用于进程和内部的所有线程,在常见情况下用于内存分配,例如 malloc()。

堆栈是用于存储常见情况下函数返回指针和变量的快速存储器,在函数调用中作为参数处理,局部函数变量。

C
Community

由于有些答案很挑剔,我将贡献我的一点。

令人惊讶的是,没有人提到不仅在外来语言 (PostScript) 或平台 (Intel Itanium) 中,而且在 fibers 中都可以找到多个(即与运行的操作系统级线程的数量无关)调用堆栈, green threadscoroutines 的一些实现。

纤维、绿色线程和协程在很多方面都很相似,这会导致很多混乱。光纤和绿色线程之间的区别在于,前者使用协作多任务,而后者可能具有协作或抢占式(甚至两者兼而有之)。关于纤程和协程之间的区别,请参见here

在任何情况下,纤程、绿色线程和协程的目的都是让多个函数同时执行,但不是在单个操作系统级线程中并行执行(区别参见this SO question),传输以有组织的方式相互控制。

当使用纤程、绿色线程或协程时,通常每个函数都有一个单独的堆栈。 (从技术上讲,每个函数不仅是堆栈,而且整个执行上下文都是每个函数。最重要的是,CPU 寄存器。)对于每个线程,堆栈的数量与并发运行的函数一样多,并且线程在执行每个函数之间切换根据您的程序的逻辑。当一个函数运行到它的末尾时,它的堆栈被销毁。因此,堆栈的数量和生命周期是动态的,而不是由操作系统级线程的数量决定的!

请注意,我说过“通常每个函数都有一个单独的堆栈”。 couroutines 有 stackfulstackless 两种实现。最值得注意的堆栈式 C++ 实现是 Boost.CoroutineMicrosoft PPLasync/await。 (然而,在 C++17 中提出的 C++ 的 resumable functions(又名“asyncawait”)很可能使用无堆栈协程。)

Fibers 对 C++ 标准库的提议即将发布。此外,还有一些第三方libraries。绿色线程在 Python 和 Ruby 等语言中非常流行。

P
Pang

我有一些东西要分享,虽然要点已经讲过了。

访问速度非常快。

存储在 RAM 中。

函数调用与传递的局部变量和函数参数一起加载。

当程序超出范围时,空间会自动释放。

存储在顺序内存中。

相对于 Stack 的访问速度较慢。

存储在 RAM 中。

动态创建的变量存储在这里,稍后需要在使用后释放分配的内存。

存储在内存分配完成的任何地方,始终由指针访问。

有趣的注释:

如果函数调用存储在堆中,则会导致 2 个混乱点:由于堆栈中的顺序存储,执行速度更快。堆中存储会导致大量时间消耗,从而使整个程序执行速度变慢。如果函数存储在堆中(指针指向的杂乱存储),则无法返回调用者地址(由于内存中的顺序存储,堆栈给出)。

由于堆栈中的顺序存储,执行速度更快。堆中存储会导致大量时间消耗,从而使整个程序执行速度变慢。

如果函数存储在堆中(指针指向的杂乱存储),则无法返回调用者地址(由于内存中的顺序存储,堆栈给出)。

简洁干净。好的:)

a
ar18

哇!这么多答案,我不认为其中一个是正确的......

1)它们在哪里,是什么(物理上在真实计算机的内存中)?

堆栈是从分配给程序映像的最高内存地址开始的内存,然后从那里减少值。它保留给被调用的函数参数和函数中使用的所有临时变量。

有两个堆:公共的和私有的。

私有堆在程序中最后一个代码字节之后的 16 字节边界(对于 64 位程序)或 8 字节边界(对于 32 位程序)开始,然后从那里增加值。它也称为默认堆。

如果私有堆太大,它将与堆栈区域重叠,如果堆栈太大,堆栈也会与堆重叠。因为堆栈从较高的地址开始并向下工作到较低的地址,通过适当的黑客攻击,您可以使堆栈变得如此之大,以至于它会超出私有堆区域并与代码区域重叠。诀窍是重叠足够多的代码区域,以便您可以挂钩到代码中。这样做有点棘手,并且您可能会面临程序崩溃的风险,但它很容易且非常有效。

公共堆驻留在程序映像空间之外的自己的内存空间中。如果内存资源变得稀缺,正是这些内存将被吸走到硬盘上。

2)它们在多大程度上受操作系统或语言运行时的控制?

堆栈由程序员控制,私有堆由操作系统管理,公共堆不受任何人控制,因为它是一种操作系统服务——你提出请求,要么被批准,要么被拒绝。

2b) 他们的范围是什么?

它们对程序都是全局的,但它们的内容可以是私有的、公共的或全局的。

2c) 是什么决定了它们的大小?

堆栈和私有堆的大小由编译器运行时选项决定。公共堆在运行时使用大小参数进行初始化。

2d) 是什么让一个更快?

它们不是为了快速而设计的,而是为了有用而设计的。程序员如何利用它们决定了它们是“快”还是“慢”

参考:

https://norasandler.com/2019/02/18/Write-a-Compiler-10.html

https://docs.microsoft.com/en-us/windows/desktop/api/heapapi/nf-heapapi-getprocessheap

https://docs.microsoft.com/en-us/windows/desktop/api/heapapi/nf-heapapi-heapcreate

i
ingconti

许多答案作为概念是正确的,但我们必须注意,硬件(即微处理器)需要一个堆栈来允许调用子例程(汇编语言中的 CALL..)。 (OOP 家伙会称之为方法)

在堆栈上保存返回地址并调用 → push / ret → pop 直接在硬件中进行管理。

您可以使用堆栈来传递参数..即使它比使用寄存器慢(微处理器专家会说还是一本好的 1980 年代 BIOS 书......)

没有堆栈,任何微处理器都无法工作。 (我们无法想象没有子程序/函数的程序,即使是汇编语言)

没有堆它可以。 (汇编语言程序可以在没有的情况下工作,因为堆是一个操作系统概念,而 malloc 是一个 OS/Lib 调用。

堆栈使用速度更快,因为:

是硬件,甚至推/弹出都非常有效。

malloc 需要进入内核模式,使用锁/信号量(或其他同步原语)执行一些代码并管理一些跟踪分配所需的结构。

什么是OPP?你是说 OOP (object-oriented_programming) 吗?

您的意思是说 malloc 是内核调用吗?

1) 是的,对不起.. OOP... 2) malloc:我写得很快,对不起... malloc 在用户空间中.. 但可以触发其他调用.. 关键是使用堆可能非常慢...

“许多答案作为概念是正确的,但我们必须注意,硬件(即微处理器)需要一个堆栈来允许调用子例程(汇编语言中的 CALL..)”。您混淆了 CPU 堆栈(如果现代 CPU 中有一个)和语言运行时堆栈(每个线程一个)。当程序员谈论堆栈时,这是运行时的线程执行堆栈,例如 NET 线程堆栈),我们不是在谈论 CPU 堆栈。

“NET 线程”不是真正的堆栈。 (JVM 也一样):它们是 SW 概念。 (其他人称之为“激活记录”)我们必须从PC历史上的真实电路开始,以获得真正的理解。

A
A. Hendry

它们在哪里,是什么(物理上在真实计算机的内存中)?

回答: 两者都在 RAM 中。

在旁边:

RAM 就像一张桌子,而 HDD/SSD(永久存储)就像书架。要阅读任何东西,您必须在办公桌上打开一本书,并且您只能在办公桌上打开尽可能多的书。要拿到一本书,你可以从书架上把它拿出来,然后在你的桌子上打开。要归还书,您需要合上办公桌上的书并将其放回书架。

堆栈和堆是我们给编译器将不同类型的数据存储在同一位置(即 RAM 中)的两种方式的名称。

他们的范围是什么?是什么决定了它们每个的大小?是什么让一个更快?

回答:

堆栈用于静态(固定大小)数据 a。在编译时,编译器会读取代码中使用的变量类型。一世。它为这些变量分配固定数量的内存。 ii.这个内存的大小不能增长。湾。内存是连续的(单个块),因此访问有时比堆 c 快。放置在堆栈上的对象在运行时在内存中增长超过堆栈大小会导致堆栈溢出错误堆用于动态(更改大小)数据 a。内存量仅受 RAM i 中可用空间量的限制。使用的数量可以在运行时根据需要增加或减少 b.由于项目是通过在 RAM 中的任何位置找到空白空间来在堆上分配的,因此数据并不总是在连续的部分中,这有时会使访问比堆栈慢 c。程序员使用 new 关键字手动将项目放在堆上,并且必须在使用完毕后手动释放此内存。一世。重复分配新内存而不在不再需要时释放它的代码会导致内存泄漏。

在旁边:

堆栈和堆的引入主要不是为了提高速度;引入它们是为了处理内存溢出。关于使用堆栈与堆的第一个问题应该是是否会发生内存溢出。如果一个对象打算增长到未知数量(如链表或成员可以保存任意数量数据的对象),请将其放在堆上。尽可能使用 C++ 标准库 (STL) 容器向量、映射和列表,因为它们具有内存和速度效率,并且添加到使您的生活更轻松(您无需担心内存分配/释放)。

在让你的代码运行之后,如果你发现它运行的慢得令人无法接受,那么回去重构你的代码,看看它是否可以更有效地编程。结果可能证明问题与堆栈或堆完全无关(例如,使用迭代算法而不是递归算法,查看 I/O 与 CPU 密集型任务,也许添加多线程或多处理)。

我在上面说有时更慢/更快,因为程序的速度可能与在堆栈或堆上分配的项目无关。

它们在多大程度上受操作系统或语言运行时的控制?

回答:

堆栈大小由编译器在编译时确定。

堆大小在运行时变化。 (堆在运行时与操作系统一起工作以分配内存。)

在旁边:

下面是关于控制和编译时与运行时操作的更多信息。

每台计算机都有一个独特的指令集架构(ISA),即它的硬件命令(例如“MOVE”、“JUMP”、“ADD”等)。

操作系统只不过是一个资源管理器(控制如何/何时/在何处使用内存、处理器、设备和信息)。

操作系统的ISA称为裸机,其余命令称为扩展机。内核是扩展机的第一层。它控制诸如确定哪些任务可以使用处理器(调度程序)、为任务分配多少内存或多少硬件寄存器(调度程序)以及应执行任务的顺序(流量控制器)等事情。

确定哪些任务可以使用处理器(调度程序),

分配给任务(调度程序)的内存或硬件寄存器数量,以及

应该执行任务的顺序(流量控制器)。

当我们说“编译器”时,我们一般指的是编译器、汇编器和链接器。到链接器 链接器获取所有机器代码(可能从多个源文件生成)并将其组合到一个程序中。

编译器将源代码转换为汇编语言并将其传递给汇编器,

汇编器将汇编语言转换为机器代码(ISA 命令),并将其传递给链接器

链接器获取所有机器代码(可能从多个源文件生成)并将其组合到一个程序中。

机器代码在执行时被传递给内核,它决定何时运行并获得控制权,但机器代码本身包含用于请求文件、请求内存等的 ISA 命令。所以代码发出 ISA 命令,但一切都必须通过由内核。

“程序员使用 new 关键字手动将项目放入堆栈”,我相信您的意思是堆而不是堆栈。

谢谢,是的,这是我的错误(对不起)。我纠正了它。感谢指出错误!

n
nCardot

堆栈本质上是一个易于访问的内存,它只是将其项目作为一个 - 好 - 堆栈来管理。只有事先知道其大小的项目才能进入堆栈。数字、字符串、布尔值就是这种情况。堆是您无法预先确定确切大小和结构的项目的内存。由于对象和数组可以在运行时变异和更改,因此它们必须进入堆。

来源:Academind

佚名

CPU 堆栈和堆在物理上与 CPU 和寄存器如何与内存一起工作,机器汇编语言如何工作,而不是高级语言本身有关,即使这些语言可以决定一些小事情。

所有现代 CPU 都使用“相同”的微处理器理论:它们都基于所谓的“寄存器”,有些用于“堆栈”以获得性能。从一开始,所有 CPU 都有堆栈寄存器,而且它们一直都在这里,就像我所知的那样。汇编语言从一开始就相同,尽管存在差异……直到 Microsoft 及其中间语言 (IL) 改变了范式以拥有 OO 虚拟机汇编语言。所以将来我们将能够拥有一些 CLI/CIL CPU(MS 的一个项目)。

CPU 具有堆栈寄存器来加速内存访问,但与使用其他寄存器来完全访问进程的所有可用内存相比,它们受到限制。这就是我们谈论堆栈和堆分配的原因。

总之,一般来说,堆是巨大而缓慢的,用于“全局”实例和对象内容,因为堆栈小而快,并且用于“局部”变量和引用(隐藏指针以忘记管理它们)。

因此,当我们在方法中使用 new 关键字时,引用(一个 int)是在堆栈中创建的,但如果我记得的话,对象及其所有内容(值类型以及对象)都是在堆中创建的。但是本地基本值类型和数组是在堆栈中创建的。

内存访问的区别在于单元引用级别:寻址堆,即进程的整体内存,在处理 CPU 寄存器方面需要更多的复杂性,而不是在寻址方面“更多”本地的堆栈,因为 CPU 堆栈如果我记得的话,寄存器被用作基地址。

这就是为什么当我们有很长或无限的递归调用或循环时,我们很快就会发生堆栈溢出,而不会冻结现代计算机上的系统......

C# Heap(ing) Vs Stack(ing) In .NET

Stack vs Heap: Know the Difference

Static class memory allocation where it is stored C#

What and where are the stack and heap?

https://en.wikipedia.org/wiki/Memory_management

https://en.wikipedia.org/wiki/Stack_register

汇编语言资源:

Assembly Programming Tutorial

Intel® 64 and IA-32 Architectures Software Developer Manuals

a
aquagremlin

感谢您进行了非常好的讨论,但作为一个真正的菜鸟,我想知道说明保存在哪里?在开始时,科学家们在两种架构之间做出选择(冯诺伊曼认为一切都被认为是数据,而哈佛则为指令保留一个内存区域,另一个用于数据)。最终,我们采用了冯诺依曼设计,现在一切都被认为是“相同的”。这让我在学习汇编 https://www.cs.virginia.edu/~evans/cs216/guides/x86.html 时感到很困难,因为他们谈论的是寄存器和堆栈指针。

以上所有内容都在谈论数据。我的猜测是,由于指令是具有特定内存占用的已定义事物,因此它将进入堆栈,因此汇编中讨论的所有“那些”寄存器都在堆栈上。当然,随后出现了面向对象编程,指令和数据混合到一个动态的结构中,所以现在指令也将保留在堆上?

AFAIK,仍然存在许多采用哈佛架构的 CPU(通常是优化的 DSP),它们具有用于指令和数据的独立内存(和总线)。 (其中一些甚至超过 1 个数据存储器)。这是为了优化周期持续时间(指令提取、数据提取和前一条指令的执行都在一个机器周期内),从而利用他们所谓的流水线。我不知道,指令应该以任何方式放在堆栈中。无论如何,我有一种偷偷摸摸的感觉,这个答案可能超出了原始问题的范围......

a
anshkun

创建进程时,在加载代码和数据之后,操作系统设置堆在数据结束后开始,并根据体系结构堆栈到地址空间的顶部

当需要更多堆时,操作系统将动态分配并且堆块总是几乎连续的

请参阅 linux 中的 brk()sbrk()alloca() 系统调用