目录算法
Generationanl GC数组
引入年龄的概念,优先回收年轻的已成为垃圾的对象。函数
书上说:“人们 从众多案例总结出一个经验:‘大部分的对象再生成后立刻就变成了垃圾。不多有对象活的好久’。”,分代,引入年龄概念,经历过一次GC的对象年龄为一岁。操作系统
分代垃圾回收中,将对象分为几类(几代),针对不一样的代使用不一样的GC算法。刚生成的对象称之为新生代,到达必定年龄的对象称为老年代对象。3d
咱们对新生代对象执行的GC称为新生代GC(minor GC)。新生代GC的前提是大部分新生代对象都没存活下来,GC在很短期就结束了。新生代GC将存活了必定次数的对象当作老年代对象来处理。这时候咱们须要把新生代对象上升为老年代对象(promotion)。老年代对象比较不容易成为垃圾,因此咱们减小对其GC的频率。咱们称面向老年代对象的GC为老年代GC(major GC)。指针
分代垃圾回收是将多种垃圾回收算法并用的一种垃圾回收机制。code
Ungar分代垃圾回收中,堆结构图以下所示。总共须要四个空间,分别是生成空间、两个大小相等的幸存空间、老年代空间,分别用$new_start、$survivor1_start、$survivor2_start、$old_start这四个变量指向他们开头。对象
生成空间和幸运空间合称为新生代空间,新生代对象会被分配到新生代空间,老年代对象则会被分配到老年代空间里。Ungar 在论文里把生成空间、幸存空间以及老年代空间的大小分别设成了 140K 字节、28K 字节和 940K 字节。blog
此外咱们准备出一个和堆不一样的数组,称为记录集(remembered set),设为 $rs。递归
过程以下图所示:
记录集用于高效的寻找从老年代对象到新生代对象的引用。在新生代 GC 时将记录集当作根(像根同样的东西),并进行搜索,以发现指向新生代空间的指针。
不过若是咱们为此记录了引用的目标对象(即新生代对象),那么在对这个对象进行晋升(老 年化)操做时,就无法改写所引用对象(即老年代对象)的指针了。以下图示:
经过查找可知对象A时新生代GC的对象,执行GC后它升级为了老年代对象A'。但在这个状态下咱们不发更新B的引用为A',记录集里没有存储老年代对象 B 引用了新生代对象 A的信息。
因此记录集里记录的不是新生代对象,而是老年代对象。他记录的老年代对象都是有子对象是新生代对象的。这样咱们就能去更新B了。
记录集大部分使用固定大小数组来实现。那么咱们如何向记录集里插入对象呢?关于写入屏障内容。
将老年代对象记录到记录集里,咱们利用写入屏障(write barrier)。write_barrier()函数。
write_barrier(obj, field, new_obj){ if(obj >= $old_start && new_obj < $old_start && obj.remembered == FALSE) $rs[$rs_index] = obj $rs_index++ obj.remembered = TRUE *field = new_obj }
obj 是发出引用的对象,obj内存放要更新的指针,而field指的就是obj内的域,new_obj 是在指针更新后成为引用的目标对象。
最后一行,用于更新指针。
对象的头部除了包含对象的种类和大小以外,还有三条信息,分别是对象的年龄(age)、已经复制完成的标识(forwarded)、向记录集中记录完毕的标识(remembered)。
对象结构以下图示:
在生成空间里进行,执行new_obj()函数代码以下:
new_obj(size){ if($new_free + size >= $survivor1_start) minor_gc() if($new_free + size >= $survivor1_start) allocation_fail() obj = $new_free $new_free += size obj.age = 0 obj.forwarded = FALSE obj.remembered = FALSE obj.size = size return obj }
生成空间被对象沾满后,新生代GC就会启动。minor_gc()函数负责吧生成空间 和From空间的活动对象移动到To空间。
咱们先来了解minor_gc()中进行复制对象的函数copy()。
copy(obj){ if(obj.forwarded == FALSE) // 检测对象是否复制完毕 if(obj.age < AGE_MAX) // 没有复制则检查对象年龄 copy_data($to_survivor_free, obj, obj.size)// 开始复制对象操做 obj.forwarede = TRUE obj.forwarding = $to_survivor_free $to_survivor_free.age++ $to_survivor_free += obj.size// 复制对象结束 for(child :children(obj)) // 递归复制其子对象 *child = copy(*child) else promote(obj) //若是年龄够了,则进行晋级的操做,升级为老年代对象。 return obj.forwarding //返回索引 }
promote(obj){ new_obj = allocate_in_old(obj) if(new_obj == NULL) // 判断可否将obj放入老年代空间中。 major_gc() //不能去就启动gc new_obj = allocate_in_old(obj)// 再次查询 if(new_obj == NULL) //再次查询。 allocation_fail()//不能放入的话就报错啦。 obj.forwarding = new_obj // 能放入则设置对象属性 obj.forwarded = TRUE for(child :children(new_obj)) //启动GC if(*child < $old_start) // obj是否有指向新生代对象的指针 $rs[$rs_index] = new_obj // 若是有就将obj写到记录集里。 $rs_index++ new_obj.remembered = TRUE return }
minor_gc(){ $to_survivor_free = $to_survivor_start // To空间开头 for(r :$roots) // 寻找能从跟复制的新生代对象 if(*r <$old_start) *r = copy(*r) i = 0 // 开始搜索记录集中的对象$rs[i] 执行子对象的复制操做。 while(i<$rs_index) has_new_obj = FALSE for(child :children($rs[i])) if(*child <$old_start) *child = copy(*child) if(*child < $old_start) //检查复制后的对象在老年代空间仍是心神的古代空间 has_new_obj = TRUE //若是在新生代空间就设置为False不然True if(has_new_obj ==FALSE) // 若是为False,$rs[i]就没有指向新生代空间的引用。接下来就要本身在记录集里的信息了。 $rs[i].remembered = FALSE $rs_index-- swap($rs[i], $rs[$rs_index]) else i++ swap($from_survivor_start, $to_survivor_start) // From 和To互换空间 }
就以前介绍的GC都行,可是具体使用哪一个看想要的效果以及内存的大小来决定。通常来讲GC标记清除就挺好的。
经过使用分代垃圾回收,能够改善 GC 所花费的时间(吞吐量)。正如 Ungar 所说的那样:“据实验代表,分代垃圾回收花费的时间是 GC 复制算法的 1/4。”可见分代垃圾 回收的导入很是明显地改善了吞吐量。
“不少对象年纪轻轻就会死”这个法则毕竟只适合大多数状况,并不适用于全部程序。固然, 对象会活得好久的程序也有不少。对这样的程序执行分代垃圾回收,就会产生如下两个问题。
除此以外,写入屏障等也致使了额外的负担,下降了吞吐量。当新生代GC带来的速度提高特别小的时候,这样作很明显是会形成相反的效果。
Ungar的分带垃圾回收,使用记录集来记录各个代间的引用关系。这样每一个发出引用的对象就要花费1个字的空间。此外若是各代之间引用超级多还会出现记录集溢出的问题。(前面说过记录集通常是一个数组。)
Paul R.Wilson 和 Thomas G.Moher开发的一种叫作卡片标记(card marking)的方法。
首先把老年代空间按照等大分割开来。每个空间就成为卡片,听说卡片适合大小时128字节。另外还要对各个卡片准备一个标志位,并将这个做为标记表格(mark table)进行管理。
当由于改写指针而产生从老年对象到新生代对象的引用时,要事前对被写的域所属的卡片设置标志位,及时对象夸两张卡片,也不会有什么影响。
GC时会寻找位图表格,当找到了设置了标志位的卡片时,就会从卡片的头开始寻找指向新生代空间的引用。这就是卡片的标记。
由于每一个卡片只须要一个位来进行标记,因此整个位表也只是老年代空间的千分之一,此外不会出现溢出的状况。可是可能会出现搜索卡片上花费大量时间。所以只有在局部存在的老年代空间指向新生代空间的引用时卡片标记才能发挥做用。
许多操做系统以页面为单位管理内存空间,若是在卡片标记中将卡片和页面设置为一样大小,就可使用OS自带的页了。
一旦mutator对堆内的某一个页面进行写入操做,OS就会设置根这个也面对应的位,咱们把这个位叫作重写标志位(dirty bit)。
卡片标记是搜索标记表格,而页面标记(page marking)则是搜索这个页面重写标志位。
根据 CPU 的不一样,页面大小也不一样,不过咱们通常采用的大小为4K字节。这个方法只适用于能利用页面重写标志位或能利用内存保护功能的环境。
Multi-generational GC
将对象划分为多个代,这样一来能晋升的对象就会一层一层的减小了。