memory-management - 栈有什么用 - 队列堆栈




堆栈和堆的内容和位置是什么? (17)

在排序

堆栈用于静态内存分配,堆用于动态内存分配,两者都存储在计算机的RAM中。

详细地

堆栈

堆栈是一个“LIFO”(后进先出)数据结构,由CPU非常密切地管理和优化。每次函数声明一个新变量时,它都会被“推”到堆栈上。然后每次函数退出时,该函数推送到堆栈的所有变量都被释放(也就是说,它们被删除)。一旦释放了堆栈变量,该内存区域就可用于其他堆栈变量。

使用堆栈存储变量的优点是可以为您管理内存。您不必手动分配内存,也可以在不再需要时释放内存。更重要的是,因为CPU如此高效地组织堆栈内存,读取和写入堆栈变量非常快。

更多可以在here找到。

堆是计算机内存的一个区域,不会自动为您管理,并且不受CPU的严格管理。它是一个更自由浮动的内存区域(并且更大)。要在堆上分配内存,必须使用malloc()或calloc(),它们是内置的C函数。一旦你在堆上分配了内存,你就有责任使用free()在你不再需要它时解除分配该内存。

如果您不这样做,您的程序将具有所谓的内存泄漏。也就是说,堆上的内存仍然会被搁置(并且不会被其他进程使用)。正如我们将在调试部分中看到的,有一个名为Valgrind的工具可以帮助您检测内存泄漏。

与堆栈不同,堆对可变大小没有大小限制(除了计算机明显的物理限制)。堆内存的读取和写入速度稍慢,因为必须使用指针来访问堆上的内存。我们将很快讨论指针。

与堆栈不同,堆上创建的变量可由程序中任何位置的任何函数访问。堆变量本质上是全局的。

更多可以在here找到。

在堆栈上分配的变量直接存储到存储器中,并且对该存储器的访问非常快,并且在编译程序时处理其分配。当函数或方法调用另一个函数,该函数又调用另一个函数等时,所有这些函数的执行将保持挂起,直到最后一个函数返回其值。堆栈始终以LIFO顺序保留,最近保留的块始终是要释放的下一个块。这使得跟踪堆栈非常简单,从堆栈中释放块只不过是调整一个指针。

在堆上分配的变量在运行时分配了内存并且访问此内存有点慢,但堆大小仅受虚拟内存大小的限制。堆的元素彼此之间没有依赖关系,并且可以随时随机访问。您可以随时分配一个块并随时释放它。这使得在任何给定时间跟踪堆的哪些部分被分配或释放变得更加复杂。

如果您在编译时确切地知道需要分配多少数据,则可以使用堆栈,并且它不会太大。如果您不确切知道运行时需要多少数据,或者需要分配大量数据,则可以使用堆。

在多线程情况下,每个线程都有自己完全独立的堆栈,但它们将共享堆。堆栈是特定于线程的,堆是特定于应用程序的。在异常处理和线程执行中,堆栈很重要。

每个线程都有一个堆栈,而应用程序通常只有一个堆(尽管为不同类型的分配设置多个堆并不罕见)。

在运行时,如果应用程序需要更多堆,它可以从空闲内存分配内存,如果堆栈需要内存,它可以从应用程序的空闲内存分配内存中分配内存。

甚至,herehere给出here更多细节。

现在来看你的问题的答案

它们在多大程度上受操作系统或语言运行时控制?

操作系统在创建线程时为每个系统级线程分配堆栈。通常,语言运行库调用OS来为应用程序分配堆。

更多可以在here找到。

它们的范围是什么?

已经在顶部给出。

“如果你确切地知道在编译之前需要分配多少数据,你就可以使用堆栈。它不是太大。如果你不确切知道在运行时需要多少数据,你可以使用堆。你需要分配大量的数据。“

更多信息可以在here找到。

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

创建线程时,OS的大小由OS设置。堆的大小是在应用程序启动时设置的,但它可以在需要空间时增长(分配器从操作系统请求更多内存)。

