栈 stack 和堆 heap 是什么以及在内存中他们具体是在哪里?

  • 什么是堆栈和堆?
  • 它们在计算机内存中的物理位置是什么?
  • 它们在多大程度上受操作系统或语言运行时的控制?
  • 它们的范围是什么?
  • 是什么决定了它们的大小?
  • 是什么让人更快?
1807364 次浏览

堆栈:

  • 存储在计算机RAM中,就像堆一样。
  • 在堆栈上创建的变量将超出范围并自动释放。
  • 与堆上的变量相比,分配要快得多。
  • 使用实际的堆栈数据结构实现。
  • 存储本地数据,返回地址,用于参数传递。
  • 当使用太多堆栈时(主要来自无限或太深的递归,非常大的分配)可能会出现堆栈溢出。
  • 在堆栈上创建的数据可以在没有指针的情况下使用。
  • 如果您确切地知道在编译时之前需要分配多少数据并且它不太大,您将使用堆栈。
  • 通常在程序启动时已经确定了最大大小。

堆:

  • 存储在计算机RAM中,就像堆栈一样。
  • 在C++,堆上的变量必须手动销毁,并且永远不会超出范围。使用deletedelete[]free释放数据。
  • 与堆栈上的变量相比,分配速度较慢。
  • 按需使用以分配数据块供程序使用。
  • 当有很多分配和释放时,可能会出现碎片。
  • 在C++或C中,在堆上创建的数据将由指针指向,并分别分配newmalloc
  • 如果请求分配的缓冲区太大,可能会出现分配失败。
  • 如果您不确切知道在运行时需要多少数据,或者您需要分配大量数据,您将使用堆。
  • 负责内存泄漏。

示例:

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 stackchar buffer[500];
//Create 500 bytes on the heappBuffer = new char[500];
}//<-- buffer is deallocated here, pBuffer is not}//<--- oops there's a memory leak, I should have called delete[] pBuffer;

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

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

《The Heap》堆是一个通用名称,用于存放您在飞行中创建的数据。如果您不知道您的程序将创建多少艘宇宙飞船,您可能会使用new(或malloc或等效)运算符来创建每艘宇宙飞船。这种分配将持续一段时间,因此我们很可能会以与创建它们不同的顺序释放东西。

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

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

只有当你的内存使用与常规完全不同时,这才是实用的-例如,对于游戏,你在一个巨大的操作中加载一个级别,并且可以在另一个巨大的操作中丢弃整个数量。

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

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

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

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

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

其他人已经很好地回答了广泛的笔触,所以我将投入一些细节。

  1. 堆栈和堆不必是单一的。拥有多个堆栈的常见情况是进程中有多个线程。在这种情况下,每个线程都有自己的堆栈。您也可以拥有多个堆,例如,某些DLL配置可能导致从不同的堆分配不同的DLL,这就是为什么释放由不同库分配的内存通常是个坏主意。

  2. 在C中,您可以通过使用alloca来获得可变长度分配的好处,它在堆栈上分配,而不是alloc,它在堆上分配。

  3. 在Windows上制作一个你不怎么使用的巨大临时缓冲区并不是免费的。这是因为编译器会生成一个堆栈探测循环,每次输入你的函数时都会调用该循环以确保堆栈存在(因为Windows在堆栈末尾使用一个保护页来检测何时需要增加堆栈。如果你在堆栈末尾访问多个页面的内存,你就会崩溃)。示例:

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

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

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

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

直接回答您的问题:

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

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

它们的范围是什么?

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

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

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

是什么让人更快?

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

一个清晰的演示:
图片来源:vikashazrati.wordpress.com

我想很多人在这个问题上给你的答案大多是正确的。

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

其他人直接回答了你的问题,但是在试图理解堆栈和堆时,我认为考虑传统UNIX进程的内存布局是有帮助的(没有线程和基于mmap()的分配器)。内存管理术语表网页有一个这种内存布局的图表。

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

