memory-management - 什么是堆栈,它在哪里?

  显示原文与译文双语对照的内容

编程语言书籍解释了值类型是在堆栈上创建的,引用类型是在堆上创建的,而不解释这两个东西是什么。 我还没有阅读清楚的解释。 我理解堆栈是什么,但和他们( 实际的内存中) 电脑?

  • 在什么程度上由操作系统或者语言运行时控制?
  • 他们的范围是什么?
  • 什么决定了它们的大小?
  • 什么使一个更快?
时间:

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

堆的内存为动态分配而预留。 堆栈不同,从堆分配和释放块没有强制模式;你可以随时分配块并在任何时候释放它。 这使得跟踪堆的哪些部分在任何给定时间分配或者释放变得更加复杂;有许多自定义堆分配器可以用于调优不同使用模式的堆性能。

每个线程获取一个堆栈,而应用程序( 虽然对于不同的分配类型有多个堆是不常见的) 通常只有一个堆。

要直接回答你的问题:

什么程度上由操作系统或者语言运行时控制?

在创建线程时,操作系统为每个system-level线程分配堆栈。 通常,操作系统调用操作系统来为应用程序分配堆。

他们的范围是什么?

堆栈被附加到一个线程,因此当线程退出堆栈时。 堆通常在运行时由运行时分配,并在应用程序( 工艺流程) 退出时回收。

什么决定了它们的大小?

创建线程时设置堆栈的大小。 堆的大小在应用程序启动时设置,但可以随着需要的空间而增长。

什么使一个更快?

堆栈更快,因为访问模式使得分配和释放来自它的内存变得微不足道,而堆在分配或者释放时涉及更复杂的簿记。 此外,堆栈中的每个字节都经常被重用,这意味着它倾向于映射到处理器的缓存,使得它非常快。 另一个的堆堆的性能下降,主要是全球资源,通常必须multi-threading安全, 换句话说,每个分配和回收需要——通常与"全部"同步其他堆访问计划。

一个清晰的演示:


图像源:vikashazrati.wordpress.com

磅:

  • 存储在计算机内存中,就像堆。
  • 在堆栈上创建的变量将超出范围并自动释放。
  • 堆上的变量相比,分配更快。
  • 使用实际堆栈数据结构实现。
  • 存储本地数据,返回地址,用于参数传递
  • 当堆栈过多时,堆栈溢出。 ( 大部分来自无限( 或者太多) 递归,非常大的分配)
  • 在堆栈上创建的数据可以不用指针。
  • 如果你知道在编译时间之前需要分配多少数据,而不是太大,那么就使用堆栈。
  • 通常在程序启动时已经确定了最大大小

堆:

  • 存储在计算机内存中,就像堆栈一样。
  • 堆上的变量必须手动销毁并且从不超出作用域。 数据被删除,删除 [] 或者释放
  • 堆栈上的变量相比,分配速度较慢。
  • 按需分配数据块供程序使用。
  • 当有大量分配和释放时,可以有碎片
  • 在堆上创建的C++ 数据将通过指针指向,并用新的或者 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;

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

  • 在一组项目中,项按放置在其中的顺序放在另一个之上,你只能删除顶部的( 不用把整个东西都翻过来) 。

    Stack like a stack of papers

  • 在堆中,对项目的放置没有特殊顺序。 你可以按任意顺序进入和删除项目,因为没有明确的'顶端'项目。

    Heap like a heap of licorice allsorts

它很好地描述了在堆栈和堆中分配和释放内存的两种方法。 Yum !

  • 在什么程度上由操作系统或者语言运行时控制?

    如上所述,堆和堆栈是通用术语,可以通过多种方式实现。 计算机程序通常有一个称为调用堆栈的堆栈,它存储与当前函数相关的信息,比如指向任何函数的指针,以及任何局部变量。 由于函数调用其他函数然后返回,堆栈会增长并收缩,以便在调用堆栈中进一步保留函数的信息。 一个程序实际上没有运行时控制;它由编程语言,操作系统甚至系统架构决定。

    堆是用于动态和随机分配的任何内存的通用术语;换句话说,越界。 内存通常由操作系统分配,应用程序调用API函数来执行这个分配。 管理动态分配的内存需要相当多的开销,它通常由操作系统处理。

  • 他们的范围是什么?

    调用堆栈是一个低级概念,它在编程方面与'作用域'无关。 如果你反汇编一些代码,你会看到相对指针样式引用到堆栈的一部分,但是,在更高级的语言中,语言会强加自己的作用域规则。 堆栈的一个重要方面是,一旦函数返回,这个函数的任何地方都会立即从堆栈中释放。 当你的编程语言工作的时候,你就会觉得它的工作方式。 在堆中,也很难定义。 范围是由操作系统公开的,但是你的编程语言可能会在你的应用程序中添加关于"作用域"的规则。 处理器架构和操作系统使用虚拟地址,处理器将它的转换成物理地址,并有页面错误,等等 跟踪哪些页面属于哪个应用程序。 但是,你永远也不用担心这个问题,因为你只使用编程语言用来分配和释放内存的任何方法,并检查错误( 如果分配/释放失败,任何原因) 。

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

    同样,它依赖于语言,编译器,操作系统和架构。 堆栈通常是 pre-allocated,因为按照定义,它必须是连续的内存( 更多关于最后一段的内容) 。 语言编译器或者操作系统决定它的大小。 你没有在堆栈上存储大量数据,因此它将足够大,除非在不需要的无限递归( 因此,"堆栈溢出") 或者其他异常编程决策中不被充分使用。

    堆是可以动态分配的任何东西的总称。 根据你看待它的方式,它不断改变大小。 在现代处理器和操作系统中,它的工作方式是非常抽象的,所以你通常不需要担心它的工作方式,除非( 使用它可以让你) 不能使用你还没有分配的内存或者你已经释放的内存。

  • 什么使一个更快?

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

