太久没有更新blog了,惭愧下先,下边进入正题。
最近在实现一个阉割版的函数式语言(编译器已完成,虚拟机在憋的过程中,之后会发帖),希望内存管理的部分由虚拟机来完成,而不是由客户自己折腾,这样显然可以降低客户使用这门语言的难度,比如不需要担心内存泄漏,减少内存运用不当的bug,这里客户只管放心地一直申请内存就行,不再需要手工释放内存(如果让客户选择是否自己释放都行,会提高难度,起码得做个平衡树放着这些申请的内存地址便于查找,这里先偷懒)。
首先讲一下虚拟机的内存管理机制。众所周知,频繁地申请和释放内存会大大降低效率,所以我们可以在虚拟机运行一开始的时候就统一申请内存,虚拟机运行结束在统一释放内存。如果中间有某些内存已经不需要再次使用,继续占着茅坑不拉屎是非常不道德的,于是需要对此做点事情,造成内存已释放的“假象”。这里强调一下,虚拟机知道哪块内存正在使用、哪块内存不需再用就行了,实际上程序吃的内存还是没有减少。声明一下,下文提到的申请和释放内存讲的都是“假象”。
如何实现这个机制呢?本能地想到引用计数,记录下到底有多少个对象引用这块内存,如果引用计数为0就释放内存。后来才意识到引用计数是行不通的,万一有环形数据结构(如一个对象自己引用自己),显然会造成内存泄漏。然后就动了对象池的念头,如果用这种方法,需要很多个对象池,包括虚拟机运行中的表、堆栈里的各种数据类型的对象,这么多个对象池看着很碍眼,于是还是决定实现垃圾收集器。
下面介绍垃圾收集器的工作机制。内存单元并不会在变成垃圾的同时就立刻被回收,而是保持不可到达的状态,直到内存被耗尽或者内存分配达到某个阈值,用户程序会被挂起,此时才进行垃圾收集,也就是先标记正在使用的内存,再清除垃圾,即内存回收。垃圾收集器回收足够的内存后,用户程序就可以继续工作。如国无法恢复足够多的内存,则抛出异常。
1. 标记
标记是为了识别垃圾,依靠对所有存活对象进行一次全局遍历来确定哪些内存可以回收。这个遍历从根出发(运行时的栈和表),利用相互引用关系,标记所有存活对象,除此之外,其他内存就是垃圾。这里强调下,标记并不会沿着已经被标记的单元追踪下去,这确保了标记能够终止。
2. 缩并
用户程序在堆上分配多种大小不同的对象,经过标记,我们发现,堆空间变得破碎,如果不扩展堆,也许就无法分配一个大型对象,因为找不到一个足够大的“空洞”容纳新的对象 ,即使空闲内存的总量是足够的。还有另外一个问题,经过若干个垃圾收集周期后,分配一个小型对象要采用什么算法?首次匹配代价低点,但是会产生更多碎片;最佳匹配产生的碎片少了,但是耗费的代价高。这显然提高了内存分配的难度。基于以上的讨论,对内存进行缩并就是自然的事了。即把空闲的内存扫到一起,也把正在使用的内存扫到一起,这样就把堆空间分成了两部分。这样空闲的内存连续了,也就解决了内存碎片的问题。当为新对象分配内存时,再也不用寻找合适的“空洞”,只需把记录空闲内存的基点后移,大大提高了内存分配的效率。
3. 分代收集
我们先有这样的假设:大多数对象都在年轻的时候死亡,而越老的对象则越不可能死亡。在垃圾收集的过程中,用户程序是被挂起的,如果对整个堆都进行垃圾收集,显然用户程序等待的时间是很长的。如果我们能把工作集中在回收最有可能是垃圾的对象上,就能让内存回收的效率更高,对用户程序的影响更小。
基于以上假设,我们需要区分年轻对象和年老对象。把对象按年龄分到堆中的不同区域里,其中最年轻的分代会被频繁地收集,而较老的分代则收集频率会低得多。对象首先在最年轻的分代里分配,如果它们的寿命足够长,能够在足够多次收集中存活下来(这里就是一次),则会被提升到较老的分代里。这里注意一下,较老的对象可能引用了较年轻的对象,我们仍需对此进行扫描,但是我们现在不必把年老的对象从一个半区复制到另一个半区了,将垃圾收集器的努力集中在回收最年轻的分代所占据的内存上,节省了用户等待的时间。
posted on 2010-05-14 12:59
Lyt 阅读(2169)
评论(3) 编辑 收藏 引用 所属分类:
垃圾收集器