在没有虚拟内存的系统中,例如一些嵌入式系统,通常适用相同的基本布局,只是堆栈和堆的大小是固定的。然而,在其他嵌入式系统(例如基于Microchip PIC微控制器的系统)中,程序堆栈是一个单独的内存块,不能通过数据移动指令寻址,只能通过程序流指令(调用、返回等)间接修改或读取。其他架构,例如Intel Itanium处理器,有多个堆栈。从这个意义上说,堆栈是CPU架构的一个元素。

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

  • 在一堆物品中,物品按照它们放置在那里的顺序一个放在另一个的顶部,你只能移除顶部的一个(而不会推翻整个东西)。

    像一叠纸一样堆叠

    堆栈的简单之处在于您不需要维护包含已分配内存每个部分记录的表;您唯一需要的状态信息是指向堆栈末尾的单个指针。要分配和取消分配,您只需递增和递减该单个指针。注意:堆栈有时可以实现为从内存部分的顶部开始并向下扩展而不是向上扩展。

  • 在堆中,项目的放置方式没有特定的顺序。您可以按任何顺序进入并删除项目,因为没有明确的“顶部”项目。

    像一堆甘草一样堆

    堆分配需要维护已分配内存和未分配内存的完整记录,以及一些开销维护以减少碎片,找到足够大的连续内存段以适应请求的大小,等等。内存可以随时释放,留下可用空间。有时内存分配器将执行维护任务,例如通过移动分配的内存来整理内存碎片,或垃圾收集-在运行时识别内存何时不再在范围内并释放它。

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

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

    如上所述,堆和栈是通用术语,可以有多种实现方式。计算机程序通常有一个名为调用堆栈的栈,用于存储与当前函数相关的信息,例如指向调用它的任何函数的指针,以及任何局部变量。因为函数调用其他函数然后返回,所以栈会变大和缩小,以保存来自调用栈下函数的信息。程序并不能真正对其进行运行时控制;它由编程语言、操作系统甚至系统架构决定。

    堆是一个通用术语,用于动态和随机分配的任何内存;即无序。内存通常由操作系统分配,应用程序调用API函数来执行此分配。管理动态分配的内存需要相当多的开销,通常由所使用的编程语言或环境的运行时代码处理。

  • 范围是什么?

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

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

    同样,这取决于语言、编译器、操作系统和体系结构。堆栈通常是预先分配的,因为根据定义,它必须是连续的内存。语言编译器或操作系统决定了它的大小。你不会在堆栈上存储大量数据,所以它足够大,不应该被完全使用,除非出现不必要的无休止递归(因此,“堆栈溢出”)或其他不寻常的编程决策。

    堆是任何可以动态分配的东西的总称。根据你看待它的方式,它的大小会不断变化。在现代处理器和操作系统中,它的确切工作方式无论如何都是非常抽象的,所以你通常不需要太担心它在内心深处是如何工作的,除了(在它允许你的语言中)你不能使用你还没有分配的内存或你已经释放的内存。

  • 是什么让人更快?

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

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

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

在“经典”系统中,RAM的布局是这样的:堆栈指针从内存的底部开始,堆指针从顶部开始,它们相互生长。如果它们重叠,你就没有RAM了。不过,这对现代多线程操作系统不起作用。每个线程都必须有自己的堆栈,并且可以动态创建。

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

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

来自WikiAnwser。

堆栈

当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数等时,所有这些函数的执行都会保持暂停状态,直到最后一个函数返回其值。

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

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

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