( 我把这个答案从另一个问题中移了过来,它或多或少是一个问题。)

你的问题的答案是实现特定的,并且可能不同于编译器和处理器架构。 然而,下面是一个简化的解释。

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

  • 堆包含已经使用和可用块的链接列表。 通过从一个可用块中创建一个合适的块,可以满足堆( 按 new 或者 malloc ) 上的新分配。 这需要更新堆上的块列表。 这元信息块堆也存储在堆上通常只在一个小区域的每一块。
  • 随着堆的增长,新的块通常从低地址分配到更高地址。 因此你可以把堆的堆生长在大小的内存块内存分配。 如果堆太小,对于分配来说,通常可以通过从底层操作系统获取更多内存来增加大小。
  • 分配和解除分配许多小块可能会使堆处于一个状态,在这个状态中,使用的块之间会有大量空闲块。 分配一个大块的请求可能会失败,因为任何可用块都不足以满足分配请求,即使可用块的组合大小可能足够大。 这叫做堆碎片 。
  • 当释放与自由块相邻的使用块时,可以将新的自由块与相邻可用块合并,从而有效地创建一个较大的自由块,从而有效地减少堆的碎片。

The heap

堆栈

  • 堆栈通常与一个名为堆栈指针的CPU上的特殊寄存器紧密相连。 堆栈指针最初指向堆栈( 堆栈上的最高地址)的顶部。
  • cpu有特殊指令推动值压入堆栈和回去从堆栈弹出。 每个推动存储堆栈指针的值在当前位置和降低了堆栈指针。 流行检索价值指向堆栈指针,然后增加堆栈指针(不要混淆,值添加到堆栈减少堆栈指针和移除 增加它的值。 记住堆栈会增长到底部。 存储和检索的值是CPU寄存器的值。
  • 当一个函数被调用时,CPU使用特殊的指令来推动当前的指令指针,换句话说,在堆栈上执行代码的地址。 然后,CPU通过将指令指针设置为函数的地址来跳转到函数。 稍后,当函数返回时,旧的指令指针从堆栈中弹出,执行在调用函数之后的代码处恢复。
  • 当输入一个函数时,堆栈指针会被减少,以便在堆栈上分配更多的空间,用于局部( 自动) 变量。 如果函数有一个本地 32位变量,则堆栈上将留出4 字节。 当函数返回时,堆栈指针被移回到释放分配的区域。
  • 如果函数有参数,在调用函数之前将它们推送到堆栈上。 函数中的代码可以从当前堆栈指针向上导航到堆栈,以定位这些值。
  • 嵌套函数调用就像一个魅力。 以正确的方式return,每一个新的函数时调用将分配函数的参数,所以返回地址和空间存放局部变量和这些活动记录为嵌套的调用并且 unwind. 可以叠加
  • 堆栈是一种有限的内存块,你可以通过调用太多导致堆栈溢出嵌套函数和/或局部变量分配太多的空间。 通常,堆栈使用的内存区域是这样设置的,在堆栈的底部( 最低地址) 下写入会触发一个陷阱或者异常。 这个异常条件可以被运行时捕获并转换为某种堆栈溢出异常。

The stack

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

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

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

然而,堆栈是一个与处理器架构紧密联系的低级特性。 在没有足够空间的情况下增长堆并不难,因为它可以在处理堆的库调用中实现。 然而,堆栈增长往往是不可能的,因为堆栈溢出在它为时已晚时才会被发现;并且关闭执行线程是唯一可行的选项。

在以下 C# 代码中


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

以下是如何管理内存的方法

Picture of variables on the stack

Local Variables,只要函数调用进入堆栈,就需要最后的时间。 变量使用的堆的一生我们预先不知道,但我们希望他们持续一段时间。 在大多数语言中,是至关重要的,我们知道在编译时多大一个变量是如果我们想将其存储在堆栈上。

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

在Java中,大多数对象直接进入堆。 在语言如c/C++ 、结构和类通常可以留在堆栈当你不处理指针。

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

还有这里:

在堆栈和堆列表中创建对象

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

但请注意,它可能包含一些不准确的地方。

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

正在释放堆栈非常简单,因为你总是按照分配的相反顺序释放。 在输入函数时添加堆栈内容,当你退出时,相应的数据将被删除。 这意味着你倾向于在堆栈的一个小区域内,除非调用大量调用其他函数的函数( 或者创建递归解决方案) 。

堆是一个通用名称,你把你创建的数据。 如果你不知道你的程序将要创建多少个 spaceships,你可能会使用新的( 或者malloc或者等价) 操作符来创建每个宇宙飞船。 这个分配会持续一段时间,所以我们可能会以不同于我们创建的顺序释放东西。

因此,堆要复杂的多,因为最终的内存区域未使用交叉的块,记忆变得支离破碎。 寻找你需要的大小的空闲内存是一个难题。 这就是为什么应该避免( 虽然它还经常被使用)的原因。

堆栈和堆的实现实现通常是运行时/操作系统。 游戏和其他性能关键的应用程序会创建自己的内存解决方案,它们会从堆中抓取大量内存,然后在内部取出来避免依赖内存。

这是唯一可行的,如果你的内存使用是完全不同于常态—— 例如 游戏加载水平在一个巨大的操作,可以扔掉了很多在另一个巨大的操作。

内存中的物理位置这比你觉得不那么重要,因为技术虚拟内存使程序认为你获得某种别的地方( 即使在硬盘上) 地址的物理数据。 当你的调用树越来越深的时候,你为栈获取的地址会越来越长。 堆的地址是 un-predictable ( 例如 implimentation特定),坦率地说不重要。

有些人回答得很好,所以我将给出一些细节。

  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.
}

