当前位置: 首页 > article >正文

GC书籍笔记

1. 垃圾回收评价标准

垃圾回收的目的是回收程序不在使用的对象所占用的空间。任何自动内存管理系统都面临三个任务:

  1. 为新对象分配空间

  2. 确定存活对象

  3. 回收死亡对象所占用的空间

安全性

这是首要考虑的因素,即在任何时候都不能回收存活的对象

吞吐量

也就是说,即便是同一 GC 算法,其吞吐量也是受 mutator 的动作左右的。评价 GC 算法

的吞吐量时,有必要把 mutator (生成对象,更新指针)的动作也考虑在内。

停顿时间

最大暂停时间指的是“因执行 GC 而暂停执行 mutator 的最长时间”。这么说可能比较难理解,请再看一遍图 1.7。最大暂停时间是 A~C 的最大值,也就是 B。

这种情况下就需要缩短最大暂停时间。然而不管尝试哪种 GC 算法,我们都会发现较大的吞吐量和较短的最大暂停时间不可兼得。所以应根据执行的应用所重视的指标的不同,来分别采用不同的 GC 算法。

空间开销

​ 某些垃圾回收器需要再每个对象内部(引用计数法)占用一定的空间,也可能引入堆级别的空间开销(标记-复制法),还有些需要额外的辅助的数据结构(bitmap)。

因为 GC 是自动内存管理功能,所以过量占用堆就成了本末倒置。堆使用效率和吞吐量,以及最大暂停时间不可兼得。简单地说就是:可用的堆越大,GC 运行越快;相反,越想有效地利用有限的堆,GC 花费的时间就越长。

完整性与及时性

理想情况下,垃圾回收应当是完整的,即堆中的所有垃圾最终都应当回收,但是这通常是不现实的,甚至是不可取的。衡量完整性更好的方法是统计所有的垃圾的最终回收情况,而不是单个回收周期的回收情况。

2. 回收算法

标记-清扫(mark-sweep),标记-复制(mark-copy),标记-整理(mark-compact),引用计数法是4中最基本的垃圾回收策略,大多数回收器可能会以不同的方式堆这些进行组合。

标记-清扫

回收过程分两个阶段。

第一阶段:追踪阶段(trace),即回收器从根集合(全局变量,寄存器等)开始遍历对象图,并标记(mark)所遇到的每个对象。

第二阶段:清扫阶段,即回收器检查堆中的每个对象,并将所有未标记的对象当做垃圾进行回收。

此方法是一种间接回收算法,它并非直接检测垃圾本身,而是先确定所有的存活对象,反过来判定其他对象都是垃圾。直接回收的算法的例子是引用计数法。

该算法的每次调用都需要重新计算存活对象的集合。

伪代码

mark_sweep()
{
    mark_phase()
    sweep_phase()
}

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

mark(obj)
{
    if(obj.mark == FALSE)
    	obj.mark = TRUE
    	for(child : children(obj))
    		mark(*child)
}

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
}

优缺点

优点:

​ 实现简单,易于其他算法相结合使用。与保守式算法兼容

缺点:

​ 碎片化;

​ 分配速度慢:因为每次都必须遍历空闲链表,也每次都需要查找垃圾;

​ 与cow(写时复制)不兼容

BiBOP

​ BiBOP法,Big Bag Of Pages,将大小相近的对象整理成固定大小的块进行管理的做法。有点像重新聚合。

​ 我们可以用这个方法:把堆分割成固定大小的块,让每个块只能配置同样大小的对象。示意如下:

位图标记法

​ 标记的位分配到各个对象的头中(存在一定的风险),最好使用一个独立的位图维护标记位,即将数据与用户数据分开单独存放。位图可以有一个,也可以有多个。数据结构可以用数组,散列表,树等表示。

​ 优点:与写时复制兼容,不再需要遍历整个堆

​ 伪代码

mark(obj)
{
    obj_num = (obj - $heap_start) / WORD_LENGTH
    index = obj_num / WORD_LENGTH
    offset = obj_num % WORD_LENGTH
    
  if(($bitmap_tbl[index] & (1 << offset)) == 0)
	$bitmap_tbl[index] |= (1 << offset)
	for(child : children(obj))
		mark(*child)

}

延迟清除法

​ 标记过程的时间复杂度是O(L),L是堆中存活对象的数量;

​ 清扫过程中的时间复杂度是O(H),其中H为堆空间的大小。

​ 由于H>L,很容易误认为清扫阶段的开销是整个标记-清扫过程的主要部分,但实际情况并非如此。标记阶段的指针桌子的内存访问模式是不可预测的,清扫过程中的可预测性则要高的多。

​ 通过对象及其标记位存在两个特征:

​ 第一:一旦某个对象成为垃圾,那么永远是垃圾

​ 第二:赋值器永远不会访问对象的标记位,因此在赋值器工作的同时,清扫器可以并行修改标记位。

所以可以使用懒惰清扫法。主要流程如下:在回收过程,回收器依然将堆中的所有存活对象标记,但标记完成之后并不给予清扫整个堆,而是简单的将完全为空的内存块归还给分配器,同时将其他的内存块添加到其岁对于的空间代销分级的回收队列中。可与清除-复制算法结合使用。

new_obj(size){
    chunk = lazy_sweep(size)
        
    if(chunk != NULL)
    	return chunk
        
    mark_phase()
    chunk = lazy_sweep(size)
    if(chunk != NULL)
    	return chunk
    allocation_fail()
}

lazy_sweep(size){
	while($sweeping < $heap_end)
    {
		if($sweeping.mark == TRUE)
			$sweeping.mark = FALSE
		else if($sweeping.size >= size)
            chunk = $sweeping
            $sweeping += $sweeping.size
            return chunk
		$sweeping += $sweeping.size
    }
    $sweeping = $heap_start
    return NULL
}
//lazy_sweep() 函数会一直遍历堆,直到找到大于等于所申请大小的分块为止。在找到
//合适分块时会将其返回。但是在这里 $sweeping 变量是全局变量。也就是说,遍历的开始位
//置位于上一次清除操作中发现的分块的右边。

延迟清除法不是一下遍历整个堆,它只在分配时执行必要的遍历,所以可以压缩因清除操作而导致的 mutator 的暂停时间。这就是“延迟”清除操作的意思。


http://www.kler.cn/a/37390.html

相关文章:

  • 探索华为、思科和瞻博网络的基本ACL和高级ACL配置方法
  • js+html 象棋游戏 可以简单人机对战
  • 目标检测——目标检测概述
  • 【Docker】快速入门,带你快速了解 Docker
  • rust abc(5): 常量
  • 第八章 npm锁定版本
  • 【MySQL】SQL入门(一)
  • JavaWeb——Cookie和Session的工作流程
  • 一套电子病历系统源码(EMR)
  • 【从零开始学习JAVA | 第二十八篇】不可变集合
  • Ext4文件系统介绍 - 实战篇
  • 【swift 代码规范】
  • globals.hpp记录
  • 前端笔记5
  • 计算机网络 day3 广播风暴 - VLAN - Trunk
  • RabbitMQ系列(28)--RabbitMQ使用Federation Queue(联邦队列)解决异地访问延迟问题
  • 网络安全中黑客的问题,黑客真的那么厉害吗?
  • 【Matlab】智能优化算法_广义正态分布优化算法GNDO
  • k8s kubeadmin方式安装部署
  • 小程序:调用手机的相册