简述 - 垃圾回收算法

垃圾回收算法

概念

垃圾回收(Garbage Collection),简称GC,主要是对那些已经分配的空间进行回收利用,实现再分配.

比如下面的Java代码

public void test() {
	List<Integer> list = new ArrayList<>();
	list.add(1);
	System.out.println(list);
}

在这个函数中,我创建了一个ArrayList对象,Java则在内存堆上为其分配空间.但是一旦出了这个函数,我们就再也访问不到这块空间了,如果不进行回收,那么内存堆的这个位置就浪费了.

所以GC要完成的主要有两件事:

  • 找到内存中的垃圾
  • 回收这部分垃圾,以便之后可以再分配

关于对象在堆上的布局

一般一个对象在堆中存储两部分信息,一部分是对象的元数据,比如对象的大小,hashcode的值以及用来标识垃圾回收的信息等.另一部分就是对象的具体数据.这部分主要分两种,如果是基本数据类型,那么这个位置存的就是它的值.如果不是,那么这个位置存放的是指向具体内存地址的指针.

image-20210322220230929

如何找到内存中的垃圾

这里采用可达性分析,首先我们会将一些对象作为根,这些对象我们把它标记为可达的,然后由这些根所能指向的其他对象,我们也标记为可达…这样一直传递下去,到最后,我们可以遍历整个堆,然后把那些不可达的对象给回收掉.

image-20210322222228195

哪些对象可以作为根呢 —> 调用栈、寄存器以及全局变量

不过,这些根可能还有潜在的问题. 怎么判断这个值是指针的地址还是一个基本数据类型呢?

image-20210322232620392

就概率上讲,数值刚好位于堆地址范围内,且刚好是对象头的地址,情况还是比较少的. 我们可以将错就错,不过在这种情况下,可能一个本该回收的对象就被我们列为根了.

当存在貌似指针的非指针时,保守式GC会把被引用的对象错误识别为活动对象。如果这个对象存在大量的子对象,那么它们一律都会被看成活动对象

要严格区别指针与非指针,需要语言处理程序的一些支持.这一块不作展开.

GC算法评价指标

  • 吞吐量

如堆的大小为HEAP_SIZE,GC总共花费的时间为(A+B+C),则吞吐量为HEAP_SIZE/(A+B+C)。

  • 最大暂停时间

最大暂停时间指的是“因执行GC而暂停执行mutator的最长时间”,比如下图指的就是B. 如果暂停时间太长,就会出现Stop-The-World

image-20210322223611521

  • 堆使用效率

不同的GC算法,会在堆中对象的对象头存放一些信息,大小不一,或者会将堆做划分.

  • 访问的局部性

我们知道计算机是有高速缓存这么个东西.一般而言,具有引用关系的对象之间通常很可能存在连续访问的情况。所以如果在堆中这两个对象存放位置连续,那当读取第一个对象时,后面的对象也跟着被读进了缓存,后续访问的速度就加快了.

GC算法介绍

标记清除算法

这个其实在前面也有提到,从根集合出发,将能访问到的对象打上标记,然后遍历堆,将没有标记的对象清除.伪代码描述如下:

mark_sweep() {
	//标记阶段
	for(r : $roots) {
		mark(*r)
	}
	//清除阶段
	sweeping = $heap_start
	while(sweeping < $heap_end)
		if(sweeping.mark == TRUE)
			sweeping.mark = FALSE
		else
			sweeping.next = $free_list
			$free_list = sweeping
		sweeping += sweeping.size
}
//标记函数
mark(obj) {
	if(obj.mark == FALSE) {
		obj.mark = TRUE
		for(child : children(obj))
			mark(*child)
	}
}

这种方式不会挪动对象在堆中的位置,但是容易产生内存碎片.关于回收后的内存如何分配使用,可以找更相关的资料.比如参考malloc库的实现,在<<深入理解计算机系统>>一书中也有所涉及.

引用计数法

顾名思义,在对象头记录有多少个引用了这个对象.当计数变为0的时候,我们就可以立即对这部分内存进行回收.

它的最大暂停时间短,但是存在多个无用对象循环引用无法释放内存的问题. Python的GC采用了引用计数法,可以去了解它如何解决循环引用的

标记复制算法

这种算法把堆划分为两部分,每次分配对象的时候只使用其中一半.当标记过程结束后,把存活的对象全都挪到堆的另一半中.

