栈和堆是什么以及在哪里?
-
09-06-2019 - |
题
编程语言书籍解释说,值类型是在 堆, ,并且引用类型是在 堆, ,没有解释这两件事是什么。我还没有读过对此的明确解释。我明白什么 一个堆栈 是。但,
- 它们在哪里、是什么(在真实计算机的内存中)?
- 它们在多大程度上受操作系统或语言运行时的控制?
- 他们的范围是什么?
- 是什么决定了它们各自的大小?
- 是什么让一个人更快?
解决方案
堆栈是为执行线程留出的临时空间的内存。当调用函数时,会在堆栈顶部保留一个块用于局部变量和一些簿记数据。当该函数返回时,该块将变为未使用状态,并且可以在下次调用函数时使用。堆栈始终按照 LIFO(后进先出)顺序保留;最近保留的块始终是下一个要释放的块。这使得跟踪堆栈变得非常简单;从堆栈中释放一个块只不过是调整一个指针。
堆是为动态分配预留的内存。与堆栈不同,堆中块的分配和释放没有强制模式;您可以随时分配块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分已分配或空闲变得更加复杂;有许多自定义堆分配器可用于针对不同的使用模式调整堆性能。
每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管对于不同类型的分配有多个堆并不罕见)。
直接回答您的问题:
它们在多大程度上受操作系统或语言运行时的控制?
当线程创建时,操作系统为每个系统级线程分配堆栈。通常,操作系统由语言运行时调用来为应用程序分配堆。
他们的范围是什么?
堆栈附加到线程,因此当线程退出时,堆栈将被回收。堆通常在应用程序启动时由运行时分配,并在应用程序(技术上的进程)退出时回收。
是什么决定了它们各自的大小?
堆栈的大小是在创建线程时设置的。堆的大小是在应用程序启动时设置的,但可以随着空间的需要而增长(分配器从操作系统请求更多内存)。
是什么让一个人更快?
堆栈速度更快,因为访问模式使得从中分配和释放内存变得很简单(指针/整数只是递增或递减),而堆在分配或释放时涉及更复杂的簿记。此外,堆栈中的每个字节往往会非常频繁地重用,这意味着它往往会被映射到处理器的缓存,从而使其速度非常快。对堆的另一个性能影响是,堆主要是全局资源,通常必须是多线程安全的,即每次分配和释放通常需要与程序中的“所有”其他堆访问同步。
清晰的演示:
图片来源: vikashazrati.wordpress.com
其他提示
堆:
- 就像堆一样存储在计算机 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;
最重要的一点是,堆和堆栈是内存分配方式的通用术语。它们可以通过多种不同的方式实现,并且这些术语适用于基本概念。
在一堆物品中,物品按照放置顺序将一个放在另一个之上,并且您只能移除最上面的一个(而不能将整个物品推倒)。
堆栈的简单性在于,您不需要维护一个包含分配内存的每个部分的记录的表;您需要的唯一状态信息是指向堆栈末尾的单个指针。要分配和取消分配,只需递增和递减该单个指针即可。笔记:有时可以将堆栈实现为从内存部分的顶部开始并向下扩展而不是向上增长。
在堆中,项目的放置方式没有特定的顺序。您可以按任何顺序进入并删除项目,因为没有明确的“顶部”项目。
堆分配需要维护已分配内存和未分配内存的完整记录,以及一些开销维护以减少碎片、查找足够大的连续内存段以满足请求的大小,等等。内存可以随时释放,留下可用空间。有时,内存分配器会执行维护任务,例如通过移动分配的内存来对内存进行碎片整理,或者垃圾收集 - 在运行时识别内存何时不再在范围内并释放它。
这些图像应该很好地描述了在堆栈和堆中分配和释放内存的两种方式。嗯!
它们在多大程度上受操作系统或语言运行时的控制?
如前所述,堆和栈是通用术语,可以通过多种方式实现。计算机程序通常有一个称为堆栈 调用栈 它存储与当前函数相关的信息,例如指向调用该函数的函数的指针以及任何局部变量。由于函数调用其他函数然后返回,因此堆栈会增长和收缩以保存来自调用堆栈下方函数的信息。程序实际上并没有对其进行运行时控制;它是由编程语言、操作系统甚至系统架构决定的。
堆是一个通用术语,用于表示动态且随机分配的任何内存;IE。出故障了。内存通常由操作系统分配,应用程序调用 API 函数来执行此分配。管理动态分配的内存需要相当多的开销,这通常由操作系统处理。
他们的范围是什么?
调用堆栈是一个低级概念,与编程意义上的“范围”无关。如果反汇编一些代码,您将看到对堆栈部分的相对指针样式引用,但就高级语言而言,该语言强加了自己的范围规则。然而,堆栈的一个重要方面是,一旦函数返回,该函数本地的任何内容都会立即从堆栈中释放。鉴于您的编程语言的工作方式,这将按照您期望的方式工作。在堆中,也很难定义。范围是操作系统公开的任何内容,但您的编程语言可能会添加有关应用程序中“范围”的规则。处理器架构和操作系统使用虚拟寻址,处理器将其转换为物理地址,并且存在页面错误等。他们跟踪哪些页面属于哪些应用程序。不过,您永远不需要真正担心这一点,因为您只需使用编程语言使用的任何方法来分配和释放内存,并检查错误(如果分配/释放因任何原因失败)。
是什么决定了它们各自的大小?
同样,这取决于语言、编译器、操作系统和体系结构。堆栈通常是预先分配的,因为根据定义它必须是连续的内存(在最后一段中有更多介绍)。语言编译器或操作系统决定其大小。您不会在堆栈上存储大量数据,因此它会足够大,因此永远不应该完全使用它,除非出现不需要的无限递归(因此,“堆栈溢出”)或其他不寻常的编程决策。
堆是任何可以动态分配的东西的总称。根据你观察它的方式,它的大小在不断变化。在现代处理器和操作系统中,它的确切工作方式无论如何都是非常抽象的,因此您通常不需要太担心它的深层工作方式,除了(在它允许您的语言中)您不能使用以下内存:您尚未分配或已释放内存。
是什么让一个人更快?
堆栈速度更快,因为所有可用内存始终是连续的。不需要维护所有空闲内存段的列表,只需维护指向当前堆栈顶部的单个指针即可。编译器通常将此指针存储在一个特殊的、快速的 登记 以此目的。此外,堆栈上的后续操作通常集中在非常接近的内存区域内,这在非常低的级别上有利于处理器片上高速缓存的优化。
(我已将这个答案从另一个问题中移走,该问题或多或少是这个问题的欺骗。)
您问题的答案是特定于实现的,并且可能因编译器和处理器架构而异。不过,这里有一个简化的解释。
- 堆栈和堆都是从底层操作系统分配的内存区域(通常是按需映射到物理内存的虚拟内存)。
- 在多线程环境中,每个线程都有自己完全独立的堆栈,但它们将共享堆。并发访问必须在堆上进行控制,而在堆栈上则不可能。
堆
- 堆包含已用块和空闲块的链接列表。堆上的新分配(通过
new
或者malloc
)通过从空闲块之一创建合适的块来满足。这需要更新堆上的块列表。这 元信息 关于堆上的块也通常存储在堆上每个块前面的一个小区域中。 - 随着堆的增长,新块通常从较低地址向较高地址分配。因此,您可以将堆视为 堆 内存块的大小随着内存的分配而增加。如果堆对于分配来说太小,通常可以通过从底层操作系统获取更多内存来增加大小。
- 分配和释放许多小块可能会使堆处于一种状态,即在已使用的块之间散布着许多小的空闲块。分配大块的请求可能会失败,因为即使空闲块的组合大小可能足够大,但没有一个空闲块大到足以满足分配请求。这就是所谓的 堆碎片.
- 当与空闲块相邻的已用块被释放时,新的空闲块可以与相邻的空闲块合并以创建更大的空闲块,有效地减少堆的碎片。
堆栈
- 堆栈通常与 CPU 上名为 堆栈指针. 。最初,堆栈指针指向堆栈顶部(堆栈上的最高地址)。
- CPU有专门的指令 推动 值入栈并且 爆裂 他们从堆栈中返回。每个 推 将值存储在堆栈指针的当前位置并减少堆栈指针。A 流行音乐 检索堆栈指针指向的值,然后增加堆栈指针(不要被以下事实所迷惑) 添加 一个值到堆栈 减少 堆栈指针和 去除 一个值 增加 它。请记住,堆栈会增长到底部)。存储和检索的值是 CPU 寄存器的值。
- 当一个函数被调用时,CPU 使用特殊指令来推送当前的 指令指针, , IE。在堆栈上执行的代码的地址。然后,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++ 等语言中,当您不处理指针时,结构和类通常可以保留在堆栈中。
更多信息可以在这里找到:
和这里:
本文为上图来源: 六个重要的 .NET 概念:堆栈、堆、值类型、引用类型、装箱和拆箱 - CodeProject
但请注意,它可能包含一些不准确之处。
堆栈当您调用函数时,该函数的参数加上一些其他开销都会放在堆栈上。一些信息(例如返回时去哪里)也存储在那里。当您在函数内声明变量时,该变量也会在堆栈上分配。
解除分配堆栈非常简单,因为您总是以与分配相反的顺序解除分配。当您进入函数时,堆栈内容会添加,当您退出函数时,相应的数据将被删除。这意味着您倾向于停留在堆栈的一个小区域内,除非您调用许多函数,而这些函数又调用许多其他函数(或创建递归解决方案)。
堆堆是放置动态创建的数据的通用名称。如果您不知道您的程序将创建多少艘宇宙飞船,您可能会使用 new (或 malloc 或等效)运算符来创建每艘宇宙飞船。这种分配将持续一段时间,因此我们很可能会以与创建它们不同的顺序来释放它们。
因此,堆要复杂得多,因为最终会出现未使用的内存区域与内存碎片交错的块。找到所需大小的可用内存是一个难题。这就是为什么应该避免使用堆(尽管它仍然经常使用)。
执行堆栈和堆的实现通常取决于运行时/操作系统。通常,游戏和其他对性能至关重要的应用程序会创建自己的内存解决方案,从堆中获取大量内存,然后在内部分配出去,以避免依赖操作系统的内存。
仅当您的内存使用情况与正常情况有很大不同时,这才实用 - 即,对于在一个巨大的操作中加载关卡并可以在另一个巨大的操作中丢弃全部内容的游戏。
内存中的物理位置由于一项名为“ 虚拟内存 这使您的程序认为您可以访问物理数据位于其他位置(甚至在硬盘上!)的某个地址。随着调用树的加深,您获得的堆栈地址将按递增顺序排列。堆的地址是不可预测的(即特定于实现的)并且坦率地说并不重要。
澄清, 这个答案 有不正确的信息(托马斯 在评论后修正了他的答案,酷:))。其他答案只是避免解释静态分配的含义。因此,我将在下面解释三种主要的分配形式以及它们通常如何与堆、堆栈和数据段相关。我还将展示一些 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 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 中,您可以通过使用以下方式获得可变长度分配的好处 分配, ,它在堆栈上分配,而不是 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()
或者 sbrk()
系统调用,将更多物理内存页映射到进程的虚拟地址空间。
在没有虚拟内存的系统中,例如一些嵌入式系统,通常应用相同的基本布局,只是堆栈和堆的大小是固定的。然而,在其他嵌入式系统(例如基于 Microchip PIC 微控制器的系统)中,程序堆栈是一个单独的内存块,无法通过数据移动指令寻址,只能通过程序流指令(call、返回等)。其他架构,例如 Intel Itanium 处理器,具有 多个堆栈. 。从这个意义上说,堆栈是CPU架构的一个元素。
堆栈是内存的一部分,可以通过几个关键的汇编语言指令进行操作,例如“pop”(从堆栈中删除并返回一个值)和“push”(将一个值推入堆栈),但也可以调用(调用子例程 - 这将地址压入堆栈)并返回(从子例程返回 - 这将地址从堆栈中弹出并跳转到该地址)。它是堆栈指针寄存器下方的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子例程,以及在调用子例程之前保存寄存器中的值。
堆是操作系统通常通过 malloc 等系统调用提供给应用程序的内存的一部分。在现代操作系统上,该内存是一组只有调用进程才能访问的页面。
堆栈的大小在运行时确定,并且通常在程序启动后不会增长。在 C 程序中,堆栈需要足够大才能容纳每个函数中声明的每个变量。堆将根据需要动态增长,但操作系统最终会进行调用(它通常会使堆增长超过 malloc 请求的值,因此至少某些未来的 malloc 不需要返回内核来获得更多内存。此行为通常是可定制的)
因为您在启动程序之前就已经分配了堆栈,所以在使用堆栈之前不需要进行 malloc,因此这是一个小小的优势。实际上,在具有虚拟内存子系统的现代操作系统中,很难预测什么会快、什么会慢,因为页面的实现方式以及它们的存储位置是一个实现细节。
我认为很多其他人在这个问题上已经给了你大部分正确的答案。
然而,一个被忽略的细节是“堆”实际上应该被称为“自由存储”。这种区别的原因是原始的免费商店是用称为“二项式堆”的数据结构实现的。因此,从Malloc()/free()的早期实施中分配的是堆分配。然而,在当今时代,大多数免费商店都是使用非常复杂的数据结构来实现的,而不是二项式堆。
什么是堆栈?
堆栈是一堆对象,通常是整齐排列的对象。
计算架构中的堆栈是内存区域,其中数据以后进先出的方式添加或删除。
在多线程应用程序中,每个线程都有自己的堆栈。
什么是堆?
堆是杂乱地堆积起来的杂乱的东西的集合。
在计算架构中,堆是动态分配的内存区域,由操作系统或内存管理器库自动管理。
在程序执行期间,堆上的内存会定期分配、释放和调整大小,这可能会导致称为碎片的问题。
当内存对象之间分配的空间太小而无法容纳额外的内存对象时,就会发生碎片。
最终结果是不可用于进一步内存分配的堆空间的百分比。
两者一起
在多线程应用程序中,每个线程都有自己的堆栈。但是,所有不同的线程将共享堆。
因为不同的线程在多线程应用程序中共享堆,这也意味着线程之间必须有一些协调,以便它们不会尝试访问和操作堆中的相同内存块同一时间。
堆栈和堆哪个更快?为什么?
堆栈比堆快得多。
这是因为在堆栈上分配内存的方式。
在堆栈上分配内存就像向上移动堆栈指针一样简单。
对于刚接触编程的人来说,使用堆栈可能是个好主意,因为它更容易。
由于堆栈很小,因此当您确切知道数据需要多少内存,或者知道数据的大小非常小时,您可能会想要使用它。
当您知道数据需要大量内存,或者您只是不确定需要多少内存(例如动态数组)时,最好使用堆。
Java内存模型
堆栈是存储局部变量(包括方法参数)的内存区域。当涉及对象变量时,它们只是堆上实际对象的引用(指针)。
每次实例化一个对象时,都会留出一块堆内存来保存该对象的数据(状态)。由于对象可以包含其他对象,因此其中一些数据实际上可以保存对这些嵌套对象的引用。
您可以使用堆栈做一些有趣的事情。例如,您有类似的功能 分配 (假设您可以克服有关其使用的大量警告),这是一种 malloc 形式,专门使用堆栈(而不是堆)作为内存。
也就是说,基于堆栈的内存错误是我经历过的最严重的错误之一。如果您使用堆内存,并且超出了分配块的范围,则很有可能触发段错误。(不是 100%:但由于在堆栈上创建的变量始终彼此连续,因此越界写入可能会更改另一个变量的值。我了解到,每当我感觉我的程序不再遵守逻辑定律时,很可能是缓冲区溢出。
简而言之,堆栈是创建局部变量的地方。此外,每次调用子例程时,程序计数器(指向下一条机器指令的指针)和任何重要的寄存器,有时参数都会被推送到堆栈上。然后,子例程内的任何局部变量都被推入堆栈(并从那里使用)。当子例程完成时,这些内容都会从堆栈中弹出。PC 和寄存器数据在弹出时获取并放回到原来的位置,这样您的程序就可以顺利运行。
堆是动态内存分配的内存区域(显式“new”或“allocate”调用)。它是一种特殊的数据结构,可以跟踪不同大小的内存块及其分配状态。
在“经典”系统中,RAM 的布局使得堆栈指针从内存底部开始,堆指针从顶部开始,并且它们相互增长。如果它们重叠,则表明 RAM 不足。但这不适用于现代多线程操作系统。每个线程都必须有自己的堆栈,并且这些堆栈可以动态创建。
来自维基百科。
堆
当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数时,所有这些函数的执行将保持挂起状态,直到最后一个函数返回其值。
这个挂起的函数调用链就是堆栈,因为堆栈中的元素(函数调用)相互依赖。
在异常处理和线程执行中,堆栈是需要考虑的重要因素。
堆
堆只是程序用来存储变量的内存。堆中的元素(变量)彼此之间没有依赖关系,并且可以随时随机访问。
堆
- 访问速度非常快
- 不必显式取消分配变量
- 空间由CPU有效管理,内存不会变得碎片
- 仅局部变量
- 堆栈大小限制(取决于操作系统)
- 变量不能调整大小
堆
- 变量可以全局访问
- 内存大小没有限制
- (相对)访问速度较慢
- 无法保证空间的有效利用,随着内存块的分配和释放,内存可能会随着时间的推移而变得碎片化
- 您必须管理内存(您负责分配和释放变量)
- 可以使用 realloc() 调整变量的大小
简而言之
堆栈用于静态内存分配,堆用于动态内存分配,两者都存储在计算机的 RAM 中。
详细
堆栈
堆栈是一种“LIFO”(后进先出)数据结构,由 CPU 密切管理和优化。每次函数声明一个新变量时,它都会被“推”到堆栈上。然后,每次函数退出时,该函数压入堆栈的所有变量都会被释放(也就是说,它们被删除)。一旦堆栈变量被释放,该内存区域就可用于其他堆栈变量。
使用堆栈存储变量的优点是可以为您管理内存。您不必手动分配内存,也不必在不再需要时释放它。更重要的是,由于 CPU 如此高效地组织堆栈内存,因此读取和写入堆栈变量的速度非常快。
可以找到更多 这里.
堆
堆是计算机内存中的一个区域,不会为您自动管理,也不会受到 CPU 的严格管理。它是一个更自由浮动的内存区域(并且更大)。要在堆上分配内存,必须使用 malloc() 或 calloc(),它们是内置的 C 函数。一旦您在堆上分配了内存,您就需要在不再需要该内存时使用 free() 来释放该内存。
如果您不这样做,您的程序将出现所谓的内存泄漏。也就是说,堆上的内存仍将被保留(并且不会可供其他进程使用)。正如我们将在调试部分看到的,有一个工具叫做 瓦尔格林德 这可以帮助您检测内存泄漏。
与堆栈不同,堆对变量大小没有大小限制(除了计算机明显的物理限制之外)。堆内存的读取和写入速度稍慢,因为必须使用指针来访问堆上的内存。我们很快就会讨论指针。
与堆栈不同,在堆上创建的变量可以由程序中任何位置的任何函数访问。堆变量本质上是全局范围的。
可以找到更多 这里.
在堆栈上分配的变量直接存储到内存中,访问该内存的速度非常快,并且其分配是在程序编译时处理的。当一个函数或方法调用另一个函数,而另一个函数又调用另一个函数时,所有这些函数的执行将保持挂起状态,直到最后一个函数返回其值。堆栈始终按 LIFO 顺序保留,最近保留的块始终是下一个要释放的块。这使得跟踪堆栈变得非常简单,从堆栈中释放一个块只不过是调整一个指针。
在堆上分配的变量在运行时分配内存,访问该内存会慢一些,但堆大小仅受虚拟内存大小的限制。堆中的元素彼此之间没有依赖关系,并且可以随时随机访问。您可以随时分配块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分已分配或空闲变得更加复杂。
如果您在编译前确切知道需要分配多少数据,并且不是太大,则可以使用堆栈。如果您不确切知道运行时需要多少数据或者需要分配大量数据,则可以使用堆。
在多线程情况下,每个线程都有自己完全独立的堆栈,但它们将共享堆。堆栈是特定于线程的,堆是特定于应用程序的。在异常处理和线程执行中,堆栈是需要考虑的重要因素。
每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管对于不同类型的分配有多个堆并不罕见)。
在运行时,如果应用程序需要更多堆,它可以从空闲内存中分配内存,如果堆栈需要内存,它可以从空闲内存中为应用程序分配内存。
现在来到 你的问题的答案.
它们在多大程度上受操作系统或语言运行时的控制?
当线程创建时,操作系统为每个系统级线程分配堆栈。通常,操作系统由语言运行时调用来为应用程序分配堆。
可以找到更多 这里.
他们的范围是什么?
已经在上面给出了。
“如果您在编译前确切知道需要分配多少数据,并且不是太大,则可以使用堆栈。如果您不确切知道运行时需要多少数据或者需要分配大量数据,则可以使用堆。”
更多内容可以在 这里.
是什么决定了它们各自的大小?
堆栈的大小由下式设置 操作系统 当创建一个线程时。堆的大小是在应用程序启动时设置的,但它可以随着空间的需要而增长(分配器从操作系统请求更多内存)。
是什么让一个人更快?
堆栈分配速度要快得多,因为它实际上所做的只是移动堆栈指针。使用内存池,您可以从堆分配中获得相当的性能,但这会稍微增加复杂性并带来一些麻烦。
另外,堆栈与堆栈的比较堆不仅仅是性能考虑;它还告诉您很多有关对象的预期寿命的信息。
详细信息可以从 这里.
在 20 世纪 80 年代,UNIX 像兔子一样传播开来,大公司也推出了自己的操作系统。埃克森美孚拥有一个,还有数十个消失在历史中的品牌名称。内存的布局方式由许多实现者自行决定。
一个典型的C程序在记忆中布置平面,并有机会通过更改BRK()值来增加。通常,堆仅在此BRK值以下,而增加的BRK增加了可用堆的量。
单个堆栈通常是堆下方的区域,直到下一个固定的内存块的顶部,它是一块内存的一块内存。下一个块通常是代码,可以在其时代著名的黑客之一中被堆栈数据覆盖。
一个典型的内存块是BSS(零值的块),在一个制造商的产品中未意外将其归零。另一个是包含初始化值的数据,包括字符串和数字。第三个是包含 CRT(C 运行时)、main、函数和库的 CODE。
UNIX 中虚拟内存的出现改变了许多限制。没有客观的理由为什么这些块需要连续或固定在大小上,或者现在以特定的方式订购了这些块。当然,在 UNIX 之前的 Multics 并没有受到这些限制。这是显示那个时代的内存布局之一的示意图。
由于有些答案很挑剔,我将贡献我的一点力量。
令人惊讶的是,没有人提到过这个倍数(即与正在运行的操作系统级线程的数量无关)调用堆栈不仅可以在外来语言(PostScript)或平台(Intel Itanium)中找到,而且还可以在 纤维, 绿线 以及一些实现 协程.
纤维、绿色线程和协程在很多方面都很相似,这导致了很多混乱。纤程和绿色线程之间的区别在于,前者使用协作式多任务处理,而后者可能具有协作式或抢占式(甚至两者兼而有之)的特点。关于纤程和协程的区别,请参见 这里.
无论如何,纤程、绿色线程和协程的目的都是让多个函数同时执行,但是 不是 并行(参见 这个问题 为了区别)在单个操作系统级线程中,以一种有组织的方式来回传输控制。
当使用纤程、绿色线程或协程时,您 通常 每个函数都有一个单独的堆栈。(从技术上讲,每个函数不仅仅是一个堆栈,而是整个执行上下文。最重要的是,CPU 寄存器。)对于每个线程,都有与并发运行的函数一样多的堆栈,并且线程根据程序的逻辑在执行每个函数之间切换。当函数运行到结束时,其堆栈将被销毁。所以, 堆栈的数量和寿命 是动态的并且 不是由操作系统级线程的数量决定的!
请注意,我说的是“通常 每个函数都有一个单独的堆栈”。两者都有 堆满的 和 无堆栈 协程的实现。最值得注意的 stackful C++ 实现是 Boost.协程 和 微软PPL的 async/await
. 。(但是,C++ 的 可恢复功能 (又名”async
和 await
”),这是向 C++17 提出的,很可能使用无堆栈协程。)
Fibers 对 C++ 标准库的提案即将推出。另外还有一些第三方的 图书馆. 。绿色线程在 Python 和 Ruby 等语言中非常流行。
我有一些东西要分享,尽管要点已经涵盖了。
堆
- 访问速度非常快。
- 存储在RAM中。
- 函数调用与传递的局部变量和函数参数一起加载到此处。
- 当程序超出范围时,空间会自动释放。
- 存储在顺序存储器中。
堆
- 访问堆栈相对较慢。
- 存储在RAM中。
- 动态创建的变量存储在这里,稍后需要在使用后释放分配的内存。
- 存储在内存分配完成的地方,始终通过指针访问。
有趣的注释:
- 如果函数调用存储在堆中,则会导致两个混乱点:
- 由于堆栈中的顺序存储,执行速度更快。存储在堆中会导致大量的时间消耗,从而使整个程序执行速度变慢。
- 如果函数存储在堆中(指针指向的混乱存储),则无法返回调用者地址(由于内存中的顺序存储而导致堆栈给出)。
许多答案作为概念是正确的,但我们必须注意硬件需要堆栈(即微处理器)以允许调用子例程(汇编语言中的 CALL..)。(OOP 的人会称之为 方法)
在堆栈上,您保存返回地址并调用→push / ret→pop直接在硬件中管理。
可以使用栈来传递参数。即使它比使用寄存器慢(微处理器专家或 20 世纪 80 年代的 BIOS 书籍会说......)
- 不带堆栈 不 微处理器可以工作。(我们无法想象一个没有子例程/函数的程序,即使是汇编语言)
- 没有堆也可以。(汇编语言程序可以在没有堆的情况下工作,因为堆是一个操作系统概念,就像 malloc 一样,这是一个操作系统/库调用。
堆栈使用速度更快:
- 是硬件,甚至push/pop都非常高效。
- malloc 需要进入内核模式,使用锁/信号量(或其他同步原语)执行一些代码并管理一些跟踪分配所需的结构。
哇!这么多答案,我认为没有一个是正确的......
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