程序运行的时候,数据都存在哪里?
基础知识
- CPU:(Central Processing Unit / Processor),又叫处理器。
- RAM:(random access memory),随机存取存储器。又叫内存。数据只能临时存储。
- ROM:(read-only memory),译为“只读存储器”;存储器的任何单元只能随机地读出信息,而不能写入新信息,ROM的特点是把信息写入存储器以后能长期保存,不会因电源断电而丢失信息。在传统的ROM类型中,比如Mask ROM(固化在硬件中)、PROM(编程一次),ROM的内容在制造或编程后是无法修改的;现代ROM,如EEPROM(电可擦除可编程只读存储器)和Flash ROM(闪存),这些ROM类型允许在特定条件下进行擦除和重新编程。ROM的大小可以从几千个字节(KB)到数百GB不等。
- 栈:(Stack),栈是一种连续储存的数据结构,它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出来)。
- 堆:(heap),是一种非连续的树形储存数据结构,每个节点有一个值,整棵树是经过排序的。特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。常用来实现优先队列,存取随意。
- 内存泄漏:内存泄漏是指在程序中动态分配了内存(例如使用malloc()、calloc()、new等),但在不再需要使用这块内存时未进行正确的释放操作,导致这块内存永远无法被回收和重用,从而造成了内存资源的浪费。它会导致程序持续占用内存而不释放,随着程序的运行时间增长,可用内存逐渐减少,最终可能导致程序崩溃或系统资源耗尽。
- 栈帧:(Stack Frame),是在函数调用过程中用于存储函数执行所需信息的一块内存区域。每当一个函数被调用时,计算机系统都会为该函数创建一个栈帧,并将其压入栈(称为函数调用栈或调用堆栈)中,用于保存函数的上下文信息和局部变量。栈帧通常包含以下内容:
- 返回地址(Return Address):保存函数执行完后返回到调用该函数的代码位置的地址。函数执行完后,会通过返回地址返回到调用者处继续执行。
- 参数(Arguments):保存函数调用时传递的参数值。这些参数值会被复制到栈帧中,供函数在执行时使用。
- 局部变量(Local Variables):保存函数内部定义的局部变量的值。局部变量是函数内部的变量,只在函数执行期间存在。
- 上一个栈帧指针(Previous Stack Frame Pointer):指向上一个函数的栈帧,用于函数执行完后返回到调用者处时找到上一个栈帧。栈帧指针是一个特殊的寄存器或内存位置,它在函数调用过程中用于引导程序继续运行,实现正确的函数调用和返回。
*注:返回地址和上一个栈帧指针的区别:
1. 保存时机不同。保存上一个栈帧指针发生在调用者调用被调用函数之前,目的是在函数调 用结束后返回到调用者的栈帧;而保存返回地址发生在函数调用者调用被调用函数时,目的是在函数调用结束后返回到调用者的正确执行点。
2. 意义不同。返回地址指的是最终结果应该返回到哪里;而上一个栈帧指针是为了指引程序能继续运行下去,即继续执行调用者的代码。
- 一个程序的运行步骤:
- 编写源代码:程序员使用编程语言(如C、C++、Python等)编写程序的源代码,源代码是程序的人类可读形式。
- 编译:源代码需要通过编译器进行编译,将其转换成计算机能够理解和执行的二进制机器代码。编译器将源代码转换成与特定计算机体系结构(如x86、ARM等)相关的机器代码文件,通常是可执行文件(例如Windows中的.exe文件、Linux中的可执行文件等)。
- 加载:当运行程序时,操作系统会将可执行文件加载到内存中。加载时,操作系统会为程序分配必要的内存空间,并为程序设置一些执行所需的环境。
- 执行:一旦程序被加载到内存中,计算机的中央处理单元(CPU)会按照指令逐条执行程序的机器代码。程序的执行过程会根据源代码中的逻辑进行操作,涉及计算、条件判断、循环、函数调用等操作。
- 运行结果:程序的运行结果将在计算机的显示器、终端或输出文件中显示,这取决于程序的设计和输出方式。
- 终止:当程序的执行完成或者遇到特定的终止条件时,程序会自动终止执行,并释放相应的资源。
- 虚拟地址空间:虚拟地址空间是计算机系统中,每个进程所看到的抽象内存地址空间。它提供了一种逻辑视图,使得每个进程认为自己拥有独立的连续地址空间,而实际上这些地址空间是映射到物理内存和其他资源的。虚拟地址空间的大小可能远大于实际的物理内存大小,这为进程提供了更大的地址空间和更灵活的内存管理。
- 程序的可执行代码通常由编译器将源代码编译成机器码或中间代码,并生成可执行文件。当你运行程序时,操作系统会将可执行文件中的代码加载到进程的虚拟地址空间中。这个虚拟地址空间包含堆、栈、代码段、数据段等区域。
存储
寄存器
- 是最快的存储区。位于CPU内部。一个CPU里只有几个寄存器,假设CPU是64位,一个寄存器可以存储一位数,数的范围是0-2的64次-1。通常由操作系统使用存储。
堆栈
-
读取,写入速度仅次于寄存器。是一种用于存储函数调用和局部变量的内存区域。它是由编译器自动管理的,遵循一种特定的内存分配方式。当程序调用一个函数时,它会为该函数的局部变量和函数调用上下文(如返回地址和调用者函数的栈帧信息)分配一块连续的内存空间。
-
栈内存遵循"后进先出"(Last-In-First-Out,LIFO)的原则,即最后入栈的数据最先被销毁(区别读取数据的顺序,读取数据根据地址直接读取,而不是按照后进先出的方式读取)。
-
举例说明:执行顺序如下:
-
main()函数被调用,创建了main()函数的栈帧。
-
在main()函数中调用functionA(),创建了functionA()函数的栈帧,functionA()的栈帧位于main()的栈帧之上。
-
在functionA()中声明局部变量a,并输出其值。
-
在functionA()中调用functionB(),创建了functionB()函数的栈帧,functionB()的栈帧位于functionA()的栈帧之上。
-
在functionB()中声明局部变量b,并输出其值。
-
functionB()执行完毕,其栈帧被销毁。
-
functionA()执行完毕,其栈帧被销毁。
-
main()函数执行完毕,其栈帧被销毁,程序结束。
在这个例子中,functionB()被后进入栈,但由于栈的"后进先出"特性,它会先被销毁,然后才是functionA()的栈帧被销毁
#include <stdio.h> void functionB() { int b = 20; printf("Function B: %d\n", b); } void functionA() { int a = 10; printf("Function A: %d\n", a); functionB(); } int main() { functionA(); return 0; }
-
-
如何知道程序需要多大的栈内存?
在编译阶段,编译器会进行栈帧大小的预估,但不会预测需要多大的栈内存。
编译器在编译阶段会分析函数的代码,计算出每个函数所需的栈帧大小,这个过程称为栈帧分配(Stack Frame Allocation),然后在编译时生成相应的代码来分配栈内存。栈帧大小的计算是基于函数的局部变量和参数的内存需求、函数调用过程中需要保存的上下文信息等因素。
栈帧大小的预估只是一个静态分析,可能并不能完全准确地预测程序的运行时内存需求。
堆
- 位于RAM区。不同于堆栈:编译器不知道存储的数据在堆里活多长时间。
- 堆内存释放顺序为"先进先出"(First-In-First-Out,FIFO),也就是最先分配的内存将被最先释放。
- 堆可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的,但缺点是,由于要在运行时动态分配内存,存取速度较慢。
- 堆内存的大小取决于操作系统和计算机硬件的配置。在现代计算机系统中,通常有一个硬件限制,即每个进程可以使用的虚拟地址空间大小。这个限制由操作系统和硬件共同决定。
在32位操作系统中,通常每个进程的虚拟地址空间大小为2^32(4GB),其中一部分用于操作系统本身和一些系统内核区域,剩余的地址空间留给进程使用。因此,在32位操作系统中,堆内存的大小通常被限制在几GB左右。 - 堆内存的分配和释放通常是由程序员手动控制的,它是通过动态内存分配的方式实现的,通常使用标准库函数(如C语言中的malloc()、calloc()、realloc(),C++中的new等)来进行堆内存的分配。动态内存分配函数会在堆内存中寻找一块足够大的连续空闲内存区域来满足请求的内存大小。如果找到合适的内存块,则将其标记为已用,并返回指向这块内存的指针;否则,会根据具体的分配策略,向操作系统请求更多的内存空间(堆扩容),然后再分配给程序。
常量存储
- 通常直接存放在代码内部,即是指在编程阶段将常量的值直接嵌入到生成的机器代码中。
例:#include <stdio.h> int main() { int x = 10; int y = 5; int result = x + y; printf("The result is: %d\n", result); return 0; }
在上面的机器代码中,常量 10 和 5 直接被加载到寄存器中,并在 ADD 指令中相加,而不需要在运行时从内存中读取这些常量的值。LOAD_CONST 10 ; 把常量 10 加载到寄存器 LOAD_CONST 5 ; 把常量 5 加载到寄存器 ADD ; 将寄存器中的两个值相加 STORE result ; 将结果存储到变量 result
- 也可以存在ROM中。
非RAM存储
- 存储在其他媒介之上,比如磁盘。
补充
高速缓冲存储器
- (Cache),是计算机中的一种高速存储器,用于暂时保存最常用的指令和数据,以加快CPU对数据的访问速度。它位于CPU内部,在内存和CPU之间。读取速度快于内存,慢于寄存器。
- 当CPU需要访问数据时,它首先查找缓存,如果数据在缓存中找到,则称为缓存命中(Cache Hit),CPU可以更快地获取数据。如果数据不在缓存中,CPU必须从主存储器中获取数据,称为缓存未命中(Cache Miss),这会导致额外的延迟。
- 大小在几百KB到几兆字节之间。
虚拟内存
- 虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。
- 虚拟内存的主要特点包括:
- 虚拟地址空间。
- 分页机制: 虚拟内存将虚拟地址空间划分为固定大小的块,称为页(Page)。物理内存也被划分为与页相同大小的块,称为物理页(Page Frame)。虚拟内存和物理内存之间的映射是通过页表(Page Table)来管理的。
- 页面置换: 当程序访问虚拟地址空间中的某一页时,操作系统会判断该页是否已经在物理内存中。如果页在物理内存中,则称为页命中(Page Hit),程序可以直接访问物理内存中的数据。如果页不在物理内存中,则称为页缺失(Page Fault)。操作系统会根据某种置换算法从物理内存中选择一个页替换出去,以便将新的页加载进来。
- 页面交换: 当物理内存不足以容纳所有活动的程序和数据时,操作系统可以将不活动的页(暂时不需要的页)交换到硬盘上的一个特殊区域,称为交换空间(Swap Space)。这样,物理内存就腾出空间来加载更需要的页。
- 虚拟内存大小为16GB意味着每个程序可以在逻辑上拥有一个16GB大小的连续虚拟地址空间。这个大小可以是32位或64位系统,取决于操作系统和计算机的体系结构。