是什么让一个更快?

堆栈分配要快得多,因为它真正做的就是移动堆栈指针。使用内存池,您可以从堆分配中获得可比较的性能,但这会带来轻微的复杂性和自身的麻烦。

此外,堆栈与堆不仅是性能考虑因素; 它还告诉你很多关于对象的预期寿命。

细节可以在here找到。

编程语言书籍解释了在堆栈上创建了值类型,并且在堆上创建了引用类型,而没有解释这两者是什么。 我还没有看到对此的明确解释。 我理解堆栈是什么。 但,

  • 它们在哪里和它们(物理上在真实计算机的记忆中)?
  • 它们在多大程度上受操作系统或语言运行时控制?
  • 它们的范围是什么?
  • 是什么决定了它们的大小?
  • 是什么让一个更快?

(我已经从另一个或多或少是这个问题的问题中提出了这个答案。)

您的问题的答案是特定于实现的,并且可能因编译器和处理器体系结构而异。 但是,这是一个简化的解释。

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

  • 堆包含已使用和可用块的链接列表。 通过从其中一个空闲块创建合适的块,可以满足堆上的新分配(通过newmalloc )。 这需要更新堆上的块列表。 有关堆上块的元信息也经常存储在堆中,位于每个块的前面。
  • 随着堆增长,新块通常从较低地址分配给较高地址。 因此,您可以将堆视为一堆内存块,这些内存块在分配内存时会增大。 如果堆对于分配而言太小,则通常可以通过从底层操作系统获取更多内存来增加大小。
  • 分配和解除分配许多小块可能使堆处于这样的状态:在所使用的块之间散布有许多小的空闲块。 分配大块的请求可能失败,因为即使空闲块的组合大小足够大,也没有任何空闲块足够大以满足分配请求。 这称为堆碎片
  • 当释放与空闲块相邻的使用块时,可以将新的空闲块与相邻的空闲块合并以创建更大的空闲块,从而有效地减少堆的碎片。

堆栈

  • 堆栈通常与CPU上的特殊寄存器紧密串联,命名为堆栈指针 。 最初,堆栈指针指向堆栈的顶部(堆栈中的最高地址)。
  • CPU具有值推入堆栈并从堆栈中弹回的特殊指令。 每次推送都将值存储在堆栈指针的当前位置,并减少堆栈指针。 pop会检索堆栈指针指向的值,然后增加堆栈指针(不要因为堆栈添加减少堆栈指针并删除增加它这一事实而感到困惑。请记住堆栈增长到底部)。 存储和检索的值是CPU寄存器的值。
  • 当调用函数时,CPU使用特殊指令来推送当前指令指针 ,即在堆栈上执行的代码的地址。 然后CPU通过将指令指针设置为被调用函数的地址来跳转到该函数。 稍后,当函数返回时,从堆栈中弹出旧的指令指针,并在调用函数后立即执行代码。
  • 输入函数时,堆栈指针会减少,以便在堆栈上为本地(自动)变量分配更多空间。 如果函数有一个本地32位变量,则在堆栈上留出四个字节。 当函数返回时,堆栈指针被移回以释放分配的区域。
  • 如果函数有参数,则在调用函数之前将这些参数压入堆栈。 然后,函数中的代码能够从当前堆栈指针向上导航堆栈以找到这些值。
  • 嵌套函数调用就像魅力一样。 每个新调用都将分配函数参数,返回地址和局部变量的空间,这些激活记录可以堆叠用于嵌套调用,并在函数返回时以正确的方式展开。
  • 由于堆栈是有限的内存块,因此可以通过调用太多嵌套函数和/或为局部变量分配太多空间来导致堆栈溢出 。 通常,用于堆栈的存储区域的设置方式是在堆栈的底部(最低地址)下写入将触发CPU中的陷阱或异常。 然后,运行时可以捕获此异常情况并将其转换为某种堆栈溢出异常。

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

