对象生命周期与内存模型(转载)

内存模型是随着越来越丰富和复杂的对象生命周期要求的发展而发展起来的。 

最初的内存模型完全是线性的,静态的,一个程序运行时所有需要的对象都是在运行前完全准备好了的,运行完了时释放掉。典型的代表就是Fortran语言。这种语言的运行性能非常高(当然了,没有任何别的消耗嘛),但是表达能力受到限制(毕竟,要求静态的确定一切对象和内存的绑定关系)。最明显的一个限制就是没办法支持递归。这种内存模型支持的对象的生命周期跟应用程序的生命周期完全一致。同生共死,天下大同。 

Alogal的出现引入了一个强大的概念: lexical scope,内存模型也相应的出现了细分概念:栈。栈就是那种先进后出的容器,它完美的切合了lexical scope。同时,栈上的对象的生命周期也很清晰,进栈是出生,出栈时死亡。栈这个概念是如此的清晰、容易理解,而且如此的强大,导致现代各种语言的内存模型中,栈都占有非常显著的地位。栈自然地解决了递归问题。栈上对象的生命周期的管理完全可以自动化而高效的完成。但是,栈仍然不足以满足某些对象生命周期的要求。 

我先说说这是那种样子的生命周期。简单的说,栈对于共享的对象的生命周期无能为力。也就是说,某个对象,在多个平级的或者嵌套的scope之间共享,而栈却没办法解决。非得用栈来满足这种需求的话,会导致大量的memcpy,导致性能的低下。而且还是模拟的解决这个问题。其实,对于参数和返回值,基于栈架构的内存模型和对象就是采用复制和反向复制的方式来完成的。 

面对这个挑战(共享对象生命周期问题),第一个反应就是避免共享,其实就是我前面说的完全副本的方式。这种方式在一定程度上有效,不过,对于大对象是非常不划算的。 

C 和Pascal语言来了,这类语言提供了另一个概念叫做堆(其实,堆这个概念并不是它们最先引入的,但是是由它们发扬光大的)。从此内存模型分裂为两个界限明显的类型:栈和堆。堆就是那种随机进随机出的对象的栖息之地。也就是说,这种对象的生命周期不再可以向栈对象那样自动高效的管理了。有了这样的基础设施,共享对象就变得简单了。创建一个堆对象,然后A使用之,只要不销毁,B就可以使用之,这样,该对象就可以由A和B共享了。当然,牵扯到共享,必然会涉及到同步和互斥的问题。也就是A和B究竟怎样访问该对象的问题。一般来说,采取的策略是是把并发访问串行化的技术。由此导出很多互斥的技术。我们就不在这上面纠缠了。我们关注的是对象本身的生命周期,也就是说。这个可以共享的对象的生、死问题。由谁负责生(创建)似乎大家都没有什么抱怨的,由第一个要用该对象的负责,这也不会导致麻烦。原因是:如果你不负责生,那么该对象就不存在,你也就没办法使用了。:)由谁负责死(销毁)呢。这个看起来也很简单:最后一个用完了就销毁。而事实上也确实就这么简单。但是这儿有一点麻烦。跟创建不一样,创建不会被忘记,而销毁会。忘记销毁对象对自己没有伤害,所以,就选择忘记吧。:)这不是拉屎不擦屁股的问题。这个不难受。 

这样说吧。现代的语言按照由谁负责堆对象的销毁问题大致可以分成两大类,一类叫做有垃圾回收的,一类没有。有垃圾回收的那一类是由运行环境(运行时)负责销毁共享对象,没有垃圾回收的那一类由程序自己负责销毁共享对象。对程序员来说,当然有垃圾回收的语言跟友好了。但问题是垃圾回收需要首先搞定哪一些共享对象不再需要了。这是一个比较困难的问题。而对于没有垃圾回收的那种语言来说,程序自己逻辑上应该很清楚哪一些对象不再需要了,可以销毁了。 

一般情况下,我们把那些由运行时负责销毁共享对象的那些堆叫做托管堆,负责销毁共享对象的运行时的一部分叫做垃圾回收器(GC),也就是说,托管堆就使那种委托给GC管理的堆。 
相应的,没有委托给GC管理的堆就是普通的堆了。普通堆有应用程序自己负责销毁共享对象。 

上面的描述还没有提到的一个问题是:对象究竟在什么时候销毁?由于一个对象不立即销毁一般不会导致什么重大的问题,所以销毁的时机相对来说可以很灵活。当然,肯定是在最后一个使用了以后。但是以后是多久呢?是立即销毁?还是延迟一段时间?如果是延迟,那么究竟延迟多长时间?还是更进一步的说延迟不定长的时间? 

由于一般情况下,我们认为批量销毁比一个一个销毁要快一些,所以一个指导性的方案就是等到垃圾(生命周期应该已经结束了的共享对象)积累到一定的量以后(只要不影响程序的正常运行)批量销毁。这一般会导致销毁的不定长延迟。 

上面说了:“由于一个对象不立即销毁一般不会导致什么重大的问题,所以销毁的时机相对来说可以很灵活。”。注意这个句子里面的“一般”这个字眼,这也就是说:在特殊情况下,不立即销毁会导致重大问题。那种情况是特殊情况呢?就是那种销毁对象的动作还带有别的副作用,而这个副作用会影响以后程序的运作行为的情况。这种情况在使用C++的RAII的时候是基本情况。 

现在粗略的说说垃圾回收的基本方法。 

我们把托管堆中的对象以及对象的互相引用关系看作是一个“图”(数学上定点和边的集合),应用程序的栈上有一些引用引用到这个图的某些顶点。从GC的角度来看,应用程序的作用就是不断地改变图的连通性,故此把应用程序叫做Mutator。GC就是通过查看图的连通性把那些孤立的顶点(就是那种生命周期结束的共享对象)回收的。 

那么,我们怎么知道图的连通性的呢? 

1、可以通过从根集跟踪。所谓根集,就是应用程序栈上的引用集合。我们知道,这种跟踪肯定可以搞定图的连通性,但是,这要求我们能够区分什么是引用,而什么不是。 
2、每次应用程序修改图的连通性的时候,记录下来。这样GC就可以直接销毁那些孤立的顶点了。 
第一种方式是最常用的方式,或者甚至可以这样说,第二种方式根本没人用。 

不过,第二种方式的某种变形方式用的人却很多。那就是RefCount。第二种方式把集中存放的连通图信息分散的放置到各个共享对象身上,让他们记住自己被多少个别的对象引用就行了。这样当它发觉自己被0个对象引用的时候,就自裁。 

这个方式看起来非常好,也非常干净,但是有两个缺点。一是性能。每一次引用一个对象,都需要增加这个引用计数,放弃引用的时候得减少之。影响了性能。另一个是对于环形图的无能为力。
posted @ 2011-03-16 09:00  小凤梨子  阅读(430)  评论(0编辑  收藏  举报