这是2004/11/25应CSDN要求写的,署名是beepbug。
O.S.以两种方式为应用里的对象提供存储空间。一是静态空间分配,二是动态空间分配。
在生成目的码时,根据不同的编译开关,生成不同存储模式的目的码。这些目的码在运行时,可能只占一个存储段,兼作代码段与数据段。或一个代码段加一个数据段,或一个代码段加几个数据段,或几个代码段加几个数据段(Turbo C 2.0也有六种模式)。这是系统invoke程序时一次性为进程分配的存储空间。当进程终止时,O.S.按进程的存储模式收回这些存储段。这就是静态分配与回收。
C使用这些静态存储空间,不必申请,可直接在里面建立对象。用完了也不必回收。反正就是这么一个固定的空间,闲着也是闲着。当超出该模式的空间时,会有链接时错或运行时错。这时,通常你不用修改源码,只要重新编译一次,在编译时选择较大模式的模式开关即可。
编译-链接时的模式开关唯一地决定了进程静态空间的大小。而这个静态空间从进程的生至死固定不变。这就是静态的概念。
从以上的分配方式不难看出,这种方式存在一些问题或不足。
一是有大的浪费。譬如,系统的存储段是64KB大,我要处理的数据量正好比64KB多一个字节,进程就要占两个数据段。
其次,譬如我先要从某文件里调入处理一大块数据,然后写回文件,再从另一文件调入结构完全不同的一大块数据做处理。尽管前一处理已完毕,可空出来的空间并不能用于后一处理。在链接时,这两块空间要同时留出。这个浪费更大。
第三,在C(C++)里,在静态空间里定义的实体(跟着叫静态实体)有两种:自动的和外部的。前者的作用域是某函数或某语句块,生命期是临时的,是该函数或语句块被执行时。后者的作用域是单个源文件,或几个文件,或整个工程,生命期(lifetime)是永久的,即直至进程终止。有时,我们希望实体的生命期是受控的(可随心所欲,要它啥时生啥时死都行)。静态实体做不到。
早期的机器主存是很小的。据说,研发第一个C的PDP-11机只有18KB。可能是这些原因,早期的C就有了动态空间机制。
有了动态空间分配管理机制,进程在运行时,就可以向O.S.申请额外的空间。代码里的new(早期C的malloc()等)就用来向O.S.申请一块动态空间。与静态对象不同,放在动态空间里的对象没有标识。有人奇怪,为什么动态对象没有名称?其实是没法为它取名。因为编译器管不着。编译器能管到那儿?有些书说,能管到目的码生成为止。不对。它可以管到进程建立时为止。再往后,那是无论如何不行了。因此,由于动态对象是在进程运行期间生成的,实在没办法给动态对象加标识。但是,你可以有指向动态对象的指针(从低层看是new操作取出来的)。用这个指针,你就可以间接访问动态对象。
动态还有第二个意义,就是它不是固定大小的。因此,你可以(也应该)需要多少申请多少。
第三点是,你要用时要申请分配,不用时也要申请回收。否则会造成memory leak(一般译作内存泄漏,我喜欢叫遗漏,好像这样贴切一点)。关键字delete就用来申请回收。
不管是静态的还是动态的,空间分配和回收(即空间管理)都是O.S.的事。C和后来的C++都从未参与过空间管理(其它语言也一样)。一是这事太累,连巨型机里的C都不愿做那事。二是O.S.和进程都管,变成一仆(存储器)两主(O.S.和应用),要打仗的。有人认为,都管,O.S.管底层,应用管上层。不对。在空间里建立对象是使用空间,不是管理空间。使用和管理是两个完全不同的概念,不是上下级关系。进程是使用,O.S.是管理。早先的alloc、malloc、free等也是向O.S.申请动态空间而已,不是分配。人们看到、用到alloc、free多了,久而久之,就以为是他的程序在分配和回收内存。非也!你仅仅是在申请而已。
静态空间的回收比较简单,在进程生成时系统按模式分配,在进程消亡时按模式回收。即使发生异常,进程变成了僵尸,问题也不大。在shell级用实用工具kill,或在进程级用系统调用kill(),就可以清除它,并回收它所占有的空间。
比较复杂的是动态空间的回收。系统难以弄清(更没法及时弄清)哪个进程的哪块动态空间不用了。这事,在2003年有点热。有人开发实用工具做garbage collection,也有人把garbage collection嵌入到某运行或开发平台里。
为什么Java的遗漏问题比C++好得多呢?Java在开发初期就考虑了这个问题。虚机器里可以嵌入garbage collection。
随着运行平台越来越复杂、庞大,空间回收的问题将越来越多样化,也将越来越受到关注。
O.S.(操作系统)也有垃圾回收garbage collection问题。安卓没有garbage collection机制,所以越用越卡。苹果的iOS有,鸿蒙的garbage collection机制比iOS完备。