不,函数的激活记录(即本地或自动变量)在堆栈上分配,不仅用于存储这些变量,还用于跟踪嵌套函数调用。

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

但是,堆栈是与处理器架构紧密相关的更低级别的功能。 当没有足够的空间时增加堆不是太难,因为它可以在处理堆的库调用中实现。 但是,堆栈的增长通常是不可能的,因为只有在为时已晚时才发现堆栈溢出; 并且关闭执行线程是唯一可行的选择。


什么是堆栈?

堆叠是一堆物体,通常是整齐排列的物体。

计算体系结构中的堆栈是存储器区域,其中以后进先出的方式添加或移除数据。
在多线程应用程序中,每个线程都有自己的堆栈。

什么是堆?

堆是随意堆积的不整齐的东西。

在计算体系结构中,堆是动态分配的内存区域,由操作系统或内存管理器库自动管理。
在程序执行期间,会定期分配,释放和重新调整堆上的内存,这可能会导致称为碎片的问题。
当内存对象被分配时,它们之间的空间太小而无法容纳额外的内存对象,就会发生碎片。
最终结果是堆空间的百分比不可用于进一步的内存分配。

两者一起

在多线程应用程序中,每个线程都有自己的堆栈。但是,所有不同的线程都将共享堆。
因为不同的线程在多线程应用程序中共享堆,这也意味着线程之间必须有一些协调,这样它们就不会尝试访问和操作堆中的同一块内存。同一时间。

哪个更快 - 堆栈还是堆? 为什么?

堆栈比堆快得多。
这是因为在堆栈上分配内存的方式。
在堆栈上分配内存就像向上移动堆栈指针一样简单。

对于刚接触编程的人来说,使用堆栈可能是个好主意,因为它更容易。
由于堆栈很小,因此当您确切知道数据需要多少内存时,或者如果您知道数据的大小非常小时,则需要使用它。
当您知道数据需要大量内存时,最好使用堆,或者您不确定需要多少内存(例如动态数组)。

Java内存模型

堆栈是存储局部变量(包括方法参数)的存储区域。对于对象变量,这些仅仅是对堆上实际对象的引用(指针)。
每次实例化对象时,都会留出一大堆堆内存来保存该对象的数据(状态)。由于对象可以包含其他对象,因此某些数据实际上可以保存对这些嵌套对象的引用。


  • 非常快速的访问
  • 不必显式取消分配变量
  • 空间由CPU有效管理,内存不会碎片化
  • 仅限局部变量
  • 堆栈大小限制(取决于操作系统)
  • 变量无法调整大小

  • 可以全局访问变量
  • 内存大小没有限制
  • (相对)访问速度较慢
  • 无法保证有效利用空间,随着时间的推移,内存可能会随着内存块的分配而变得碎片化,然后被释放
  • 你必须管理内存(你负责分配和释放变量)
  • 可以使用realloc()调整变量大小

堆:

  • 像堆一样存储在计算机RAM中。
  • 在堆栈上创建的变量将超出范围并自动取消分配。
  • 与堆上的变量相比,分配要快得多。
  • 使用实际的堆栈数据结构实现。
  • 存储本地数据,返回地址,用于参数传递。
  • 当使用过多的堆栈时(主要来自无限或太深的递归,非常大的分配),可能会出现堆栈溢出。
  • 可以在没有指针的情况下使用在堆栈上创建的数据。
  • 如果您确切地知道在编译之前需要分配多少数据并且它不是太大,您将使用堆栈。
  • 通常在程序启动时已确定最大大小。

