栈和堆是什么?它们分别位于哪里?

9349
  • 栈和堆是什么?
  • 它们在计算机内存中的物理位置在哪里?
  • 它们在多大程度上受操作系统或语言运行时的控制?
  • 它们的作用范围是什么?
  • 是什么决定它们的大小?
  • 是什么使它们更快?

246
这里有一个非常好的解释,可以阐述栈和堆之间的区别:什么是栈和堆?它们有何区别? - Songo
18
也很好:http://www.codeproject.com/Articles/76153/Six-important-NET-concepts-Stack-heap-value-types(关于堆栈/堆的部分) - Ben
4
请参见 Stack Clash。栈冲突修复措施会影响诸如 rlimit_stack 等系统变量和行为的某些方面。另请参见 Red Hat 的 Issue 1463241 - jww
4
栈和堆的定义与值类型和引用类型无关,即使没有值类型和引用类型,栈和堆也可以完全定义。而且,在理解值类型和引用类型时,栈只是一种实现细节。根据Eric Lippert的说法:栈是一种实现细节(第一部分) - Matthew
显示剩余2条评论
32个回答

28

几个要点:我认为,将内存绘制成图形并简化更好:

这是我的处理器内存构造的想法,是为了更易于理解发生了什么


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

因此,简单来说,进程堆对进程和其中所有线程通用,通常与像malloc()之类的东西一起用于内存分配。

堆栈是快速的内存,用于在函数返回指针和变量等一般情况下进行存储,作为函数调用中的参数和本地函数变量进行处理。


25

虽然主要内容已经涵盖了,但我还有一些需要分享的。

堆栈

  • 访问非常快。
  • 存储在RAM中。
  • 函数调用与传递的本地变量和函数参数一起加载到此处。
  • 当程序退出作用域时,空间会自动释放。
  • 存储在连续的内存中。

  • 相对于堆栈而言,访问速度较慢。
  • 存储在RAM中。
  • 动态创建的变量存储在此处,使用后需要释放分配的内存。
  • 存储在分配内存的位置,总是通过指针访问。

有趣的事实:

  • 如果函数调用被存储在堆中,将会导致两个混乱的点:
    1. 由于堆栈中的顺序存储,执行速度更快。如果存储在堆中,将导致巨大的时间消耗,从而使整个程序执行速度变慢。
    2. 如果函数存储在堆(通过指针指向混乱的存储)中,就没有办法返回到调用者地址(由于在内存中的顺序存储,堆栈可以给出)。

1
concise and clean. nice:) - ingconti

24

有些答案过于苛刻,我来做出一点贡献。

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

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

无论如何,纤程、绿色线程和协同程序的目的都是在单个操作系统级线程内同时执行多个函数,但不是并行执行(有关区别,请参见此 SO 问题),它们通过有组织的方式相互之间转移控制。

使用纤程、绿色线程或协同程序时,通常每个函数都有一个单独的堆栈。(严格来说,不仅是堆栈,还包括整个执行上下文。最重要的是,CPU 寄存器。)对于每个线程,存在与并发运行的每个函数一样多的堆栈,并且线程按照程序逻辑在这些函数之间进行切换。当函数运行到其结束时,其堆栈被销毁。因此,堆栈数量和生命周期是动态的,不取决于操作系统级线程的数量!

请注意我说“通常每个函数都有一个单独的堆栈”。协程有“有栈”和“无栈”两种实现方式。最著名的C++有栈实现是Boost.CoroutineMicrosoft PPLasync/await。 (但是,C ++的可恢复函数(即“ async”和await “)被建议用于C++17,则可能使用无栈协程。)

C ++标准库的纤程提议即将出现。此外还有一些第三方。在像Python和Ruby这样的语言中,绿色线程非常流行。


17

哇!这么多答案,我觉得没一个是正确的...

1)它们在哪里,以及它们(在实际计算机内存中)是什么?

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

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

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

如果私有堆过大,则会与栈区重叠,就像栈过大时将重叠堆一样。因为栈从更高的地址开始并向低地址运行,借助适当的黑客技巧,您可以使栈变得非常大,以至于它将超出私有堆区域并重叠代码区域。然后的技巧是重叠足够的代码区域,以便您可以钩入代码。这有点棘手且可能导致程序崩溃,但很容易且非常有效。

公共堆驻留在自己的内存空间中,超出您的程序图像空间。如果内存资源不足,它将被吸进硬盘。

2)操作系统或语言运行时控制它们的程度如何?

栈由程序员控制,私有堆由操作系统管理,公共堆不受任何人控制,因为它是一个操作系统服务 - 您发出请求,然后请求可能被授予或拒绝。

2b) 它们的作用域是什么?

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

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

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

2d) 什么使得其中一个更快?

它们并没有被设计成快速的,它们被设计成有用的。程序员如何利用它们决定它们是“快”还是“慢”。

REF:

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


11

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

答案: 它们都位于RAM中。

