细说:程序运行的环境和运行过程,再看不懂请自行面壁( 二 )


 
对于不太常用的内存数据,操作系统会写到磁盘上,以便腾出更多可用的物理内存 。
当然,也存在没有操作系统的情况,这个时候你的程序所使用的内存就是物理内存,我们必须自己做好内存的管理 。
对于这个内存,该怎么用呢?
本质上来说,你想怎么用就怎么用,并没有什么特别的限制 。一个编译器的作者,可以决定在哪儿放代码,在哪儿放数据,当然了,别的作者也可能采用其他的策略 。实际上,C 语言和 Java 虚拟机对内存的管理和使用策略就是不同的 。
尽管如此,大多数语言还是会采用一些通用的内存管理模式 。以 C 语言为例,会把内存划分为代码区、静态数据区、栈和堆 。

细说:程序运行的环境和运行过程,再看不懂请自行面壁

文章插图
 
一般来讲,代码区是在最低的地址区域,然后是静态数据区,然后是堆 。而栈传统上是从高地址向低地址延伸,栈的最顶部有一块区域,用来保存环境变量 。
代码区(也叫文本段)存放编译完成以后的机器码 。这个内存区域是只读的,不会再修改,但也不绝对 。现代语言的运行时已经越来越动态化,除了保存机器码,还可以存放中间代码,并且还可以在运行时把中间代码编译成机器码,写入代码区 。
静态数据区保存程序中全局的变量和常量 。它的地址在编译期就是确定的,在生成的代码里直接使用这个地址就可以访问它们,它们的生存期是从程序启动一直到程序结束 。它又可以细分为 Data 和 BSS 两个段 。Data 段中的变量是在编译期就初始化好的,直接从程序装在进内存 。BSS 段中是那些没有声明初始化值的变量,都会被初始化成 0 。
堆适合管理生存期较长的一些数据,这些数据在退出作用域以后也不会消失 。比如,我们在某个方法里创建了一个对象并返回,并希望代表这个对象的数据在退出函数后仍然可以访问 。
而栈适合保存生存期比较短的数据,比如函数和方法里的本地变量 。它们在进入某个作用域的时候申请内存,退出这个作用域的时候就可以释放掉 。
讲完了 CPU 和内存之后,我们再来看看跟程序打交道的操作系统 。
2. 程序和操作系统的关系程序跟操作系统的关系比较微妙:
一方面我们的程序可以编译成不需要操作系统也能运行,就像一些物联网应用那样,完全跑在裸设备上 。另一方面,有了操作系统的帮助,可以为程序提供便利,比如可以使用超过物理内存的存储空间,操作系统负责进行虚拟内存的管理 。
在存在操作系统的情况下,因为很多进程共享计算机资源,所以就要遵循一些约定 。这就仿佛办公室是所有同事共享的,那么大家就都要遵守一些约定,如果一个人大声喧哗,就会影响到其他人 。
程序需要遵守的约定包括:程序文件的二进制格式约定,这样操作系统才能程序正确地加载进来,并为同一个程序的多个进程共享代码区 。在使用寄存器和栈的时候也要遵守一些约定,便于操作系统在不同的进程之间切换的时候、在做系统调用的时候,做好上下文的保护 。
所以,我们编译程序的时候,要知道需要遵守哪些约定 。因为就算是使用同样的 CPU,针对不同的操作系统,编译的结果也是非常不同的 。
好了,我们了解了程序运行时的硬件和操作系统环境 。接下来,我们看看程序运行时,是怎么跟它们互动的 。
程序运行的过程你天天运行程序,可对于程序运行的细节,真的清楚吗?
1. 程序运行的细节首先,可运行的程序一般是由操作系统加载到内存的,并且定位到代码区里程序的入口开始执行 。比如,C 语言的 main 函数的第一行代码 。
每次加载一条代码,程序都会顺序执行,碰到跳转语句,才会跳到另一个地址执行 。CPU里有一个指令寄存器,里面保存了下一条指令的地址 。
细说:程序运行的环境和运行过程,再看不懂请自行面壁

文章插图
 
假设我们运行这样一段代码编译后形成的程序:
int main(){int a = 1;foo(3);bar();}int foo(int c){int b = 2;return b+c;}int bar(){return foo(4) + 1;}我们首先激活(Activate)main() 函数,main() 函数又激活 foo() 函数,然后又激活 bar()函数,bar() 函数还会激活 foo() 函数,其中 foo() 函数被两次以不同的路径激活 。
细说:程序运行的环境和运行过程,再看不懂请自行面壁

文章插图
 
我们把每次调用一个函数的过程,叫做一次活动(Activation) 。每个活动都对应一个活动记录(Activation Record),这个活动记录里有这个函数运行所需要的信息,比如参数、返回值、本地变量等 。


推荐阅读