堆:

  • 像堆栈一样存储在计算机RAM中。
  • 在C ++中,必须手动销毁堆上的变量,并且永远不会超出范围。 使用deletedelete[]free释放数据。
  • 与堆栈上的变量相比,分配更慢。
  • 按需使用以分配程序使用的数据块。
  • 当存在大量分配和解除分配时,可能会出现碎片。
  • 在C ++或C中,在堆上创建的数据将由指针指向,并分别用newmalloc分配。
  • 如果请求分配的缓冲区太大,可能会出现分配失败。
  • 如果您不确切知道运行时需要多少数据,或者需要分配大量数据,则可以使用堆。
  • 负责内存泄漏。

例:

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;

好吧,简单而言之,它们意味着有序不是有序 ......!

堆栈:在堆栈项目中,事物处于彼此的顶部,意味着要更快,更有效地处理!...

所以总有一个索引指向特定项目,处理也会更快,项目之间也有关系!...

:没有订单,处理速度会慢,价值混乱,没有特定的订单或索引......有随机的,它们之间没有关系...所以执行和使用时间可能会有所不同......

我还创建了下面的图像,以显示它们的外观:


几分钱:我认为,绘制内存图形化更简单:


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

这么简单的方法:进程堆通常用于进程和内部的所有线程,在常见情况下用于内存分配,例如malloc()

堆栈是快速存储器,用于存储常见情况函数返回指针和变量,作为函数调用中的参数处理,本地函数变量。


在以下C#代码中

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

以下是内存的管理方式

只要函数调用进入堆栈,只需要持续的Local Variables 。 堆用于变量,我们事先并不知道它们的生命周期,但我们希望它们可以持续一段时间。 在大多数语言中,如果我们想要将它存储在堆栈中,那么在编译时我们知道变量的大小是至关重要的。

对象(在我们更新它们时大小不同)会在堆上进行,因为我们在创建时不知道它们将持续多长时间。 在许多语言中,堆被垃圾收集以查找不再具有任何引用的对象(例如cls1对象)。

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

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

堆栈和堆内存分配之间的区别«timmurphy.org

和这里:

在堆栈和堆上创建对象

本文是上图的源代码: 六个重要的.NET概念:堆栈,堆,值类型,引用类型,装箱和拆箱 - CodeProject

但请注意,它可能包含一些不准确之处。


我想很多其他人在这件事上给了你大部分正确答案。

然而,遗漏的一个细节是“堆”实际上可能被称为“免费商店”。这种区别的原因是原始的免费存储是使用称为“二项式堆”的数据结构实现的。因此,从malloc()/ free()的早期实现中分配是从堆中分配的。然而,在这个现代,大多数免费商店都使用非二维堆的非常精细的数据结构来实现。


我有一些东西要与你分享,虽然已经写了重点。

  • 非常快速的访问。
  • 存储在RAM中。
  • 这里加载函数调用以及传递的局部变量和函数参数。
  • 当程序超出范围时,会自动释放空间。
  • 存储在顺序存储器中。

  • 相对于Stack而言访问速度较慢。
  • 存储在RAM中。
  • 动态创建的变量存储在此处,以后需要在使用后释放分配的内存。
  • 存储在内存分配的任何位置,始终由指针访问。

有趣的说明:

  • 如果函数调用已存储在堆中,则会产生两个混乱点:
    1. 由于堆栈中的顺序存储,执行速度更快。堆中存储会导致大量时间消耗,从而导致整个程序执行速度变慢。
    2. 如果函数存储在堆中(指针指向凌乱的存储),则无法返回到调用者地址(由于内存中的顺序存储,该堆栈会产生)。

反馈很好。


来自WikiAnwser。

当函数或方法调用另一个函数,该函数又调用另一个函数等时,所有这些函数的执行将保持挂起,直到最后一个函数返回其值。

这个挂起函数调用链是堆栈,因为堆栈中的元素(函数调用)彼此依赖。

在异常处理和线程执行中,堆栈很重要。

堆只是程序用来存储变量的内存。堆的元素(变量)彼此之间没有依赖关系,并且可以随时随机访问。