ASIDE:

RAM就像一张桌子,而HDDs/SSDs (永久存储)则像书架。要阅读任何内容,您必须在桌子上打开一本书,您只能打开适合您桌子大小的书。要获取一本书,您从书架上取出并在桌子上打开它。要归还一本书,您关闭桌子上的书并将其放回书架。

堆栈和堆是编译器用于在同一位置(即RAM中)存储不同类型数据的方式所给出的名称。

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

答案:

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

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

    i. 为这些变量分配一定数量的内存。
    ii. 这个内存的大小无法增长。

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

    c. 在运行时超过堆栈大小并导致堆栈溢出错误的堆栈对象会被放置在堆上

  2. 堆是用于动态(可变大小)数据的

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

    b. 由于在RAM中查找空闲空间来分配项目,因此数据不总是在连续的部分,这有时会导致访问比堆栈慢。

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

附注:

堆和堆栈的主要引入目的不是为了提高速度,而是为了处理内存溢出。使用堆与堆栈之间的主要考虑应该是是否会发生内存溢出。如果对象的大小可能会增长到未知数量(例如链表或其成员可以容纳任意数量数据的对象),则将其放置在堆上。尽可能使用C++标准库(STL)容器vector、map和list,它们既具有内存效率又具有速度效率,并且添加它们是为了让您的生活更轻松(您不需要担心内存分配/释放)。

在让代码能够运行后,如果您发现它的运行速度不可接受,那么回过头来重构您的代码并查看是否可以更加高效地编写程序。问题可能根本与堆栈或堆没有直接关系(例如,使用迭代算法而不是递归算法,考虑I/O与CPU绑定任务,也许添加多线程或多进程等)。

我之前说“有时候”快慢是因为程序的速度可能与在堆栈或堆上分配项目无关。

在多大程度上由操作系统或语言运行时来控制?

答案:

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

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

