内存用例、堆栈和堆:基本内存管理
在之前的部分中,我们已经了解到在ISA的抽象中,程序的“状态”完全由寄存器和内存表示(这并不是完全正确的,但是现在你不需要担心这个)。也就是说,它们的内容完全确定了程序的行为。由于内存远远大于寄存器,它是程序存储其状态的主要存储器。
然而,ISA并没有提供任何关于如何使用内存的指导。虽然这增加了灵活性,但也增加了复杂性。通常,程序需要知道哪个内存地址存储了什么值;如果它假设某个地址存储了某个值,但实际上该地址被先前的指令覆盖了,那么它将无法按预期工作。
随着软件应用程序越来越大,追踪每个地址手动存储了什么值变得越来越困难,并确保内存读写正常工作。这就是内存管理问题。
为了系统地解决这个问题,人们提出了许多不同的“如何使用内存”的模型,例如所有权、自动垃圾回收、RAII(Resource Allocation Is Initialization)等等。我们不会在这里对它们进行详细描述,但我们会描述它们几乎都使用或依赖的共享概念。这些概念是内存用例、堆栈和堆。
值得注意的是,这些概念在更高级的语言中通常都被抽象化了,不会显式地显示在代码中,尤其是那些具有垃圾回收器(如Python)的语言。然而,理解这些概念提供了一个很好的基础,用以理解程序在低级别如何运行(与ISA一样)。
内存用例
内存用例帮助回答以下问题:对于某个内存片段(即与一组内存地址对应的内存位置),在某个时间段内,应如何使用它?也就是说,在每个时间点上,这块内存用于什么?
内存用例作为抽象
如果你回想一下之前模块中抽象的定义,你会发现内存用例也是一种抽象:内存用例定义了某物(内存)对于某个时间段内某种用例而言的用途(你可以在特定时间段内使用它),而忽略了所有无关的细节。
虽然这似乎是一个显而易见的想法,但仅仅通过内存用例就能解决许多内存管理中的问题,因为它明确地说明了对内存地址应该和不应该做什么。
在定义内存片段的用例之前,程序员可能会盯着一块内存看着也不知道该怎么做: 我可以往里面写数据吗?这里存储的值是什么? 它只是一些我可以随意覆盖的随机字节,还是一些有意义的内容我应该保存? 以后的任何指令会使用这块内存吗? 如果我想在这里存储一些持久的东西,它会在某个时刻被覆盖吗? 随着程序变得越来越大,回答这些问题变得越来越困难,因为这需要查看程序中的每条指令并查看它对内存的操作。
内存用例使得这变得容易得多。在了解了一块内存的用例之后,开发人员可以对其使用的有效方式做出一些假设,并且可以预测在某些方式下其使用的结果。例如,开发人员可以思考:“好的,在这一点上,这块内存并没有存储任何重要的东西,所以我可以任意写入它。然而,在操作A完成后,操作B将使用这块内存来存储其记录,我在这里写入的任何内容都将被覆盖,所以我必须确保在操作B开始后不再读取这块内存。”
与内存用例相关的一些术语
以下描述了一些与内存用例相关的术语,你在开发者的世界中可能会遇到。
分配
“分配”一块内存意味着找到一块“空闲”的内存(即没有人使用的内存)并为某种用例分配它。在内存分配给某个用例之后,它被视为不能用于其他任何用例。
例如,在计算机程序中,通常会分配内存来存储数据,比如某人的生日。
回收
“回收”一块内存意味着标记与之相关的当前用例(无论是什么)。在一块内存被回收之后,它被视为可以重新用于任何其他用例,并且不可以再用于之前的用例。
例如,如果一块内存当前正在存储某人的生日并且已被回收,则可以将其用于存储\(153 \times 13251\)的结果等。
术语“回收”有时被用作“释放”或“销毁”的同义词。
堆栈和堆
建立在内存用例的概念之上,堆栈和堆是两个特殊的内存块,它们创建了一个更明确定义的内存管理模型。堆栈和堆不是为特定的内存用例而存在(如存储某人的生日);相反,它们是可以按照一种明确定义的方式分配内存的“池”。
你可以将堆栈和堆都看作在程序的整个生命周期内存在的。
堆栈和堆作为数据结构
堆栈和堆也是数据结构的名称。虽然它们与此处介绍的内存对象有一些相似之处,但它们并不是同一样东西。
为了澄清“堆栈内存对象”和“堆栈数据结构”的区别,前者通常被称为“堆栈内存”,而后者则是“堆栈”。类似地,“堆内存对象”和“堆数据结构”通常被称为“堆内存”和“堆”。
堆栈
在内存用例方面,堆栈是一个特殊的,并且通常较小的内存块,用于存储大致上“小而且需要快速访问的东西”。
堆栈是后进先出(LIFO)的顺序,这意味着当你从中分配内存时,新分配的内存位于先前分配的最后一块内存的上方(即其地址始终在其地址之后或之前)。类似地,当你从中回收内存时,最上层的内存块首先被回收。
为什么堆栈适合“小而且需要快速访问的东西”?
由于堆栈通常是较小的,它只能存储较小的东西。另一方面,堆栈适用于需要快速访问的东西,因为它是有序的:只要你知道在你要访问的东西之后的东西的大小和堆栈顶部的内存地址(即堆栈上的最后一块东西的地址),你就可以轻松计算要访问的东西的内存地址并访问它。在编译语言(例如C++)中,此类内存地址计算通常在编译时静态完成,这意味着当程序运行时,你可以几乎不进行任何计算就获取要访问的堆栈上的任何东西的地址(目前,你不需要理解这是如何工作的;只需记住在堆栈上访问东西是简单而快速的)。
有趣的事实
堆栈很小,其大小通常在程序的生命周期内是固定的;当你尝试使用比堆栈提供的内存更多的内存时,会触发一个异常,称为“堆栈溢出(stack overflow)”,这恰好也是一个平台的名称(https://stackoverflow.com/),开发人员在该平台上讨论程序错误及其修复方法。
堆
在内存用例方面,堆是一个特殊的,并且通常较大的内存块,用于存储大致上“大且持久的东西”。堆通常比堆栈大得多。
堆是无序的,这使得与堆栈相比,在分配和访问内存方面都更加困难。在堆栈中,内存总是在顶部分配;然而,在堆中,你必须首先找到一块空闲的内存,然后再分配它;当你想要访问某个东西时,你必须知道它的内存地址,否则在堆中找到它将非常困难。
在计算机程序中,一个典型的组合是将某个非常大的东西存储在堆上,但将其内存地址存储在堆栈上。这样,你可以存储大型的东西(比如数据库,因为堆很大),但也可以相对快速地访问它们(因为堆栈很快)。
结论
在本节中,我们介绍了内存管理的基本模型,即内存用例,堆栈和堆。
关键要点:
- 内存用例定义了内存在每一时间点上的用途。
- 堆栈是一个小的、有序的内存块,可以快速访问和分配内存。
- 堆是一个无序的内存块。与堆栈相比,从堆中分配内存更慢,但堆比堆栈大得多。
AI提示样例
如果你对本节的主题感兴趣,请随时向ChatGPT等AI咨询获取更多信息。
一些初始提示:
- 请解释堆栈和堆内存之间的区别。