澄清一下 , 这个答案有不正确的信息( thomas在评论后修正了他的答案,很酷:))。 其他答案只是避免解释静态分配的含义。 因此,我将解释三种主要的分配形式以及它们通常如何与下面的堆,堆栈和数据段相关联。 我还将在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

你可以用堆栈做一些有趣的事情。例如,你有像alloca这样的函数(假设你可以通过关于它的使用的大量警告),这是一种malloc,它专门使用堆栈而不是堆来存储。

也就是说,基于堆栈的内存错误是我经历过的最糟糕的错误。如果使用堆内存,并且超出了已分配块的范围,则有可能触发段故障。(不是100%:你的块可能偶然与你先前分配的另一个块连续。)但是由于在堆栈上创建的变量总是彼此连续,因此写出越界可以改变另一个变量的值。我了解到每当我觉得我的程序已经停止遵守逻辑定律时,它可能就是缓冲区溢出。


其他人直接回答了你的问题,但在尝试理解堆栈和堆时,我认为考虑传统UNIX进程的内存布局(没有线程和mmap()基于分配器)是有帮助的。“ 内存管理术语表”网页提供了此内存布局的图表。

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

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


堆栈是内存的一部分,可以通过几个关键的汇编语言指令来操作,例如'pop'(从堆栈中删除并返回一个值)和'push'(将值推送到堆栈),还可以调用(调用子程序 - 这会将地址推回到堆栈中)并返回(从子程序返回 - 这会将地址弹出堆栈并跳转到它)。它是堆栈指针寄存器下面的内存区域,可以根据需要进行设置。堆栈还用于将参数传递给子例程,也用于在调用子例程之前保留寄存器中的值。

堆是操作系统给应用程序的内存的一部分,通常通过类似malloc的系统调用。在现代操作系统上,此内存是一组只有调用进程才能访问的页面。

堆栈的大小在运行时确定,并且通常在程序启动后不会增长。在C程序中,堆栈需要足够大以容纳每个函数中声明的每个变量。堆将根据需要动态增长,但操作系统最终会进行调用(它通常会使堆积增长超过malloc请求的值,因此至少某些未来的malloc将不需要返回到内核获得更多内存。这种行为通常可以自定义)

因为你在启动程序之前已经分配了堆栈,所以在使用堆栈之前你永远不需要malloc,所以这是一个小优势。在实践中,很难预测具有虚拟内存子系统的现代操作系统的速度和速度会有多快,因为页面的实现方式和存储位置是实现细节。