附注:

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

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

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

  • 操作系统(OS)的ISA被称为裸机(bare machine),其余命令被称为扩展机器(extended machine)。内核是扩展机器的第一层。它控制诸如:

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

    • 编译器将源代码转换为汇编语言,并将其传递给汇编器,
    • 汇编器将汇编语言转换为机器码(ISA指令),并将其传递给链接器,
    • 链接器将所有机器码(可能由多个源文件生成)合并为一个程序。
  • 当执行时,机器码被传递到内核中,内核确定何时运行和接管控制,但机器码本身包含用于请求文件、请求内存等的ISA指令。因此,代码发出ISA指令,但所有内容都必须通过内核。


  • 10
    很多答案在概念上都是正确的,但我们必须注意到硬件(即微处理器)需要一个堆栈来允许调用子程序(汇编语言中的CALL..)。 (面向对象的人会称其为方法
    在堆栈上保存返回地址和调用→推送/ ret →弹出由硬件直接管理。
    您可以使用堆栈传递参数..即使比使用寄存器更慢(可能是微处理器大师或良好的1980年代BIOS书籍所说...)
    没有堆栈,没有微处理器可以工作。(我们无法想象一个程序,即使是在汇编语言中,也没有子例程/函数)
    没有堆,它可以。 (汇编语言程序可以正常工作,因为堆是一个操作系统概念,如malloc,它是一个OS / Lib调用。)
    堆栈使用速度更快,因为:
    - 是硬件,即使是推送/弹出也非常高效。 - malloc需要进入内核模式,使用锁/信号量(或其他同步原语)执行一些代码并管理一些结构以跟踪分配情况。

    什么是OPP?你是指OOP(面向对象编程)吗? - Peter Mortensen
    你的意思是说 malloc 是一个内核调用吗? - Peter Mortensen
    1. 是的,抱歉.. 面向对象编程...
    2. malloc:我简单地写一下,抱歉... malloc 在用户空间中... 但可能会触发其他调用... 关键是使用堆可能会非常慢...
    - ingconti
    很多答案在概念上是正确的,但我们必须注意到硬件(即微处理器)需要一个堆栈来允许调用子程序(汇编语言中的CALL)。您正在混淆CPU堆栈(如果现代CPU中有堆栈)和语言运行时堆栈(每个线程一个)。当程序员谈论堆栈时,这是运行时的线程执行堆栈,例如.NET线程堆栈),我们不是在谈论CPU堆栈。 - mins
    “NET线程”并不是真正的堆栈。(JVM也一样)它们只是软件概念。(其他人称之为“激活记录”)我们必须从个人电脑历史上真正的电路开始,才能真正理解。 - ingconti

    6
    我觉得大多数答案都很复杂和技术性,但我没有找到一个能简单解释这两个概念背后的原因(即为什么人们首先创建它们?)以及为什么您应该关心它们。以下是我的一种尝试:

    堆栈上的数据是临时的并且会自动清除

    堆上的数据是永久的,直到手动删除

    那就是它。
    但是,如果需要更多解释: 堆栈旨在用作短暂或工作内存,这是我们知道将在程序生命周期内定期完全删除的内存空间。这就像您桌子上的备忘录,您随便涂鸦任何想法,几乎感觉可能很重要,但是您知道您将在一天结束时扔掉它,因为您将在另一个介质中过滤和组织实际重要的笔记,例如文档或书籍。我们不关心演示文稿、划掉或无法理解的文本,这只是我们当天工作的快速而肮脏的方式,我们稍后想要记住的想法的存储方式,而不会伤害我们当前的思维流。这就是人们所说的“堆栈是草稿本”。
    然而,是长期记忆,实际上是创建后很长一段时间内将存储、查询和依赖的重要文档。因此,它需要具有完美的形式并严格包含重要数据。这就是为什么它成本高昂并且不能用于我们先前备忘录的用例。将所有笔记都写在学术论文演示文稿中,将文本写成书法并不值得,甚至只是无用的。但是,这种演示对于经过精心策划的数据非常有用。这就是堆应该是什么样子的。熟知的数据,对应用程序寿命非常重要,在您的代码中需要在许多地方进行控制。因此,系统永远不会在您明确要求之前删除此宝贵的数据,因为它知道“重要数据在那里!”
    这就是为什么您需要管理并关注堆上的内存分配,但不需要为堆栈而烦恼。
    大多数顶级答案仅涉及实际计算机中该概念的实现的技术细节。
    因此,从中可以得出以下结论: 不重要的、工作性质的、临时的数据只是为了使我们的函数和对象工作而(通常)更相关的存储在堆栈上。 重要的、永久的和基础应用程序数据(通常)更相关的存储在堆上。 当然,这只需要考虑程序的生命周期。由程序生成的真正重要的数据显然需要存储在外部文件中。(因为无论是堆还是栈,在程序终止时都会被完全清除。)
    PS:这些只是一般规则,你总是可以找到边缘情况,并且每种语言都带有其自身的实现和结果怪癖,这旨在作为概念和经验法则。

    2
    堆栈(stack)本质上是一种易于访问的内存,它将其项作为一个堆栈进行管理。只有那些大小预先知道的项才能放入堆栈中。这适用于数字、字符串和布尔值。
    堆(heap)是一种内存,用于存储那些无法预先确定大小和结构的项。由于对象和数组可以在运行时发生变异和改变,因此它们必须进入堆中。
    来源:Academind

    1
    CPU的堆栈和堆与CPU及寄存器如何与内存交互、机器汇编语言的工作原理有着物理关联,而不是高级语言本身,即使这些语言可以决定一些小事情。
    所有现代CPU都使用“相同”的微处理器理论:它们都基于所谓的“寄存器”,其中一些用于“堆栈”以获得性能。自始至终,所有CPU都具有堆栈寄存器,而且一直存在,就我所知,汇编语言自始至终都是相同的,尽管有所变化...直到Microsoft及其中间语言(IL)改变了范例,引入了OO虚拟机汇编语言。因此,我们将来可能会有一些CLI/CIL CPU(MS的一个项目)。
    CPU具有堆栈寄存器以加速内存访问,但与使用其他寄存器来完全访问所有可用内存相比,它们受到限制。这就是为什么我们谈论堆栈和堆分配的原因。
    总之,在一般情况下,堆非常大而且慢,用于“全局”实例和对象内容,而堆栈很小而且快,用于“本地”变量和引用(隐藏指针,以便忘记管理它们)。

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

    内存访问的差异在于单元格引用级别:访问堆需要更多的复杂性来处理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#

    栈和堆是什么,它们分别在哪里?

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

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

    汇编语言资源:

    汇编语言编程教程

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


    0

    非常感谢您进行了一次非常好的讨论,但作为一个真正的新手,我想知道指令存储在哪里?在最初,科学家们在决定两种架构(von NEUMANN,其中一切都被视为数据和HARVARD,其中一个内存区域被保留用于指令和另一个用于数据)之间进行选择。最终,我们选择了von Neumann设计,现在一切都被认为是“相同的”。当我学习汇编语言https://www.cs.virginia.edu/~evans/cs216/guides/x86.html时,这使我感到困难,因为他们谈论寄存器和堆栈指针。

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


    据我所知,仍然存在许多具有哈佛架构(通常优化的DSP)的CPU,其具有分离的指令和数据存储器(和总线)。 (其中一些甚至拥有超过1个数据存储器)。 这是为了优化周期持续时间(指令获取,数据获取和上一个指令的执行都在一个机器周期内),从而利用他们所谓的流水线。 我不知道指令是否必须以任何方式放置在堆栈中。 无论如何,我有一种隐隐的感觉,这个答案可能会超出原始问题的范围... - Sold Out
    @SoldOut 这根本不是答案...而是另一个问题,污染了答案流。如果仍然需要,应该将其删除并重新发布为单独的问题。 - adamency

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接