垃圾回收的算法与实现

168 阅读3分钟

1、基础知识

1、GC中对象的定义

对象是什么?表示的是通过应用程序利用的数据的集合

对象配置在内存空间里。GC根据情况将配置好的对象进行移动或者销毁。因此对象是GC的基本单位

对象由头(header)和域(field)组成

1.1对象头

保存对象本身的信息:

  • 对象的大小
  • 对象的种类

这两个内容用来确定内存中存储对象的边界

1.2、对象域

对象使用者在对象中可访问的部分称为"域",包含两部分:

  • 指针
  • 非指针

指针是指向内存空间中某块区域的值。非指针指的是在编程中直接使用值本身。数值、字符、以及真假值都是非指针

image.png

2、指针

GC根据对象的指针搜寻其他对象。两点前提:

  • 语言处理程序能判别指针和非指针
  • 指针默认指向对象的首地址

3、堆

堆指的是执行程序时存放对象的内存空间。当应用程序申请存放对象时,所需的内存空间就会从堆里面分配
GC是管理堆中已经分配的对象的机制

4、评价GC标准

  • 吞吐量
  • 最大暂停时间
  • 堆使用效率
  • 访问的局部性

吞吐量:单位时间内的处理能力。mutator(应用程序)

image.png GC吞吐量 = (堆大小) / (A+B+C)

最大暂停时间:在执行GC的时候是StopTheWorld的,这个时间值

堆使用效率:

访问的局部性:具有引用关系的对象之间通常很可能存在连续访问的情况 PC上有4中存储器,分别是寄存器、缓存、内存、辅助存储器。

image.png

2、GC 标记 - 清除算法

该算法由标记阶段 + 清除阶段组成。

2.1、标记阶段

mark_sweep()函数

mark_sweep() {
  mark_phase()
  sweep_phase()
}

image.png

mark_phase()函数

mark_phase() {
  for(r : $root) 
    mark(*r)
}

collector会为堆里的所有活动对象打上标记。首先通过根直接引用的对象。然后递归地标记通过指针数组能访问到的对象,这样就可以把所有活动对象都标记上

mark(obj) {
  if(obj.mark == FALSE)  // 检查实参的obj是否被标记,为了避免重复标记
     obj.mark = TRUE  // 对象头部进行位操作置位操作
     
     for(child : childern(obj)) 
       mark(*child)
 }

image.png

标记完成后:

image.png

  • 深度优先搜索与广度优先搜索

image.png

2.2、清除阶段

collector遍历整个堆,回收没有打上标记的对象 sweep_phase() 函数:

sweep_phase(){
   sweeping = $heap_start  // 从堆首地址开始遍历对象标志位
   while(sweeping < $heap_end)
     if(sweeping.mark == TRUE)   // 对象标志位
         sweeping.mark = FALSE   // 取消标志位,等待下一次GC时设置该值
     else
         sweeping.next = $free_list // 对象连接到"空闲链表"的单向链表
         $free_list = sweeping
     sweeping += sweeping.size 
}

image.png

2.3、分配

被清除的对象已经连接到空闲链表了,搜索空闲链表并找寻大小合适的分块,就叫分配(new_obj())

new_obj(size) {
  chunk = pickup_chunk(size, $free_list) 
  if(chunk != NULL)
    return chunk 
  else
    allocation_fail() // 未找到合适的分块

chunk = pickup_chunk(size, $free_list) 遍历$free_list,寻找>=size的分块,如果它找到和 size 大小 相同的分块,则会直接返回该分块;如果它找到比 size 大的分块,则会将其分割成 size 大 小的分块和去掉 size 后剩余大小的分块,并把剩余的分块返回空闲链表

First -fit、Best -fit、Worst -fit 的不同.
之前我们讲的分配策略叫作 First -fit。因为在 pickup_chunk() 函数中,最初发现大于等于 size 的分块时就会立即返回该分块。
然而,分配策略不止这些。还有遍历空闲链表,返回大于等于 size 的最小分块,这种策略叫 作 Best -fit。
还有一种策略叫作 Worst -fit,即找出空闲链表中最大的分块,将其分割成 mutator 申请的 大小和分割后剩余的大小,目的是将分割后剩余的分块最大化。但因为 Worst -fit 很容易生成大 量小的分块,所以不推荐大家使用此方法。
除去 Worst -fit,剩下的还有 Best -fit 和 First -fit 这两种。当我们使用单纯的空闲链表时, 考虑到分配所需的时间,选择使用 First -fit 更为明智。

2.4、合并

分配策略不同可能会产生大量的小分块,连接连续分块的操作就是"合并",合并是在清除阶段进行的

合并函数:sweep_phase()

sweep_phase(){
  sweeping = $heap_start 
  while(sweeping < $heap_end)
    if(sweeping.mark == TRUE) 
       sweeping.mark = FALSE
    else
      if(sweeping == $free_list + $free_list.size) // 判断发现的分块和上次分块是否连续
         $free_list.size += sweeping.size  // 合并分块
      else
         sweeping.next = $free_list
         $free_list = sweeping 
   sweeping += sweeping.size
}

2.5、优缺点

优点:对象不会移动 缺点:碎片化严重;空闲空间不连续导致分配时遍历链表耗时长;与写时复制技术不兼容