很多答案都是正确的概念,但我们必须注意硬件(即微处理器)需要一个堆栈来允许调用子程序(汇编语言中的CALL ......)。(OOP人会称之为方法

在堆栈上保存返回地址并调用→push / ret→pop直接在硬件中管理。

您可以使用堆栈传递参数..即使它比使用寄存器慢(微处理器大师会说或者20世纪80年代的BIOS书籍......)

  • 没有堆栈,没有微处理器可以工 (我们无法想象一个程序,即使是汇编语言,没有子程序/函数)
  • 没有堆它可以。(汇编语言程序可以在没有,因为堆是OS概念,而不是malloc,即OS / Lib调用。

堆栈使用速度更快:

  • 硬件,甚至推/弹都非常有效。
  • malloc需要进入内核模式,使用执行某些代码的锁/信号量(或其他同步原语)并管理跟踪分配所需的一些结构。

  • 介绍

物理存储器是存储器单元的物理地址的范围,其中应用程序或系统在执行期间存储其数据,代码等。内存管理表示通过将数据从物理内存交换到存储设备然后在需要时返回物理内存来管理这些物理地址。 OS使用虚拟内存实现内存管理服务。作为C#应用程序开发人员,您无需编写任何内存管理服务。 CLR使用底层操作系统内存管理服务为C#或任何其他针对CLR的高级语言提供内存模型。

图4-1显示了使用虚拟内存概念由OS抽象和管理的物理内存。虚拟内存是由OS管理的物理内存的抽象视图。虚拟内存只是一系列虚拟地址,这些虚拟地址在需要时由CPU转换为物理地址。

图4-1。CLR内存抽象

CLR使用操作内存服务为虚拟执行环境提供内存管理抽象层。CLR使用的抽象概念是AppDomain,线程,堆栈,堆存储映射文件等。应用程序域(AppDomain)的概念为您的应用程序提供了一个独立的执行环境。

  • CLR和OS之间的内存交互

通过在调试以下C#应用程序时查看堆栈跟踪,使用WinDbg,您将看到CLR如何使用底层操作系统内存管理服务(例如,来自KERNEL32.dll的HeapFree方法,来自ntdll.dll的RtlpFreeHeap方法)来实现自己的记忆模型:

using System;
namespace CH_04
{
    class Program
    {
        static void Main(string[] args)
        {
            Book book = new Book();
            Console.ReadLine();
        }
    }

    public class Book
    {
        public void Print() { Console.WriteLine(ToString()); }
    }
}

程序的编译程序集被加载到WinDbg中以开始调试。您可以使用以下命令初始化调试会话:

0:000> sxe ld clrjit

0:000> g

0:000> .loadby sos clr

0:000> .load C:\ Windows \ Microsoft.NET \ Framework \ v4.0.30319 \ sos.dll

然后,使用!bpmd命令在Program类的Main方法中设置断点:

0:000>!bpmd CH_04.exe CH_04.Program.Main

要继续执行并在断点处中断,请执行g命令:

0:000> g

当执行在断点处中断时,使用!eestack命令查看为当前进程运行的所有线程的堆栈跟踪详细信息。以下输出显示了为应用程序CH_04.exe运行的所有线程的堆栈跟踪:

0:000>!eestack

线程0

当前帧:(MethodDesc 00233800 +0 CH_04.Program.Main(System.String []))

ChildEBP RetAddr Caller,Callee

0022ed24 5faf21db clr!CallDescrWorker + 0x33

/ trace删除 /

0022f218 77712d68 ntdll!RtlFreeHeap + 0x142,调用ntdll!RtlpFreeHeap

0022f238 771df1ac KERNEL32!HeapFree + 0x14,调用ntdll!RtlFreeHeap

0022f24c 5fb4c036 clr!EEHeapFree + 0x36,调用KERNEL32!HeapFree

0022f260 5fb4c09d clr!EEHeapFreeInProcessHeap + 0x24,调用clr!EEHeapFree

0022f274 5fb4c06d clr!operator delete [] + 0x30,调用clr!EEHeapFreeInProcessHeap / trace删除 /

0022f4d0 7771316f ntdll!RtlpFreeHeap + 0xb7a,调用ntdll!_SEH_epilog4

0022f4d4 77712d68 ntdll!RtlFreeHeap + 0x142,调用ntdll!RtlpFreeHeap

0022f4f4 771df1ac KERNEL32!HeapFree + 0x14,调用ntdll!RtlFreeHeap

/ trace删除 /

此堆栈跟踪指示CLR使用OS内存管理服务来实现其自己的内存模型。.NET中的任何内存操作都通过CLR内存层传输到OS内存管理层。

图4-2说明了CLR在运行时使用的典型C#应用程序内存模型。

图4-2。典型的C#应用​​程序内存模型

CLR内存模型与OS内存管理服务紧密耦合。要了解CLR内存模型,了解底层操作系统内存模型非常重要。了解物理内存地址空间如何被抽象到虚拟内存地址空间,用户应用程序和系统应用程序使用虚拟地址空间的方式,虚拟到物理地址映射如何工作,内存如何也是至关重要的。映射文件有效,等等。这些背景知识将提高您对CLR内存模型概念的掌握,包括AppDomain,堆栈和堆。

有关更多信息,请参阅本书:

C#解构:了解C#如何在.NET Framework上运行

本书+ ClrViaC#+ Windows Internals是已知的.net框架的深入资源和与OS的关系的优秀资源。





heap