彻底理解操作系统 3.2:程序员应如何理解内存

什么是内存

0和1这两个简单的数字能做什么?在其它学科中也许什么都做不了,但是在计算机科学中这就是全部。精彩纷呈的计算机世界正是构筑在这样两个简单数字之上。

内存本身其实非常简单,内存的作用就是用来装数字0和数字1的,如图所示,图中的一个盒子就是内存的一个基本单元,装的不是0就是装的1。

img

内存由一大堆的“盒子”组成,每个盒子中要么是0要么是1,其中8个盒子被称之为一个“字节”,每8个盒子也就是一个字节都有一个编号,这些编号就是简单的从0开始依次累加的,这个编号就被称之为“内存地址”。如图所示,你可以把内存理解为下面这张图,其中左边的数字是内存地址,每一排是一个字节,图中展示的就是一个8字节大小的内存。

img

而对于我们平时使用的比如2G、4G甚至8G大小的内存来说,只不过就是“盒子”多一点能装的01多一点而已,本质上和我们在这里展示的8字节大小的内存没有任何区别。

在后面的章节中我将用右图来表示内存,但是你的大脑里一定要有左图这样一个概念。当计算机在执行我们的程序时,无论是我们的机器指令还是机器指令操作的数据,都需要存放在这些小盒子中(内存)。

img

以上就是从硬件角度来看内存,那么从编程语言上来看,程序员应该如何理解内存呢?

C/C++内存模型

对于C/C++程序员来说,常用的int,char等变量都被装在盒子中,char值只需要一排盒子就能装下(8字节),一个int值一般需要四排盒子才能装得下。连续几排装有同样类型变量的盒子就是数组(array),连续几排装有不同类型变量的盒子就是结构体(struct),C/C++语言中不管多么复杂的数据结构都是在此基础上构建出来的,都需要装在这些盒子里,没什么大不了的。

现在你已经知道了对于C/C++程序员来说,我们使用的变量是直接放在内存中的(盒子),每一排盒子的地址就是我们熟知的“指针”,请记住,指针就是你使用的变量在内存中的地址,仅此而已。

C/C++程序在被执行时,需要在内存中划出两段区域用于存放数据,这两个区域就是我们熟悉的堆(Heap)和栈(Stack),也称堆区和栈区,如图所示,其中数据段和代码段我们已经熟悉了(不熟悉的同学请参见链接器系列文章),在这里我们将进一步完善C/C++程序在内存中的样子,如图所示,其中堆区紧邻数据段,在数据段之上,而栈在最上方,栈和堆之间是尚未被使用的内存,随着程序的运行,当程序申请内存时栈区和堆区之间的空隙会减小,当程序释放内存后空隙会扩大,这就是C/C++程序的内存模型。

img

每个函数运行时都会在栈区上占用一块内存,这块内存中保存的是调用函数的参数以及函数中的定义的局部变量,这些变量在函数调用完成后会被释放。从这里可以看出栈上的变量无需程序员关心其释放问题,当函数调用完毕后会自动释放所占用的空间。

和栈上的变量不同的是,堆上分配的内存不会像栈一样被自动释放,在堆上分配的内存需要程序员手动释放,如果程序员在堆上分配了一块内存,但在使用完后忘记释放,这种情况就被称之为“内存泄漏”,所谓“内存泄漏”就是使用完毕后的内存没有释放掉,但是这块内存也不能被用作其它地方从而导致堆占用的内存不断增大,表现出来的就是如果我们去检测程序所占用的内存,会发现程序所占用的内存不断增大,当操作系统是不可能坐视某个进程不断吞噬掉系统内存的,当出现系统内存资源不足时将触发操作系统的保护机制,这在Linux中就是著名的OOM Killer,即Out Of Memory Killer,OOM Killer会根据一些策略Killer有问题的进程,这个进程通常都是占用内存最多的那个。

下面我们用一小段C代码来实际演示一变量是如何在堆区栈区上分配的,不用担心,这段代码非常简单:


include <stdlib.h>

void f2() {
    int c;
    int* heap;

    c = 3;
    heap = (int *)malloc(sizeof(int));
    *heap = 4;
}

void f1() {
    int b;
    
    b = 2;
    f2();
}

int main() {
   int a;
   a = 1;

   f1();
   return 0;
}

如图所示,这就是以上代码运行过程中的样子,你会发现,每个函数在被执行的时候都在栈区上占有一小段,在这一小段中存放当前函数中定义的局部变量和传入函数的参数。每个函数所占用的这一段内存有一个很形象的名字,叫做“栈帧(stack frame)”,原因就在于栈是随着函数调用一帧一帧增加的,每个函数在被调用时都会在栈上分配一帧,所以就叫栈帧。这个词请大家不必去深究,每个被调函数在栈区上做占用的内存总要有个名字,栈帧只不过比较形象而已。

这段代码中,main函数会调用函数f1,f1会调用函数f2(),其中变量a,b,c以及heap依次被放在各自函数的栈帧中,值得注意的一点在于,heap这个变量本身是在栈上的,但是heap所指向的内存是分配在堆上的,heap本身仅仅保存的是4这个值在内存中的位置,比如这里的0x10,表示的就是4这个值放在了内存0x10的这个位置上,heap就是C/C++语言中所谓的指针。如图所示:

img

你会发现随着函数的调用,栈是不断在扩大的,当f2,f1执行完毕返回main时就是如下图所示的样子。

从图中我们可以看出,f2在执行完毕后,f2所占用的内存就被回收了,所谓“回收”就是这块内存又可以用作其它用途了。f1执行完毕后所占用的内存同样也被回收,这样我们就又回到了main()函数中。

这个过程中我们还会发现一个很有意思的现象就是最先被使用的栈帧其实是最后才被释放的,这种先进后出的性质就被称之为“栈”,如下图所示。所以你会看到“栈“这个词更多的是指顺序上的先进后出,只不过函数调用时所占用的内存在使用方式上也是先进后出的,所以这块内存就被称之为栈区了。

img

在讲解完栈之后,我们来看看堆,不同于像a,b,c这样存在于栈区上的变量,栈区上的变量可以在函数执行完成后被自动释放掉,在堆区上的分配内存除非程序员手动调用free,delete明确的告知内存使用完毕,否则这块内存就会一直被占用而不能用作其它用途,这就是堆区。

你可能会问,什么样的变量在需要在堆上分配呢,我们知道,函数调用完成后栈上的分配的局部变量会因为栈帧被释放而不再可用,堆区的存在就是为了解决这个问题,堆区中申请的内存不会因为栈帧的释放而不再可用,使得变量的生命周期不再局限于某个函数,其生命周期是靠程序员用malloc(new)以及free(delete)来控制的,这样的变量在使用时可以跨越函数调用。

另外一点值得注意的是,f2函数中我们在堆上申请了一块内存用来存放整数,但是f2执行完成后并没有去释放这块内存,根据堆的性质我们知道这块函数在接下来的运行过程中无法再被使用,就好像这块内存被遗忘了一样,这就是内存泄漏。

后续文章见《彻底理解操作系统:程序员应如何理解内存(2)》,如果你喜欢这篇文章,欢迎关注微信公共账号:码农的荒岛求生,获取更多精彩内容。

在这里插入图片描述
彻底理解操作系统系列文章
1,什么程序?
2,进程?程序?傻傻分不清
3,程序员应如何理解内存:上篇
4,程序员应如何理解内存:下篇
 
 

计算机基础决定程序员职业生涯高度

堆区与栈区的本质
Jave、Python内存模型
Jave内存模型
Jave中堆与栈是如何实现的
Python内存模型
指针与引用
进程的内存模型
幻想大师——操作系统
总结
发布了38 篇原创文章 · 获赞 30 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/github_37382319/article/details/99688821