编程语言书籍解释了在类型上创建的值类型 堆,并在上创建引用类型 堆,没有解释这两件事是什么。我还没有看到对此的明确解释。我明白了什么 一堆 是。但,
- 它们在哪里和它们(物理上在真实计算机的记忆中)?
- 它们在多大程度上受操作系统或语言运行时控制?
- 它们的范围是什么?
- 是什么决定了它们的大小?
- 是什么让一个更快?
编程语言书籍解释了在类型上创建的值类型 堆,并在上创建引用类型 堆,没有解释这两件事是什么。我还没有看到对此的明确解释。我明白了什么 一堆 是。但,
堆栈是作为执行线程的临时空间留出的内存。调用函数时,在堆栈顶部保留一个块,用于本地变量和一些簿记数据。当该函数返回时,该块将变为未使用状态,并可在下次调用函数时使用。堆栈始终以LIFO(后进先出)顺序保留;最近保留的块始终是要释放的下一个块。这使得跟踪堆栈非常简单;从堆栈中释放块只不过是调整一个指针。
堆是为动态分配留出的内存。与堆栈不同,堆中的块的分配和释放没有强制模式;您可以随时分配一个块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分被分配或释放变得更加复杂;有许多自定义堆分配器可用于调整不同使用模式的堆性能。
每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管为不同类型的分配设置多个堆并不罕见)。
直接回答您的问题:
它们在多大程度上受操作系统或语言运行时控制?
操作系统在创建线程时为每个系统级线程分配堆栈。通常,语言运行库调用OS来为应用程序分配堆。
它们的范围是什么?
堆栈附加到一个线程,因此当线程退出堆栈时将被回收。堆通常在应用程序启动时由运行时分配,并在应用程序(技术过程)退出时回收。
是什么决定了它们的大小?
创建线程时设置堆栈的大小。堆的大小在应用程序启动时设置,但可以在需要空间时增长(分配器从操作系统请求更多内存)。
是什么让一个更快?
堆栈更快,因为访问模式使得从中分配和释放内存变得微不足道(指针/整数简单地递增或递减),而堆在分配或释放中涉及更复杂的簿记。此外,堆栈中的每个字节都经常被频繁地重用,这意味着它往往被映射到处理器的缓存,使其非常快。堆的另一个性能损失是堆(主要是全局资源)通常必须是多线程安全的,即每个分配和释放需要 - 通常 - 与程序中的“所有”其他堆访问同步。
明确的示范:
图片来源: vikashazrati.wordpress.com
堆栈:
堆:
delete
, delete[]
, 要么 free
。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;
最重要的一点是堆和堆栈是可以分配内存的通用术语。它们可以以多种不同的方式实现,并且这些术语适用于基本概念。
在一堆物品中,物品按照它们放置在那里的顺序一个在另一个上面,你只能移除顶部的物品(不会翻倒整个物品)。
堆栈的简单性在于您不需要维护包含已分配内存的每个部分的记录的表;您需要的唯一状态信息是指向堆栈末尾的单个指针。要分配和取消分配,只需递增和递减该单个指针即可。注意:有时可以实现堆栈从一部分内存的顶部开始并向下延伸而不是向上扩展。
在堆中,对项目的放置方式没有特定的顺序。您可以按任意顺序进入和移除商品,因为没有明确的“顶部”商品。
堆分配需要维护已分配内存和不分配内存的完整记录,以及减少碎片的一些开销维护,找到足以满足请求大小的连续内存段,等等。内存可以在任何时候释放,留下空闲空间。有时,内存分配器将执行维护任务,例如通过移动已分配的内存来对内存进行碎片整理,或者进行垃圾收集 - 在内存不再占用范围并在解除分配时在运行时进行识别。
这些图像应该可以很好地描述在堆栈和堆中分配和释放内存的两种方法。百胜!
它们在多大程度上受操作系统或语言运行时控制?
如上所述,堆和堆栈是通用术语,可以通过多种方式实现。计算机程序通常有一个名为a的堆栈 调用堆栈 它存储与当前函数相关的信息,例如指向调用它的函数的指针,以及任何局部变量。因为函数调用其他函数然后返回,所以堆栈增大和缩小以保持来自调用堆栈中的函数的信息。程序实际上没有对它进行运行时控制;它取决于编程语言,操作系统甚至系统架构。
堆是一个通用术语,用于动态和随机分配的任何内存;即无序。内存通常由OS分配,应用程序调用API函数来执行此分配。管理动态分配的内存需要相当大的开销,这通常由操作系统处理。
它们的范围是什么?
调用堆栈是一种低级概念,它与编程意义上的“范围”无关。如果您反汇编某些代码,您将看到对堆栈部分的相对指针样式引用,但就更高级别的语言而言,该语言强加了自己的范围规则。但是,堆栈的一个重要方面是,一旦函数返回,该函数的任何本地函数都会立即从堆栈中释放出来。考虑到编程语言的工作原理,它的工作方式与预期的方式相同。在堆中,它也很难定义。范围是操作系统公开的内容,但是您的编程语言可能会添加有关应用程序中“范围”的规则。处理器体系结构和OS使用虚拟寻址,处理器将虚拟寻址转换为物理地址,并存在页面错误等。它们跟踪哪些页面属于哪些应用程序。但是,您永远不必担心这一点,因为您只需使用编程语言用于分配和释放内存的任何方法,并检查错误(如果分配/释放因任何原因失败)。
是什么决定了它们的大小?
同样,它取决于语言,编译器,操作系统和体系结构。堆栈通常是预先分配的,因为根据定义它必须是连续的内存(更多内容在最后一段中)。语言编译器或OS确定其大小。您不会在堆栈上存储大量数据,因此除非出现不必要的无限递归(因此,“堆栈溢出”)或其他异常编程决策,否则它应该永远不会被完全使用。
堆是可以动态分配的任何东西的通用术语。根据您看待它的方式,它会不断变化大小。在现代处理器和操作系统中,它的工作方式无论如何都是非常抽象的,所以你通常不需要担心它如何深入工作,除了(在它允许你的语言中)你不能使用的内存你尚未分配或已释放的记忆。
是什么让一个更快?
堆栈更快,因为所有可用内存始终是连续的。不需要维护所有可用内存段的列表,只需要指向当前堆栈顶部的单个指针。编译器通常将此指针存储在特殊的快速指针中 寄存器 以此目的。更重要的是,堆栈上的后续操作通常集中在非常靠近的内存区域,这非常低的水平有利于处理器片上高速缓存的优化。
(我已经从另一个或多或少是这个问题的问题中提出了这个答案。)
您的问题的答案是特定于实现的,并且可能因编译器和处理器体系结构而异。但是,这是一个简化的解释。
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 ++这样的语言中,当你不处理指针时,结构和类通常可以保留在堆栈中。
更多信息可以在这里找到:
和这里:
本文是上图的来源: 六个重要的.NET概念:堆栈,堆,值类型,引用类型,装箱和拆箱 - CodeProject
但请注意,它可能包含一些不准确之处。
堆栈 当你调用一个函数时,该函数的参数加上一些其他开销被放在堆栈上。一些信息(例如返回的地方)也存储在那里。 在函数内部声明变量时,该变量也会在堆栈中分配。
取消分配堆栈非常简单,因为您总是按照分配的相反顺序解除分配。输入函数时会添加堆栈内容,退出时会删除相应的数据。这意味着您倾向于保持在堆栈的一个小区域内,除非您调用许多调用许多其他函数的函数(或创建递归解决方案)。
堆 堆是您放置动态创建的数据的通用名称。如果您不知道程序将要创建多少太空飞船,您可能会使用新的(或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()
要么 sbrk()
系统调用,将更多页面的物理内存映射到进程的虚拟地址空间。
在没有虚拟内存的系统中,例如某些嵌入式系统,通常会应用相同的基本布局,但堆栈和堆的大小是固定的。但是,在其他嵌入式系统(例如基于Microchip PIC单片机的系统)中,程序堆栈是一个单独的内存块,无法通过数据移动指令寻址,只能通过程序流指令间接修改或读取(调用,返回等)。其他架构,如Intel Itanium处理器,也有 多个堆栈。从这个意义上说,堆栈是CPU架构的一个元素。
我想很多其他人在这件事上给了你大部分正确答案。
然而,遗漏的一个细节是“堆”实际上可能被称为“免费商店”。这种区别的原因是原始的免费存储是使用称为“二项式堆”的数据结构实现的。因此,从malloc()/ free()的早期实现中分配是从堆中分配的。然而,在这个现代,大多数免费商店都使用非二维堆的非常精细的数据结构来实现。
堆栈是内存的一部分,可以通过几个关键的汇编语言指令来操作,例如'pop'(从堆栈中删除并返回一个值)和'push'(将值推送到堆栈),还可以调用(调用子程序 - 这会将地址推回到堆栈中)并返回(从子程序返回 - 这会将地址弹出堆栈并跳转到它)。它是堆栈指针寄存器下面的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子例程,也用于在调用子例程之前保留寄存器中的值。
堆是操作系统给应用程序的内存的一部分,通常通过类似malloc的系统调用。在现代操作系统上,此内存是一组只有调用进程才能访问的页面。
堆栈的大小在运行时确定,并且通常在程序启动后不会增长。在C程序中,堆栈需要足够大以容纳每个函数中声明的每个变量。堆将根据需要动态增长,但操作系统最终会进行调用(它通常会使堆积增长超过malloc请求的值,因此至少某些未来的malloc将不需要返回到内核获得更多内存。这种行为通常可以自定义)
因为你在启动程序之前已经分配了堆栈,所以在使用堆栈之前你永远不需要malloc,所以这是一个小优势。在实践中,很难预测具有虚拟内存子系统的现代操作系统的速度和速度会有多快,因为页面的实现方式和存储位置是实现细节。