(我把这个答案从另一个问题移过来,这个问题或多或少是一个欺骗。

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

  • 堆栈和堆都是从底层操作系统分配的内存区域(通常是按需映射到物理内存的虚拟内存)。
  • 在多线程环境中,每个线程都有自己完全独立的堆栈,但它们将共享堆。并发访问必须在堆上控制,在堆栈上是不可能的。

  • 堆包含一个已用块和空闲块的链表。通过从一个空闲块创建一个合适的块来满足堆上的新分配(由newmalloc)。这需要更新堆上的块列表。关于堆上块的元信息也存储在堆上,通常位于每个块前面的一个小区域中。
  • 随着堆的增长,新块通常会从较低的地址分配到较高的地址。因此,您可以将堆视为的内存块,随着内存的分配而增长。如果堆太小而无法分配,通常可以通过从底层操作系统获取更多内存来增加大小。
  • 分配和释放许多小块可能会使堆处于有许多小空闲块散布在使用块之间的状态。分配大块的请求可能会失败,因为即使空闲块的组合大小可能足够大,也没有一个空闲块足够大来满足分配请求。这称为堆碎片
  • 当与空闲块相邻的已用块被释放时,新的空闲块可以与相邻的空闲块合并以创建更大的空闲块,从而有效地减少堆的碎片。

堆

堆栈

  • 堆栈通常与CPU上名为堆栈指针的特殊寄存器紧密协同工作。最初,堆栈指针指向堆栈顶部(堆栈上的最高地址)。
  • CPU有特殊指令,用于将推动值放入堆栈和弹出值移出堆栈。每个推送将值存储在堆栈指针的当前位置并减少堆栈指针。A流行检索堆栈指针指向的值,然后增加堆栈指针(不要被添加指向堆栈的值减少是堆栈指针,删除是值增加这一事实所困惑。请记住堆栈增长到底部)。存储和检索的值是CPU寄存器的值。
  • 如果函数有参数,则在调用函数之前将这些参数推送到堆栈上。然后,函数中的代码能够从当前堆栈指针向上导航堆栈以定位这些值。
  • 调用某个函数时,CPU会使用特殊指令将当前的指令指针压入堆栈。0是执行在堆栈上的代码的地址。CPU会将该指针设置为所调用函数的地址,从而跳转到该函数。之后,当该函数返回时,将旧的指针从堆栈中弹出,继续执行该函数。
  • 输入函数时,堆栈指针会减小以在堆栈上为本地(自动)变量分配更多空间。如果函数有一个本地32位变量,堆栈上会留出四个字节。当函数返回时,堆栈指针会移回以释放分配的区域。
  • 嵌套函数调用像魅力一样工作。每个新调用都将分配函数参数、局部变量的返回地址和空间,这些激活记录可以堆叠用于嵌套调用,并在函数返回时以正确的方式展开。
  • 由于堆栈是一个有限的内存块,你可以通过调用太多嵌套函数和/或为局部变量分配太多空间来导致堆栈溢出。通常,用于堆栈的内存区域的设置方式是,在堆栈底部(最低地址)下方写入将触发CPU中的陷阱或异常。然后,这种异常情况可以被运行时捕获并转换为某种堆栈溢出异常。

堆栈

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

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

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

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

在下面的C#代码中

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

内存是这样管理的

堆栈上的变量图片

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

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

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

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

堆栈和堆内存分配的区别"timmurphy.org

在这里:

在堆栈和堆上创建对象

本文是上面图片的来源:六个重要的。NET概念:堆栈、堆、值类型、引用类型、装箱和拆箱-CodeProject

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

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

“静态”(又名静态分配)变量不会在堆栈上分配。不要这样假设-许多人这样做只是因为“静态”听起来很像“堆栈”。它们实际上既不存在于堆栈中也不存在于堆中。它们是所谓的数据段的一部分。

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

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

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

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

我将提供一些简单的注释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 codeint 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 filestatic 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 toosomeDynamicVariable = 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.

一个特别尖锐的例子说明了为什么区分生命周期和作用域很重要,那就是一个变量可以有局部作用域,但可以有静态生命周期——例如,上面代码示例中的“某些LocalStaticVariable”。这样的变量会让我们常见但非正式的命名习惯变得非常混乱。例如,当我们说“当地”时,我们通常指的是“局部作用域自动分配变量”,当我们说全局时,我们通常指的是“全局范围的静态分配变量”。不幸的是,当涉及到“文件范围的静态分配变量”这样的东西时,许多人只是说…“哈???”。

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

int var1; // Has global scope and static allocationstatic int var2; // Has file scope and static allocation
int main() {return 0;}

请注意,在上面的声明中放置关键字“静态”可以防止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 allocatedionprint("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 allocatedfido = Dog() # Dynamically allocatedrinTinTin = 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

堆栈

  • 非常快速的访问
  • 不必显式地取消分配变量
  • 空间由CPU高效管理,内存不会碎片化
  • 仅局部变量
  • 堆栈大小限制(取决于操作系统)
  • 变量不能调整大小

  • 变量可以全局访问
  • 内存大小无限制
  • (相对)访问速度较慢
  • 没有保证有效使用空间,随着时间的推移,内存可能会变得支离破碎,因为内存块被分配,然后被释放
  • 您必须管理内存(您负责分配和释放变量)
  • 变量可以使用real loc()调整大小

什么是堆栈?

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

在此处输入图片描述

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

什么是堆?

堆是杂乱无章地堆起来的东西的不整洁的集合。

在此处输入图片描述

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

都在一起

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

堆栈和堆哪个更快?为什么?

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

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

Java记忆模型

在此处输入图片描述

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

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

令人惊讶的是,没有人提到多个(即与运行的操作系统级线程数量无关)调用堆栈不仅存在于异国语言(PostScript)或平台(Intel Itanium)中,而且还存在于纤维绿色线程协程的一些实现中。

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

在任何情况下,光纤、绿色线程和协程的目的都是让多个函数同时执行,但没有在单个操作系统级线程中并行执行(请参阅这个so问题的区别),以有组织的方式来回转移控制。

当使用光纤、绿色线程或协程时,每个函数都有一个单独的堆栈。(从技术上讲,每个函数不仅仅是一个堆栈,而是一个完整的执行上下文。最重要的是,CPU寄存器。)对于每个线程,有多少个堆栈就有多少个并发运行的函数,线程根据你的程序逻辑在执行每个函数之间切换。当一个函数运行到最后时,它的堆栈就被销毁了。所以,堆栈的数量和寿命是动态的,不是由OS级线程的数量决定的!是动态的

请注意,我说的是“通常每个函数都有一个单独的堆栈”。有堆叠无堆叠的Couroutines实现。最著名的堆栈C++实现是提升协程microsoftpplasync/await。(然而,C++的可恢复函数(又名“asyncawait”),C++17提出,可能使用无堆栈协程。)

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

在20世纪80年代,UNIX像兔子一样传播,大公司推出了自己的产品。埃克森美孚有一个,几十个品牌也失去了历史。如何设置内存是由许多实现者自行决定的。

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

单个STACK通常是HEAP下面的一个区域,它是一个内存区域不包含任何有价值的东西,直到下一个固定内存块的顶部。下一个块通常是CODE,可以被堆栈数据覆盖在其时代的著名黑客之一。

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

UNIX中虚拟内存的出现改变了许多约束。没有客观的理由说明为什么这些区块必须是毗连的,或者固定的大小,或者现在以特定的方式订购。当然,在UNIX之前是Multics,它没有受到这些限制。这是一个示意图,显示了那个时代的内存布局之一。

典型的1980年代风格的UNIX C程序内存布局

几分钱:我认为,这将是很好的绘制内存图形和更简单:

这是我对进程内存构建的愿景,简化后更容易理解发生了什么


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

所以简单的方法:进程堆对于进程和里面的所有线程都是通用的,在常见的情况下用于内存分配,比如malloc()

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

简而言之

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


详细

堆栈

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

使用堆栈存储变量的好处是内存是为你管理的。你不必手动分配内存,或者在不再需要它时释放它。更重要的是,因为CPU如此有效地组织堆栈内存,从堆栈变量读取和写入堆栈变量非常快。

可以找到更多这里


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

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

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

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

可以找到更多这里


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

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

在此处输入图片描述

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

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

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

在此处输入图片描述

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

甚至,给出了更多细节<强>这里<强>这里


现在来到你的问题的答案

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

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

可以找到更多这里

它们的范围是什么?

已经在顶部。

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

更多信息可以在这里中找到。

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

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

是什么让人更快?

堆栈分配要快得多,因为它真正做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得类似的性能,但这会增加一些复杂性和它自己的麻烦。

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

详细信息可以从这里中找到。

OK,简单地说,他们的意思是命令没有订购…!

堆栈:在堆栈项目中,事情会在彼此的顶部,这意味着处理速度更快,效率更高!…

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

:没有顺序,处理会更慢,值被搞乱了,没有特定的顺序或索引……它们是随机的,它们之间没有关系……所以执行和使用时间可能会有所不同……

我还创建了下面的图像来显示它们的样子:

在此处输入图片描述

作为概念,很多答案都是正确的,但我们必须注意,硬件(即微处理器)需要一个堆栈来允许调用子程序(汇编语言中的CALL…)。(面向对象的人会称之为方法

在堆栈上,您保存返回地址,调用→推送/ret→pop直接在硬件中管理。

您可以使用堆栈传递参数…即使它比使用寄存器慢(微处理器大师会说还是20世纪80年代的BIOS书…)

  • 没有堆栈微处理器可以工作。(我们无法想象一个程序,即使在汇编语言中,没有子程序/函数)
  • 没有堆它可以。(汇编语言程序可以在没有堆的情况下工作,因为堆是一个操作系统概念,如malloc,即OS/Lib调用。

堆栈使用更快,因为:

  • 是硬件,甚至推送/弹出都非常高效。
  • malloc需要进入内核模式,使用锁/信号量(或其他同步原语)执行一些代码并管理一些跟踪分配所需的结构。

虚拟内存中每个进程的堆栈数据

堆栈、堆和静态数据

我有一些东西要分享,虽然要点已经涵盖。

堆栈

  • 非常快速的访问。
  • 存储在RAM中。
  • 函数调用与传递的局部变量和函数参数一起在此处加载。
  • 当程序超出范围时,空间会自动释放。
  • 存储在顺序存储器中。

  • 对Stack的访问相对较慢。
  • 存储在RAM中。
  • 动态创建的变量存储在此处,稍后需要在使用后释放分配的内存。
  • 存储在内存分配完成的任何地方,始终由指针访问。

有趣的说明:

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

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

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

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

有两个堆:公共和私有。

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

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

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

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

堆栈由程序员控制,私有堆由操作系统管理,公共堆不受任何人控制,因为它是操作系统服务——您发出请求,它们要么被授予,要么被拒绝。

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

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

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

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

2)是什么让人更快?

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

参考:

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

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

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

堆栈本质上是一个易于访问的内存,它只是管理它的项目只有预先知道大小的项目可以进入堆栈。这是数字、字符串、布尔值的情况。

堆是您无法预先确定项目的内存精确的大小和结构。由于对象和数组可以突变和在运行时更改,它们必须进入堆。

来源:学术机构

感谢您的讨论,但作为一个真正的新手,我想知道指令保存在哪里?一开始,科学家们在两种架构之间做出决定(von Neumann,其中一切都被认为是DATA和HARVARD,其中一个内存区域用于指令,另一个用于数据)。最终,我们采用了von Neumann设计,现在一切都被认为是“相同的”。这让我在学习汇编时很难https://www.cs.virginia.edu/~evans/cs216/guides/x86.html因为他们谈论寄存器和堆栈指针。

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

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

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

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

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

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

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

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

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

堆栈与堆:知道区别

存储它的静态类内存分配C#

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

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

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

汇编语言资源:

汇编编程教程

英特尔®64和IA-32架构软件开发人员手册

当创建进程时,加载代码后,数据操作系统设置堆在数据结束后启动,并根据架构堆栈到地址空间顶部

当需要更多的堆时,操作系统将动态分配,堆块总是虚拟连续的

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

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

答案:两者都在RAM中。

旁听:

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

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

范围是什么?
是什么决定了它们的大小?
什么能让你更快?

答案:

  1. 堆栈用于静态(固定大小)数据

    a.在编译时,编译器读取代码中使用的变量类型。

    i。它为这些变量分配固定数量的内存。
    这个内存的大小不能增长。

    b.内存是连续的(单个块),因此访问是有时比堆更快

    c.放置在堆栈上的对象在运行时在内存中增长超过堆栈的大小会导致堆栈溢出错误

  2. 堆用于动态(改变大小)数据

    a.内存量仅受RAM中可用空间量的限制
    i.使用的量在运行时根据需要可以增长或收缩

    b.由于项目是通过在RAM中存在的任何地方查找空白空间来在堆上分配的,因此数据并不总是在连续的部分中,这有时使得访问比堆栈慢

    c.程序员手动将带有#0关键字的项目放在堆上,并且必须在使用完成后手动释放此内存。
    i.重复分配新内存而不在不再需要时释放内存的代码会导致内存泄漏。

旁听:

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

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

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

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

答案:

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

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

旁听:

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

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

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

  • 操作系统的ISA称为裸机,其余命令称为扩展机内核是扩展机器的第一层。它控制诸如

    • 确定哪些任务可以使用处理器(调度程序),
    • 要为任务(调度程序)分配多少内存或多少硬件寄存器,以及
    • 执行任务的顺序(交通控制器)。
  • 当我们说“编译器”时,我们通常指的是编译器、汇编器和链接器

    • 编译器将源代码转换为汇编语言并将其传递给汇编器,
    • 汇编器将汇编语言转换为机器代码(ISA命令),并将其传递给链接器
    • 链接器获取所有机器代码(可能从多个源文件生成)并将其组合成一个程序。
  • 机器代码在执行时传递给内核,这决定了它应该何时运行和控制,但是机器代码本身包含用于请求文件、请求内存等的ISA命令,所以代码发出ISA命令,但一切都必须通过内核。

下面是答案,但我肯定会推荐你看这个视频,其中显示了堆栈,内存地址中的堆,显示了堆栈内存地址是如何连续的,这对堆栈和堆的工作方式有了一个公平的了解。https://www.youtube.com/watch?v=7O4JPdKjc30

什么是堆栈和堆?

堆栈和堆是在应用程序运行时分配给应用程序的内存类型。在堆栈中,存储本地原始类型、结构、对象引用指针,而在堆中存储实际对象数据。***堆也可以存储原始类型/结构,只有当它们是对象的一部分时。

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

它们在RAM(运行内存)上分配

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

一切都由OS控制。

它们的范围是什么?

堆栈内存在函数执行后立即被清除。堆稍后被垃圾收集器清除。垃圾收集器以自己的心情运行。因此,即使函数已经完成执行,GC也很有可能稍后出现并将内存还给操作系统。

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

原始数据类型是固定大小的,具体取决于数据类型,例如int是32位,long 64等。但是当涉及到对象时,没有固定大小,它可以是巨大的图像对象,也可以是具有两个3属性的小对象。

是什么让人更快?

堆栈肯定更快,因为内存地址是连续的,所以很容易分配和释放。而堆是以随机地址分配的,所以分配和释放都是乏味的任务。尤其是释放内存是最资源密集型的,这就是垃圾收集器所做的。

想一想是不是很容易找到一本书在堆栈或堆。