Skip to content

Latest commit

 

History

History
85 lines (38 loc) · 7.76 KB

Algorithm by Fan.md

File metadata and controls

85 lines (38 loc) · 7.76 KB

三个算法

GC 标记-清除算法

由标记和清除两个阶段构成。

标记阶段标记所有活动对象。从根直接引用的对象开始,利用深度(广度)优先算法递归地标记所有指针数组能访问到的对象。为了降低内存使用率,通常使用深度优先算法。

清除阶段清楚所有未标记的非活动对象。清除阶段从堆的起点开始,遍历整个堆,回收未被标记的对象,将已被标记的对象的标记抹去。

回收好的空间可以重新进行分配。包括First Fit、Best Fit、Worst Fit。分配完的小分块如果是连续的,可以合并成较大的一个。

优点:实现简单,不移动对象,与保守GC算法兼容。

缺点:分配时产生空间碎片,遍历链表导致分配速度慢,与写时复制技术(进程内存共享)不兼容。

对策:

分配速度慢:创建多个空闲链表——长度为1字节的空间、长度为2字节的空间.......长度大于100字节的空间(通常很少申请较大空间的分块)

碎片化:BiBOP法,即把堆分割成固定大小的块,特定大小的对象只能由同样大小的块分配空间。但这样可能导致很多同样大小的空间未被利用,未彻底解决碎片化问题。

不兼容写时复制:位图标记法,标记的位处于对象的头,使得更改标记位(即使不重写对象)也需要复制共享内存的数据,冗余复制过多。为此我们单独为标志位制作一张表格(每个字分配一位),标记是对表格进行操作。由于所有对象的位集中在一起,可以快速消除标志位。

清除花费时间过多:延迟清除法,即如果能用清除操作分配空间,则返回分块,否则先标记,再调用延迟清除函数进行分配。

引用计数法

每个对象拥有一个计数器,记录有多少个对象引用了自己。

计数器在以下情况进行改变:①新创建一个对象,该对象的计数器重置为一 ②当一个指针新指向一个对象的时候,它原来指向的对象计数器减一(为零时回收该对象,该对象引用的所有子对象引用减一),新指向的对象计数器加一。

优点:垃圾可以立刻回收,最大暂停时间短,不需要沿指针查找。

缺点:处理计数器增减开销过大,计数器占用空间较多,实现较为复杂,无法回收循环引用的对象。

改良:

延迟引用计数:处理计数器增减开销过大,很大一部分原因是根引用变化频繁。为此,根引用的对象变化不改变计数器,为了防止活动对象被回收,计数器为0的对象暂存在一个表里,需要释放内存时,扫描该表,将其中根引用的对象计数器加一,回收剩余计数器为0的对象。但这毁坏了立刻回收的性质,也加大了最大暂停时间。

Sticky引用计数法:主要是为了解决计数器占位太多的问题。实际由很多对象刚被创建就死亡,不必留太多位给计数器,计数器溢出的对象很少(这部分对象我们不做处理)。此外,还可以将标记-清除算法与引用计数法结合,标记阶段对计数器进行增量操作,清除阶段回收计数器为0的对象。它能回收计数器溢出的对象和循环的垃圾。

1位引用计数法:Sticky引用计数法的极端情况(观察发现几乎所有对象是不共有的),只留一位(可以称为标签)给计数器,计数器由指针持有。0代表只有1次引用,1代表有多次引用。指针操作仅有复制和删除(释放标签为0的指针所指的对象),这使得高速缓存不容易缺失。这种方法难点在于处理溢出的对象。

部分标记-清除算法:为了处理循环垃圾。只对有可能循环引用的垃圾使用标记-清除算法,其他对象使用引用计数法。对象有四种标记:绝对不是垃圾、绝对是垃圾、搜索完毕、可能是循环垃圾。通过遍历可能是循环垃圾的对象,递归访问其子对象,使其计数器减一,将最终计数器为0的对象(循环垃圾)回收,实现了对循环垃圾的处理。缺点在于遍历代价太高,极大延长了最大暂停时间。

分代垃圾回收

大部分对象生成后马上变成垃圾,据此特点,给对象引入年龄的概念,对不同代使用不同GC算法。本算法不能单独执行,需要和标记-清除等基本算法一起使用。下面给出两种经典的分代垃圾回收算法。

Ungar的垃圾回收

新生代:算法将堆分成四块——生成空间,两个幸存空间,老年代空间。生成空间满后,执行新生代GC算法,将生成空间和幸存空间(仅一个被使用)中的活动对象复制到另一个幸存空间中。对象存活一定时间后可晋升到老年代空间。为此,新生代对象头部需要包括年龄和是否复制完毕的标志位。

老年代:老年代可以采用普通GC回收算法,如GC标记-清除算法。

代之间的引用:分代垃圾回收将垃圾回收的重点放在新生代对象上,因此,检查老年代对象对新生对象的引用会降低效率,我们引入记录集(通常是数组的形式)来记录这种引用。为了防止新生代指针晋升后引用丢失,记录集记录引用的对象而非被引用的对象。需要特定函数来写入记录集,以确定是未被记录的老年代对新生代对象的引用(老年代对象头部需要有是否被记录的标志位)。如果引用过多,会导致溢出。也可以利用卡片标记的方式——将老年代空间等大分割,为每个空间设置标志位进行管理,空间有老年代对象引用新生代对象时,设置标志位。特别的,部分OS利用页面管理内存,将空间切割成页面大小,可以借助OS管理标志位。

代的分割 :实际上可以划分更多代。通常划分2~3代效果最好。

列车垃圾回收

堆的结构:这是与Ungar算法最大不同之处。新生代空间不做划分,老年代空间按照一定大小划分,空间之间按照产生顺序赋予编号,依次相连。这些空间也称之为车厢,列车由1个或更多的车厢组成。列车的记录集记录其他列车的引用,车厢的记录集包括来自同一列车的不同车厢的引用。写入记录集时,我们需要根据引用对象和目标对象所处的车厢和列车确定写入哪个记录集。

新生代:新生代空间满后,新生代GC算法将根或者老年代对象引用的新生代对象复制到老年代空间中。形象地说,我们把新生代对象和引用它的对象(如果是根,那么新建一个车厢,作为新列车)放在同一辆列车,列车最后一节车厢剩余空间足够,放在那节车厢里,否则新建一节车厢,搭在列车后面。

老年代:老年代GC算法对以开头列车的开头车厢作为对象。类似复制算法,准备一节空车厢作为目标空间,首先复制检查的列车车厢中所有根引用的对象,放到空车厢中。列车的所有车厢检查完后,如果列车记录集为空,没有来自别的列车的引用,可以回收整个列车。否则,根据访问记录集的结果,重新分配对象的存储位置来释放空间。

优缺点:一次老年代GC所访问的空间减小,降低了最大暂停时间,且能回收循环垃圾。缺点是写入记录集的代价提升,降低了吞吐量。

总结

优点:花费时间较长的老年GC算法执行少,新生代GC算法只面向刚生成的对象,提高了效率,吞吐量得到改善,

缺点:不是所有程序都符合该规律,对这些程序,分代回收算法只会起到反作用。