编程语言书籍解释了值类型是在堆栈上创建的,而引用类型是在堆上创建的,但没有解释这两个东西是什么。我还没有阅读对此的明确解释。我明白什么是堆栈。但,
- 它们在哪里,是什么(物理上在真实计算机的内存中)?
- 它们在多大程度上受操作系统或语言运行时的控制?
- 他们的范围是什么?
- 是什么决定了它们每个的大小?
- 是什么让一个更快?
编程语言书籍解释了值类型是在堆栈上创建的,而引用类型是在堆上创建的,但没有解释这两个东西是什么。我还没有阅读对此的明确解释。我明白什么是堆栈。但,
堆栈是为执行线程预留的内存空间。调用函数时,堆栈顶部会保留一个块用于局部变量和一些簿记数据。当该函数返回时,该块将变为未使用,并且可以在下次调用函数时使用。堆栈始终以 LIFO(后进先出)顺序保留;最近保留的块总是下一个要释放的块。这使得跟踪堆栈变得非常简单;从堆栈中释放一个块只不过是调整一个指针。
堆是为动态分配预留的内存。与堆栈不同,堆中块的分配和释放没有强制模式;您可以随时分配块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分已分配或空闲变得更加复杂。有许多自定义堆分配器可用于针对不同的使用模式调整堆性能。
每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管有多个堆用于不同类型的分配并不少见)。
直接回答您的问题:
它们在多大程度上受操作系统或语言运行时的控制?
操作系统在创建线程时为每个系统级线程分配堆栈。通常,语言运行时调用操作系统来为应用程序分配堆。
他们的范围是什么?
堆栈附加到线程,因此当线程退出时,堆栈被回收。堆通常由运行时在应用程序启动时分配,并在应用程序(技术进程)退出时回收。
是什么决定了它们每个的大小?
堆栈的大小在创建线程时设置。堆的大小在应用程序启动时设置,但可以随着空间的需要而增长(分配器从操作系统请求更多内存)。
是什么让一个更快?
堆栈更快,因为访问模式使得从中分配和释放内存变得微不足道(指针/整数只是增加或减少),而堆在分配或释放中涉及更复杂的簿记。此外,堆栈中的每个字节往往被非常频繁地重用,这意味着它往往被映射到处理器的缓存,从而使其非常快。堆的另一个性能损失是堆,主要是全局资源,通常必须是多线程安全的,即每个分配和释放需要 - 通常 - 与程序中的“所有”其他堆访问同步。
清晰的演示:
图片来源:vikashazrati.wordpress.com
堆:
堆:
delete
使用、delete[]
或释放数据free
。new
or分配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;
最重要的一点是,堆和栈是内存分配方式的通用术语。它们可以以许多不同的方式实现,并且这些术语适用于基本概念。
在一堆物品中,物品按照它们放置在那里的顺序一个放在另一个上面,您只能移除最上面的一个(不会将整个东西翻倒)。
堆栈的简单性在于您不需要维护一个包含每个已分配内存部分的记录的表;您需要的唯一状态信息是指向堆栈末尾的单个指针。要分配和取消分配,您只需增加和减少单个指针。注意:有时可以实现堆栈以从一段内存的顶部开始并向下扩展而不是向上增长。
在堆中,物品的放置方式没有特定的顺序。您可以按任何顺序进入和移除项目,因为没有明确的“顶部”项目。
堆分配需要维护已分配和未分配内存的完整记录,以及一些开销维护以减少碎片,找到足够大以适应请求大小的连续内存段,等等。内存可以随时释放,留下可用空间。有时,内存分配器将执行维护任务,例如通过移动分配的内存或垃圾收集来对内存进行碎片整理 - 在运行时识别内存不再在范围内并释放它。
这些图像应该很好地描述了在堆栈和堆中分配和释放内存的两种方式。嗯!
它们在多大程度上受操作系统或语言运行时的控制?
如前所述,堆和堆栈是通用术语,可以通过多种方式实现。计算机程序通常有一个称为调用堆栈的堆栈,它存储与当前函数相关的信息,例如指向它被调用的任何函数的指针,以及任何局部变量。因为函数调用其他函数然后返回,所以堆栈会增长和缩小以保存来自调用堆栈更下方的函数的信息。程序实际上并没有对其进行运行时控制。它由编程语言、操作系统甚至系统架构决定。
堆是用于任何动态随机分配的内存的通用术语;即乱序。内存通常由操作系统分配,应用程序调用 API 函数来执行此分配。管理动态分配的内存需要相当多的开销,这通常由所使用的编程语言或环境的运行时代码处理。
他们的范围是什么?
调用堆栈是一个如此低级的概念,它与编程意义上的“范围”无关。如果您反汇编一些代码,您将看到对堆栈部分的相对指针样式引用,但就更高级别的语言而言,该语言强加了自己的范围规则。然而,堆栈的一个重要方面是,一旦函数返回,该函数本地的任何内容都会立即从堆栈中释放。考虑到您的编程语言的工作方式,这将按照您期望的方式工作。在堆中,也很难定义。范围是操作系统公开的任何内容,但是您的编程语言可能会添加有关应用程序中“范围”是什么的规则。处理器架构和操作系统使用虚拟寻址,处理器将其转换为物理地址,并且存在页面错误等。它们跟踪哪些页面属于哪些应用程序。不过,您永远不需要担心这一点,因为您只需使用您的编程语言用来分配和释放内存的任何方法,并检查错误(如果分配/释放因任何原因失败)。
是什么决定了它们每个的大小?
同样,它取决于语言、编译器、操作系统和架构。堆栈通常是预先分配的,因为根据定义它必须是连续的内存。语言编译器或操作系统决定了它的大小。您不会在堆栈上存储大量数据,因此它将足够大以至于永远不会完全使用它,除非在不需要的无限递归(因此,“堆栈溢出”)或其他不寻常的编程决策的情况下。
堆是可以动态分配的任何东西的总称。根据你看待它的方式,它的大小会不断变化。在现代处理器和操作系统中,它的确切工作方式无论如何都是非常抽象的,所以你通常不需要太担心它是如何工作的,除了(在它允许你的语言中)你不能使用内存您尚未分配或已释放的内存。
是什么让一个更快?
堆栈更快,因为所有空闲内存始终是连续的。不需要维护所有空闲内存段的列表,只需一个指向当前堆栈顶部的指针。为此,编译器通常将此指针存储在一个特殊的快速寄存器中。更重要的是,堆栈上的后续操作通常集中在非常靠近的内存区域中,这在非常低的级别有利于处理器片上缓存的优化。
(我已将这个答案从另一个或多或少是这个问题的欺骗的问题中移出。)
您的问题的答案是特定于实现的,并且可能因编译器和处理器架构而异。但是,这里有一个简化的解释。
new
或)。malloc
这需要更新堆上的块列表。有关堆上块的元信息也存储在堆上,通常位于每个块前面的一小块区域中。可以在堆而不是堆栈上分配函数吗?
不,函数(即本地或自动变量)的激活记录分配在堆栈上,该堆栈不仅用于存储这些变量,还用于跟踪嵌套函数调用。
如何管理堆实际上取决于运行时环境。C 使用malloc
和 C++ 使用new
,但许多其他语言都有垃圾收集。
但是,堆栈是与处理器架构密切相关的更底层的功能。当没有足够的空间时增加堆并不太难,因为它可以在处理堆的库调用中实现。然而,增加堆栈通常是不可能的,因为堆栈溢出只有在为时已晚时才被发现;关闭执行线程是唯一可行的选择。
在以下 C# 代码中
public void Method1()
{
int i = 4;
int y = 2;
class1 cls1 = new class1();
}
这是内存的管理方式
Local Variables
只要函数调用进入堆栈,它就需要持续。堆用于存放我们事先并不真正知道其生命周期但我们希望它们能持续一段时间的变量。在大多数语言中,如果我们想将变量存储在堆栈中,我们必须在编译时知道变量有多大。
对象(随着我们更新它们的大小会有所不同)在堆上,因为我们在创建时不知道它们会持续多久。在许多语言中,堆被垃圾收集以查找不再有任何引用的对象(例如 cls1 对象)。
在 Java 中,大多数对象直接进入堆。在 C / C++ 等语言中,当您不处理指针时,结构和类通常会保留在堆栈上。
更多信息可以在这里找到:
和这里:
本文为上图来源:6个重要的.NET概念:栈、堆、值类型、引用类型、装箱、拆箱 - CodeProject
但请注意,它可能包含一些不准确之处。
堆栈 当您调用一个函数时,该函数的参数加上一些其他开销都放在堆栈上。一些信息(例如去哪里返回)也存储在那里。当您在函数中声明一个变量时,该变量也会在堆栈上分配。
释放堆栈非常简单,因为您总是以与分配相反的顺序释放。当您输入函数时会添加堆栈内容,退出函数时会删除相应的数据。这意味着您倾向于停留在堆栈的一个小区域内,除非您调用许多调用许多其他函数的函数(或创建递归解决方案)。
堆堆 是放置动态创建的数据的通用名称。如果您不知道您的程序将创建多少艘飞船,您可能会使用 new(或 malloc 或等效的)运算符来创建每艘飞船。这种分配会持续一段时间,所以我们很可能会以不同于我们创建它们的顺序释放它们。
因此,堆要复杂得多,因为最终会出现未使用的内存区域与块交错 - 内存变得碎片化。找到所需大小的空闲内存是一个难题。这就是为什么应该避免使用堆的原因(尽管它仍然经常使用)。
堆栈和堆的实现 通常取决于运行时/操作系统。通常,对性能至关重要的游戏和其他应用程序会创建自己的内存解决方案,这些解决方案会从堆中获取大量内存,然后在内部将其分发出去,以避免依赖操作系统来获取内存。
这仅在您的内存使用与标准完全不同的情况下才实用 - 即对于您在一个巨大的操作中加载一个关卡并且可以在另一个巨大的操作中丢弃全部内容的游戏。
内存中的物理位置 这并不像您想象的那么重要,因为一种称为虚拟内存的技术使您的程序认为您可以访问物理数据在其他地方(甚至在硬盘上!)的某个地址。随着调用树的深入,您获得的堆栈地址将按递增顺序排列。堆的地址是不可预测的(即特定于实现的)并且坦率地说并不重要。
其他答案只是避免解释静态分配的含义。因此,我将在下面解释三种主要的分配形式以及它们通常与堆、堆栈和数据段的关系。我还将展示一些 C/C++ 和 Python 中的示例,以帮助人们理解。
“静态”(AKA 静态分配)变量不在堆栈上分配。不要这么假设——很多人这样做只是因为“静态”听起来很像“堆栈”。它们实际上既不存在于堆栈中,也不存在于堆中。它们是所谓的数据段的一部分。
但是,通常最好考虑“范围”和“生命周期”而不是“堆栈”和“堆”。
范围是指代码的哪些部分可以访问变量。通常我们会考虑局部作用域(只能由当前函数访问)与全局作用域(可以在任何地方访问),尽管作用域会变得更加复杂。
生命周期是指在程序执行期间分配和释放变量的时间。通常我们会想到静态分配(变量将在程序的整个持续时间内持续存在,这对于在多个函数调用中存储相同的信息很有用)与自动分配(变量仅在对函数的单个调用期间持续存在,使其有用存储仅在函数期间使用并且一旦完成就可以丢弃的信息)与动态分配(其持续时间在运行时定义的变量,而不是像静态或自动的编译时间)。
尽管大多数编译器和解释器在使用堆栈、堆等方面类似地实现此行为,但只要行为正确,编译器有时可能会破坏这些约定。例如,由于优化,局部变量可能只存在于寄存器中或被完全删除,即使大多数局部变量存在于堆栈中。正如一些评论中所指出的,您可以自由地实现一个编译器,它甚至不使用堆栈或堆,而是使用其他一些存储机制(很少这样做,因为堆栈和堆非常适合此)。
我将提供一些简单的带注释的 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
其他人已经很好地回答了粗略的笔画,所以我将提供一些细节。
堆栈和堆不必是单数。拥有多个堆栈的常见情况是进程中有多个线程。在这种情况下,每个线程都有自己的堆栈。您还可以拥有多个堆,例如,某些 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.
}
其他人已经直接回答了您的问题,但是在尝试理解堆栈和堆时,我认为考虑传统 UNIX 进程的内存布局(没有线程和mmap()
基于 - 的分配器)是有帮助的。内存管理词汇表网页有一个内存布局图。
堆栈和堆传统上位于进程的虚拟地址空间的两端。堆栈在访问时会自动增长,达到内核设置的大小(可以使用 调整setrlimit(RLIMIT_STACK, ...)
)。当内存分配器调用brk()
orsbrk()
系统调用时,堆会增长,将更多的物理内存页面映射到进程的虚拟地址空间。
在没有虚拟内存的系统中,例如一些嵌入式系统,通常采用相同的基本布局,只是堆栈和堆的大小是固定的。但是,在其他嵌入式系统中(例如基于 Microchip PIC 单片机的系统),程序堆栈是一个单独的内存块,无法通过数据移动指令寻址,只能通过程序流指令(调用、返回等)。其他架构(例如 Intel Itanium 处理器)具有多个堆栈。从这个意义上说,堆栈是 CPU 架构的一个元素。
什么是堆栈?
堆栈是一堆对象,通常是整齐排列的对象。
计算架构中的堆栈是以后进先出的方式添加或删除数据的内存区域。
在多线程应用程序中,每个线程都有自己的堆栈。
什么是堆?
堆是杂乱无章的东西堆积起来的不规则集合。
在计算架构中,堆是动态分配的内存区域,由操作系统或内存管理器库自动管理。
在程序执行期间,堆上的内存会被定期分配、释放和调整大小,这可能会导致称为碎片的问题。
当分配的内存对象之间的小空间太小而无法容纳额外的内存对象时,就会发生碎片。
最终结果是不能用于进一步内存分配的堆空间百分比。
两者一起
在多线程应用程序中,每个线程都有自己的堆栈。但是,所有不同的线程将共享堆。
因为在多线程应用程序中不同的线程共享堆,这也意味着线程之间必须有一些协调,以便它们不会尝试访问和操作堆中的相同内存块同时。
哪个更快——堆栈还是堆?为什么?
堆栈比堆快得多。
这是因为在堆栈上分配内存的方式。
在堆栈上分配内存就像向上移动堆栈指针一样简单。
对于刚接触编程的人来说,使用堆栈可能是一个好主意,因为它更容易。
因为堆栈很小,所以当您确切知道数据需要多少内存时,或者如果您知道数据的大小非常小,您会想要使用它。
当您知道需要大量内存来存储数据时,最好使用堆,或者您只是不确定需要多少内存(例如使用动态数组)。
堆栈是存储局部变量(包括方法参数)的内存区域。当涉及到对象变量时,这些仅仅是堆上实际对象的引用(指针)。
每次实例化一个对象时,都会留出一块堆内存来保存该对象的数据(状态)。由于对象可以包含其他对象,因此其中一些数据实际上可以保存对这些嵌套对象的引用。
堆栈是内存的一部分,可以通过几个关键的汇编语言指令进行操作,例如“pop”(从堆栈中删除并返回一个值)和“push”(将一个值推送到堆栈),但也可以调用 (调用子程序 - 这会将地址推送到堆栈中)并返回(从子程序返回 - 这会将地址从堆栈中弹出并跳转到它)。它是堆栈指针寄存器下方的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子程序,也用于在调用子程序之前保存寄存器中的值。
堆是由操作系统提供给应用程序的内存的一部分,通常通过诸如 malloc 之类的系统调用。在现代操作系统上,这个内存是一组只有调用进程才能访问的页面。
堆栈的大小是在运行时确定的,一般在程序启动后不会增长。在 C 程序中,堆栈需要足够大以容纳每个函数中声明的每个变量。堆将根据需要动态增长,但操作系统最终会发出调用(它通常会使堆增长超过 malloc 请求的值,因此至少一些未来的 malloc 不需要回到内核来获得更多内存。这种行为通常是可定制的)
因为您在启动程序之前已经分配了堆栈,所以在使用堆栈之前您永远不需要 malloc,所以这是一个轻微的优势。在实践中,很难预测在具有虚拟内存子系统的现代操作系统中什么会快什么会慢,因为页面是如何实现的以及它们存储在哪里是一个实现细节。
我想很多其他人在这个问题上给了你大部分正确的答案。
然而,一个被遗漏的细节是“堆”实际上应该被称为“自由存储”。这种区别的原因是最初的自由存储是用一种称为“二项式堆”的数据结构实现的。出于这个原因,从 malloc()/free() 的早期实现中分配是从堆中分配的。然而,在现代,大多数免费商店都是用非常复杂的数据结构实现的,这些数据结构不是二项式堆。
你可以用堆栈做一些有趣的事情。例如,你有像alloca这样的函数(假设你可以通过关于它的使用的大量警告),这是一种 malloc 形式,专门使用堆栈而不是堆来存储内存。
也就是说,基于堆栈的内存错误是我经历过的最糟糕的一些错误。如果您使用堆内存,并且超出了分配块的范围,那么您很有可能触发段错误。(不是 100%:您的块可能偶然与您之前分配的另一个块相邻。)但是由于在堆栈上创建的变量始终彼此相邻,因此越界写入可能会更改另一个变量的值。我了解到,每当我觉得我的程序不再遵守逻辑规律时,很可能是缓冲区溢出。
简单地说,堆栈是创建局部变量的地方。此外,每次调用子程序时,程序计数器(指向下一条机器指令的指针)和任何重要的寄存器,有时参数都会被压入堆栈。然后子例程内的任何局部变量都被压入堆栈(并从那里使用)。当子例程完成时,所有东西都会从堆栈中弹出。PC 和寄存器数据在弹出时得到并放回原处,因此您的程序可以继续进行。
堆是内存动态内存分配的区域(显式“新”或“分配”调用)。它是一种特殊的数据结构,可以跟踪不同大小的内存块及其分配状态。
在“经典”系统中,RAM 的布局使得堆栈指针从内存底部开始,堆指针从顶部开始,它们彼此相向增长。如果它们重叠,则说明您的 RAM 不足。但是,这不适用于现代多线程操作系统。每个线程都必须有自己的堆栈,并且这些堆栈可以动态创建。
来自 WikiAnwser。
当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数等时,所有这些函数的执行将保持挂起,直到最后一个函数返回其值。
这个挂起的函数调用链就是堆栈,因为堆栈中的元素(函数调用)相互依赖。
堆栈在异常处理和线程执行中很重要。
堆只是程序用来存储变量的内存。堆的元素(变量)相互之间没有依赖关系,可以随时随机访问。
堆
堆
堆栈用于静态内存分配和堆用于动态内存分配,两者都存储在计算机的 RAM 中。
堆栈
堆栈是一种“LIFO”(后进先出)数据结构,由 CPU 密切管理和优化。每次函数声明一个新变量时,它都会被“推入”堆栈。然后每次函数退出时,该函数压入堆栈的所有变量都被释放(也就是说,它们被删除)。释放堆栈变量后,该内存区域可用于其他堆栈变量。
使用堆栈存储变量的优点是内存是为您管理的。您不必手动分配内存,也不必在不再需要时释放它。更重要的是,由于 CPU 如此高效地组织堆栈内存,读取和写入堆栈变量非常快。
更多可以在这里找到。
堆
堆是计算机内存中的一个区域,它不会自动为您管理,也不是由 CPU 严格管理。它是一个更自由浮动的内存区域(并且更大)。要在堆上分配内存,您必须使用 malloc() 或 calloc(),它们是 C 的内置函数。一旦你在堆上分配了内存,你有责任在你不再需要它时使用 free() 来释放该内存。
如果你不这样做,你的程序就会发生所谓的内存泄漏。也就是说,堆上的内存仍将被留出(并且不会对其他进程可用)。正如我们将在调试部分看到的,有一个名为Valgrind的工具可以帮助您检测内存泄漏。
与堆栈不同,堆对可变大小没有大小限制(除了计算机明显的物理限制)。堆内存的读取和写入速度稍慢,因为必须使用指针来访问堆上的内存。我们将很快讨论指针。
与堆栈不同,在堆上创建的变量可以被程序中任何位置的任何函数访问。堆变量在范围内本质上是全局的。
更多可以在这里找到。
分配在栈上的变量直接存储到内存中,访问这块内存非常快,它的分配是在程序编译的时候处理的。当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数等时,所有这些函数的执行将保持挂起,直到最后一个函数返回其值。堆栈始终以 LIFO 顺序保留,最近保留的块始终是下一个要释放的块。这使得跟踪堆栈变得非常简单,从堆栈中释放一个块只不过是调整一个指针。
在堆上分配的变量在运行时分配它们的内存,访问这块内存有点慢,但堆大小仅受虚拟内存大小的限制。堆的元素彼此之间没有依赖关系,并且总是可以随时随机访问。您可以随时分配块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分已分配或空闲变得更加复杂。
如果您确切知道在编译时需要分配多少数据,并且不会太大,则可以使用堆栈。如果您不知道运行时需要多少数据或者需要分配大量数据,则可以使用堆。
在多线程情况下,每个线程都有自己完全独立的堆栈,但它们将共享堆。堆栈是特定于线程的,而堆是特定于应用程序的。堆栈在异常处理和线程执行中很重要。
每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管有多个堆用于不同类型的分配并不少见)。
在运行时,如果应用程序需要更多的堆,它可以从空闲内存中分配内存,如果堆栈需要内存,它可以从为应用程序分配的空闲内存中分配内存。
现在来回答你的问题。
它们在多大程度上受操作系统或语言运行时的控制?
操作系统在创建线程时为每个系统级线程分配堆栈。通常,语言运行时调用操作系统来为应用程序分配堆。
更多可以在这里找到。
他们的范围是什么?
已经在上面给出了。
“如果您确切知道在编译时需要分配多少数据,并且它不是太大,您可以使用堆栈。如果您不知道运行时需要多少数据或者如果您不知道在运行时需要多少数据,您可以使用堆。你需要分配大量数据。”
更多可以在这里找到。
是什么决定了它们每个的大小?
堆栈的大小由操作系统在创建线程时设置。堆的大小在应用程序启动时设置,但它可以随着空间的需要而增长(分配器从操作系统请求更多内存)。
是什么让一个更快?
堆栈分配要快得多,因为它真正所做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得相当的性能,但这会稍微增加复杂性并带来一些麻烦。
此外,堆栈与堆不仅是性能考虑因素;它还告诉你很多关于对象的预期生命周期的信息。
可以从这里找到详细信息。
在 1980 年代,UNIX 像兔子一样传播开来,大公司纷纷推出自己的产品。埃克森美孚拥有一个,数十个品牌名称已被历史遗忘。内存的布局方式由许多实现者自行决定。
一个典型的 C 程序在内存中被平放,并有机会通过更改 brk() 值来增加。通常,HEAP 刚好低于这个 brk 值,增加 brk 会增加可用堆的数量。
单个堆栈通常是 HEAP 下方的区域,它是一段内存,在下一个固定内存块的顶部之前不包含任何价值。下一个块通常是 CODE,它可以被当时著名的黑客攻击中的堆栈数据覆盖。
一个典型的内存块是 BSS(零值块),它在一家制造商的产品中意外地没有归零。另一个是包含初始化值的 DATA,包括字符串和数字。第三种是包含 CRT(C 运行时)、main、函数和库的 CODE。
UNIX 中虚拟内存的出现改变了许多限制。没有客观的理由为什么这些块需要是连续的、大小固定的,或者现在以特定的方式排序。当然,在 UNIX 之前是不受这些限制的 Multics。这是一个示意图,显示了那个时代的一种内存布局。
由于有些答案很挑剔,我将贡献我的一点。
令人惊讶的是,没有人提到不仅在外来语言(PostScript)或平台(Intel Itanium)中,而且在纤维,绿色线程中都可以找到多个(即与运行的操作系统级线程的数量无关)调用堆栈以及协程的一些实现。
纤维、绿色线程和协程在很多方面都很相似,这会导致很多混乱。光纤和绿色线程之间的区别在于,前者使用协作多任务,而后者可能具有协作或抢占式(甚至两者兼而有之)。关于纤维和协程之间的区别,请参见此处。
在任何情况下,纤程、绿色线程和协程的目的都是在单个操作系统级线程中同时执行多个功能,但不是并行执行(请参阅此 SO 问题以了解区别),从而在彼此之间来回传输控制以有组织的方式。
当使用纤程、绿色线程或协程时,通常每个函数都有一个单独的堆栈。(从技术上讲,每个函数不仅是堆栈,而且整个执行上下文都是每个函数。最重要的是,CPU 寄存器。)对于每个线程,堆栈的数量与并发运行的函数一样多,并且线程在执行每个函数之间切换根据您的程序的逻辑。当一个函数运行到它的末尾时,它的堆栈被销毁。因此,堆栈的数量和生命周期是动态的,而不是由操作系统级线程的数量决定的!
请注意,我说“通常每个函数都有一个单独的堆栈”。couroutines有stackful和stackless两种实现。最著名的堆栈式 C++ 实现是Boost.Coroutine和Microsoft PPL的async/await
. (但是,在 C++17 中提出的 C ++ 的可恢复函数(又名 "async
和")很可能使用无堆栈协程。)await
Fibers 对 C++ 标准库的提议即将发布。此外,还有一些第三方库。绿色线程在 Python 和 Ruby 等语言中非常流行。
我有一些东西要分享,虽然要点已经讲过了。
堆
堆
有趣的注释:
哇!这么多答案,我不认为其中一个是正确的......
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
许多答案作为概念是正确的,但我们必须注意,硬件(即微处理器)需要一个堆栈来允许调用子例程(汇编语言中的 CALL..)。(OOP 家伙会称之为方法)
在堆栈上保存返回地址并调用 → push / ret → pop 直接在硬件中进行管理。
您可以使用堆栈来传递参数..即使它比使用寄存器慢(微处理器专家会说还是一本好的 1980 年代 BIOS 书......)
堆栈使用速度更快,因为:
堆栈本质上是一个易于访问的内存,它只是将其项目作为一个 - 好 - 堆栈来管理。只有事先知道其大小的项目才能进入堆栈。数字、字符串、布尔值就是这种情况。
堆是您无法预先确定确切大小和结构的项目的内存。由于对象和数组可以在运行时变异和更改,因此它们必须进入堆。
资料来源:学术
CPU 堆栈和堆在物理上与 CPU 和寄存器如何与内存一起工作,机器汇编语言如何工作,而不是高级语言本身有关,即使这些语言可以决定一些小事情。
所有现代 CPU 都使用“相同”的微处理器理论:它们都基于所谓的“寄存器”,有些用于“堆栈”以获得性能。从一开始,所有 CPU 都有堆栈寄存器,而且它们一直都在这里,就像我所知的那样。汇编语言从一开始就相同,尽管存在差异……直到 Microsoft 及其中间语言 (IL) 改变了范式以拥有 OO 虚拟机汇编语言。所以将来我们将能够拥有一些 CLI/CIL CPU(MS 的一个项目)。
CPU 具有堆栈寄存器来加速内存访问,但与使用其他寄存器来完全访问进程的所有可用内存相比,它们受到限制。这就是我们谈论堆栈和堆分配的原因。
总之,一般来说,堆是巨大而缓慢的,用于“全局”实例和对象内容,因为堆栈小而快,并且用于“局部”变量和引用(隐藏指针以忘记管理它们)。
因此,当我们在方法中使用 new 关键字时,引用(一个 int)是在堆栈中创建的,但如果我记得的话,对象及其所有内容(值类型以及对象)都是在堆中创建的。但是本地基本值类型和数组是在堆栈中创建的。
内存访问的区别在于单元引用级别:寻址堆,即进程的整体内存,在处理 CPU 寄存器方面需要更多的复杂性,而不是在寻址方面“更多”本地的堆栈,因为 CPU 堆栈如果我记得的话,寄存器被用作基地址。
这就是为什么当我们有很长或无限的递归调用或循环时,我们很快就会发生堆栈溢出,而不会冻结现代计算机上的系统......
https://en.wikipedia.org/wiki/Memory_management
https://en.wikipedia.org/wiki/Stack_register
汇编语言资源:
感谢您进行了非常好的讨论,但作为一个真正的菜鸟,我想知道说明保存在哪里?在开始时,科学家们在两种架构之间做出选择(冯诺伊曼认为一切都被认为是数据,而哈佛则为指令保留一个内存区域,另一个用于数据)。最终,我们采用了冯诺依曼设计,现在一切都被认为是“相同的”。这让我在学习汇编时感到很困难 https://www.cs.virginia.edu/~evans/cs216/guides/x86.html 因为他们谈论寄存器和堆栈指针。
以上所有内容都在谈论数据。我的猜测是,由于指令是具有特定内存占用的已定义事物,因此它将进入堆栈,因此汇编中讨论的所有“那些”寄存器都在堆栈上。当然,随后出现了面向对象编程,指令和数据混合到一个动态的结构中,所以现在指令也将保留在堆上?
创建进程时,在加载代码和数据之后,操作系统设置堆在数据结束后开始,并根据体系结构堆栈到地址空间的顶部
当需要更多堆时,操作系统将动态分配并且堆块总是几乎连续的
请看brk()
,sbrk()
和alloca()
linux 中的系统调用
它们在哪里,是什么(物理上在真实计算机的内存中)?
回答: 两者都在 RAM 中。
在旁边:
RAM 就像一张桌子,而 HDD/SSD(永久存储)就像书架。要阅读任何东西,您必须在办公桌上打开一本书,并且您只能在办公桌上打开尽可能多的书。要拿到一本书,你可以从书架上把它拿出来,然后在你的桌子上打开。要归还书,您需要合上办公桌上的书并将其放回书架。
堆栈和堆是我们给编译器将不同类型的数据存储在同一位置(即 RAM 中)的两种方式的名称。
他们的范围是什么?
是什么决定了它们每个的大小?
是什么让一个更快?
回答:
堆栈用于静态(固定大小)数据
一种。在编译时,编译器会读取代码中使用的变量类型。
一世。它为这些变量分配固定数量的内存。
ii. 这个内存的大小不能增长。
湾。内存是连续的(单个块),因此访问 有时 比堆快
C。放置在堆栈上的对象在运行时在内存中增长超过堆栈大小会导致 堆栈溢出错误
堆用于动态(改变大小)数据
一种。
内存量仅受 RAM i中可用空间量的限制。
使用量可以在运行时根据需要增加或减少
湾。由于项目是通过在 RAM 中的任何位置找到空白空间来在堆上分配的,因此数据并不总是在连续的部分中,这 有时 会使访问速度比堆栈慢
C。程序员使用关键字手动将项目放入堆栈,new
并且必须在使用完毕后手动释放此内存。
一世。当不再需要内存泄漏时重复分配新内存而不释放它的代码。
在旁边:
堆栈和堆的引入主要不是为了提高速度;引入它们是为了处理内存溢出。关于使用堆栈与堆的第一个问题应该是是否会发生内存溢出。如果一个对象打算增长到未知数量(如链表或成员可以保存任意数量数据的对象),请将其放在堆上。尽可能使用 C++ 标准库 (STL) 容器vector、map和list,因为它们具有内存和速度效率,并且可以让您的生活更轻松(您无需担心内存分配/释放)。
在让你的代码运行之后,如果你发现它运行的慢得令人无法接受,那么回去重构你的代码,看看它是否可以更有效地编程。结果可能证明问题与堆栈或堆完全无关(例如,使用迭代算法而不是递归算法,查看 I/O 与 CPU 密集型任务,也许添加多线程或多处理)。
我在上面说有时更慢/更快,因为程序的速度可能与在堆栈或堆上分配的项目无关。
它们在多大程度上受操作系统或语言运行时的控制?
回答:
堆栈大小由编译器在编译时确定。
堆大小在运行时变化。(堆在运行时与操作系统一起工作以分配内存。)
在旁边:
下面是关于控制和编译时与运行时操作的更多信息。
每台计算机都有一个独特的指令集架构(ISA),即它的硬件命令(例如“MOVE”、“JUMP”、“ADD”等)。
操作系统只不过是一个资源管理器(控制如何/何时/在何处使用内存、处理器、设备和信息)。
操作系统的 ISA 称为裸机,其余命令称为扩展机。内核是扩展机的第一层。它控制诸如
当我们说“编译器”时,我们通常指的是编译器、汇编器和链接器
机器代码在执行时被传递给内核,它决定何时运行并获得控制权,但机器代码本身包含用于请求文件、请求内存等的 ISA 命令。因此代码发出 ISA 命令,但一切都必须通过由内核。