这种的好处是避免了内存碎片,因为每次都有一整块的空闲空间用于分配对象.但显然堆的使用效率降低了

注意

在这种算法中,由于挪动了对象,所以原先指向这些对象的指针,要做一个映射,使其访问到新的内存位置. 解决方法可以采用句柄的方式间接处理对象. (采用指向指针的指针也可以)

image-20210322232131923

通过引入句柄,当我们移动对象的时候,只需要修改句柄里的指针,而不用更新根的值.

标记整理算法

这种算法可以理解为先执行了标记清除算法,然后把存活的对象都挪到一起,这样空闲的空间就是连续的了.

image-20210322231857408

image-20210322231906519

同样的,由于移动了对象,所以指针的访问也要做些修改.

分代回收

在对象头部分多了对象的存活年龄,据此划分了新生代,老年代对象.在堆中也是划分存储.

根据经验显示,新生代的对象存活率较低,所以这部分会有大量对象回收.对这部分采取标记复制算法收益比较好;

而针对老年代对象,则采用标记清除算法. 当需要GC时,优先从新生代入手.

当我们对新生代进行GC时,如果有老年代的指向新生代的引用,怎么办?

如果遍历老年代去标记,那其实又跟遍历整个堆没区别了.

所以这里引入了记录集写入屏障

记录集上面保存了老年代对象的指针. 它是在我们更新对象指针的时候,通过写入屏障的方式加入的.

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
}

如上述伪代码那样,当老年代对象有某个属性指向新生代对象时,将其加入记录集中.

这样的好处是当新生代对象的内存位置移动时,能够知道要更新哪个老年代对象的指针

image-20210323203741382

增量式垃圾回收

上面提到的GC都有一个问题,它们在进行垃圾回收时会阻塞主线程,直到垃圾回收结束.

image-20210323204218375

这里主要讲一种增量GC的算法,它的GC要分好几个阶段完成,这样保证最大暂停时间变小.

image-20210323204322491

三色标记算法

这是Edsger W. Dijkstra等人提出的三色标记算法(Tri-color marking),它将每个对象都标记了一个颜色:

  • 白色:还未搜索过的对象

  • 灰色:正在搜索的对象

  • 黑色:搜索完成的对象

GC开始运行前所有的对象都是白色。GC一开始运行,所有从根能到达的对象都会被标记,然后被堆到栈里。GC只是发现了这样的对象,但还没有搜索完它们,所以这些对象就成了灰色对象。灰色对象会被依次从栈中取出,其子对象也会被涂成灰色。当其所有的子对象都被涂成灰色时,对象就会被涂成黑色。当GC结束时已经不存在灰色对象了,活动对象全部为黑色,垃圾则为白色。


它可以分为三个阶段: 根查找阶段,标记阶段,清除阶段

根查找阶段把能直接从根引用的对象涂成灰色。

标记阶段查找灰色对象,将其子对象也涂成灰色,查找结束后将灰色对象涂成黑色。

清除阶段则查找堆,将白色对象连接到空闲链表,将黑色对象变回白色.

// 在标记,清除阶段中,其都是增量式的,即每次只标记或清除一定数量的对象后切回主线程.


这里还有几个需要注意的地方.

问题1

image-20210323210817028

如图,当A标记完所有子对象后,自己变黑. 此时切换回主线程,然后主线程的代码做了个操作,将B–C之间的引用删掉,然后加了A–C的引用. 这时再回来GC的时候,由于A已经黑了,所以不会再对其子对象做标记, 这样就会错过对象C, 这样会误回收对象!!

解决这个问题的办法同样使用写入屏障, 在添加A指向C的引用时, 如果C是白色的,就将其标黑.

write_barrier(obj, field, newobj) {
	if(newobj.mark == FALSE) {
		newobj.mark = TRUE
		push(newobj, $mark_stack)
	}
	*field = newobj
}

问题2

image-20210323211416289

如图,当GC开始清除阶段时,它会把没有标黑的对象给清除掉. 如果这时切换回主线程,而我们又要分配新的对象时就要注意了, 如果分配的新对象位置在$sweeping的右边, 那我们要把新分配的对象给标成黑色的,以防被误清除.

参考资料

<<垃圾回收的算法与实现>> ----- [日]中村成洋,[日]相川光

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值