为了澄清,这个的答案有不正确的信息( 在评论后修正了他的回答,冷静:) 。 其他的答案只是避免解释静态分配意味着什么。 所以我将解释三种主要的分配形式以及它们通常与堆,堆栈和数据段的关系。 我还将在 python/cpp和中展示一些例子来帮助人们理解。

"静态"( 静态分配的) 变量不能在堆栈上分配。 不要这么做- 很多人只是因为"静态"听起来像"堆栈"。 它们实际上既不存在于堆栈中,也不存在于堆中。 是数据段的一部分。

然而,通常最好考虑"磅范围"和"终身"而不是"堆栈"和"堆"。

作用域指代码的哪些部分可以访问一个变量。 通常我们认为局部范围 ( 只能由当前函数访问) 与全球范围 ( 可以在任何地方访问) 虽然范围可以更复杂。

生命周期指在程序执行期间分配和释放变量的时间。 通常我们认为静态分配 ( 变量将持续整个程序的持续时间,使得它可以在多个函数调用中存储相同的信息) 与自动分配 ( 变量只持续在一个调用一个函数,使其可用于存储信息只在你使用的功能,可以丢弃一旦你完成) 与动态分配 ( 在运行时定义的变量,而不是像静态或者自动的编译时间) 。

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

我将提供一些简单注释的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 b/w 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/cpp中的一些语法选择加剧了这个问题- 例如许多人认为全局变量不是"静态",因为下面显示的语法。


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

int main() {return 0;}

请注意,在上面的声明中放置关键字"静态"可以防止var2全局作用域。 然而,全局var1有静态分配。 这一点也不直观因为这个原因,我试图在描述范围时从不使用"静态",而是在"文件"或者"文件受限"作用域中使用。 然而很多人使用"静态"或者"静态作用域"来描述一个变量,它只能从一个代码文件中访问。 在生命周期的上下文中,"静态"总是在程序启动时被分配,当程序退出时释放。

有些人认为这些概念是/cpp 。 没有。例如下面的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

其他人直接回答了你的问题,但是当你试图理解堆栈和堆时,我认为考虑传统的UNIX进程( 没有线程和 mmap() -based分配器)的内存布局是有帮助的。 内存管理词汇表 web页面具有这里内存布局的图表。

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

在没有虚拟内存的系统中,比如一些嵌入式系统,相同的基本布局通常会应用,除非堆栈和堆被固定在大小上。 但是,在其他嵌入式系统( 例如基于单片机的微控制器) 中,程序堆栈是一个单独的内存块,不能通过数据移动指令进行寻址,并且只能通过程序流指令( 调用,返回,等等 ) 进行修改或者间接读取。 其他架构,比如 Intel Itanium处理器,拥有多个栈。 在这个意义上,堆栈是CPU架构的一个元素。

我认为很多其他人都给了你正确的答案。

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

...