趣谈GC里的那些算法

首先,我们知道许多主流GC都是基于分代收集理论设计的,所以常把Java堆分为新生代和老年代(但这不是一个强制
规范,只是一种设计思想),本文也按照这种经典思想将Java堆分代。当然,你可能会说,都0202了,不应该直接
学习G1吗,私以为“循序而渐进”乃学习之正道,施主,急不得。

一、基础理论

根可达性分析

目前主流的GC在标记阶段采用的都是这种算法。
  1. 什么是引用计数法?它的劣势主要在哪?
  • 引用计数法的做法是让对象额外记录一个计数器,当有一个变量或其他对象引用它时,这个计数器自增1,如果一个对象没有被引用(计数器的值为0),就会被认定为垃圾。如果两个对象相互引用(对象内心OS:不要清理我们,我们深爱着对方),则它们不会被清理,但实际上对于线程而言,它们已经没用(秀恩爱死得快)了,因为线程不能通过引用访问到它们。
    图1 循环引用
  1. 那什么是根可达性分析呢?
  • 首先,得明确一下“根”的定义,由于标准定义过于冗长,难以记忆,这里简化一下,我们可以认为“根”是被JVM管理着的线程们(或者说是存活着的线程们)存储着的引用
  • 根可达性分析本质上就是DFS(Depth-First-Search,深度优先搜索),即从“根”(对象的引用)出发,一根筋地往下蹿,把遍历到的对象都标记为可用的。
    图2 根可达性分析
  1. 为什么要根可达性分析算法是主流?
  • 最重要的原因就是根可达性算法不会让1中所述的循环引用(我好像在说废话,因为它们根不可达)存在(其实这个算法是fff团设计的),即它不会因为自身算法的问题而导致未回收垃圾。

下面粗略地给出(怎么高兴怎么写)以上算法的实现:

// 遍历每个GCRoot的引用链
void isReachableFromRoots(GCRoots[] gcRoots) {
	for (GCRoots gcRoot : gcRoots) {
		// 遍历当前gcRoot的引用链
		sign(gcRoot.getRefs());
	}
}
// 标记(dfs)
void sign(Reference... refs) {
	if (refs == null || refs.isEmpty()) {
		return;
	}
	for (Reference ref : refs) {
		// 标记为有用对象
		ref.isReachable = true;
		// 遍历当前对象的引用集
		sign(ref.getRefs());
	}
}

注:下文的一些图片也用黑色表示对象可达,白色表示不可达(但是作为一个资深垃圾收集者,你可能会说,咱玩的不都是三色标记吗,啊,被发现了,下面会谈到~)

二、算法的内容与实现

复制算法

复制算法(严格来说是改进版的复制算法)就是针对新生代的。
  1. 什么是复制算法?
  • 现在我们有一块内存空间,线程们迫不及待地想要在上边放置自己的对象。
  • 但是聪明的线程管理者——JVM,不允许线程任意妄为,JVM将这块内存划成两个部分(实际上通常是三个部分,这里只关注“复制”环节),我们暂且分别称它们为小a小b吧。
  • 活泼的线程们很快就把小a填满了,这时JVM(GC)拍马赶到,开始了自己的擦屁股之旅:它把“垃圾”(根不可达的对象)清理了,然后把存活的对象挪到了小b。宝剑锋从磨砺出,梅花香自苦寒来,经过这一次历练,小b中的对象成长了(分代年龄+1)。
  • 现在小a空空如也,小b放着一些还有用的对象,然后线程们又开始在小b上放置对象了,于是下一个轮回开始了……
  1. 普通的复制算法有什么优点?缺点呢?
  • 从1中我们知道这种算法很简单,但你可能会说,这太浪费空间了啊!古人云:“鱼与熊掌不可兼得”,故普通的复制算法的优点就是实现简单不产生内存碎片(不连续的内存空间),缺点就是内存利用率低
    图3 复制算法
  1. JVM中的复制算法是怎么样的呢?
  • 前面提到,JVM通常把新生代划分为3个部分,它们分别是Eden区,From区和To区,其中Eden区是新对象优先被分配的地方(故事的开始,对象们快乐地生活在伊甸园),From区和To区对应的就是1中举例用的小a小b
  • 默认情况下,Eden:From:To=8:1:1,为什么这样分配呢?因为设计大佬们统计发现,98%的对象是会被回收的,也就是说,大部分对象其实在Eden区就被回收了,即能幸存下来溜到From区和to区的对象少之又少。
  • 增加了一个Eden区(Appel式的复制算法,别问,问就是用大佬名字命的名)提高了复制算法的空间利用率,使得这个算法既省事儿又有效,存活下来的少部分对象在From区和To区之间反复横跳,满足一定条件之后被送到老年代(动态年龄判定)。
  • 关于动态年龄判定,这里不阐述,因为比较容易产生歧义,具体内容见下方源码及其注释。
    图4 Appel式的复制算法
// 动态年龄判定(计算晋升至老年代的阈值年龄)
uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
  	// survivor_capacity是survivor空间(即上图的From区加上To区)的大小
  	// TargetSurvivorRatio默认值是50,即desired_survivor_size默认是survivor的一半
    size_t desired_survivor_size = (size_t)((((double) survivor_capacity) * TargetSurvivorRatio) / 100);
    size_t total = 0;
    uint age = 1;
    while (age < table_size) {
	    total += sizes[age]; // sizes数组存储了每个年龄段对象总大小
	    if (total > desired_survivor_size) break; // 当前总大小大于目标值则直接结束循环
	    age++;
    }
 	// MaxTenuringThreshold是默认阈值,一般设为15
    // result是动态判定之后的阈值,GC会把年龄大于等于result的对象送往老年代
    uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
	// 之后的内容省略...
}
  1. 小结
  • 复制算法常用于新生代,很贴合对象“朝生夕死”的特点。许多算法本身往往没有优劣之分,要结合应用场景选取合适的算法。

复制算法是针对新生代的,剩下的两个算法则在老年代中运用。

标记-清除算法

标记清除的英文是Mark-Sweep,没错,常用的并发GC——CMS中的MS就是标记-清除。接下来就基于CMS分析标记-清除算法。
  1. 什么是标记?如何实现?
  • 标记的含义和实现见根可达性分析部分(CMS中的标记比较复杂,这篇文章不详细讨论)。咳咳,差点忘了,还得填坑:在之前的分析中我们简单地把对象分为可达(图中用黑色表示)和不可达(图中用白色表示),但实际上(并发)GC采用的是三色标记法
  1. 什么是三色标记?
  • 我们知道多线程可以提升效率,但由于涉及到线程的调度,我们就不能简单地只把对象分为可达和不可达,因为线程被挂起时很有可能并没有完成整个引用链的扫描。
  • 于是引入了第三种颜色——“灰色”,用于标记可达但未遍历其引用集的的对象。再明确一下“黑色”和“白色”的定义——“黑色”用来标记可达且已经遍历其引用集的对象,“白色”用来标记不可达的对象。
    图5 三色标记
  1. 什么是漏标?
  • 三色标记解决了一部分问题,但由于GC线程和业务线程是并发执行的(妈妈一边打扫,你一边丢垃圾,请问医院wifi速度快吗),难以避免产生新的引用关系,或是删除/改变旧的引用关系,即并发会导致漏标,于是由此又引出了新的算法(如Increment Update),本文不做详细阐述(作者不会)。下图(承接上图)简单描绘了漏标的情形:
    图6 漏标
  1. 优点?缺点?
  • 标记-整理算法实际上包括标记清除整理,即标记-清除算法与之相比少了清除之后的整理。这样明显省事儿,但是代价是产生了内存碎片。同样地,CMS中的清除也是分阶段进行的。CMS的设计初衷就是要减短STW(Stop the world,咋瓦鲁多)的时间,而标记-清除算法就很符合这样的理念,因为它够快,缺点在上文也提及了,就是会产生内存碎片

标记-整理算法

  • 很明显,此算法的优点是不产生内存碎片,缺点是实现繁琐(涉及到地址的改变)。
  • 说累了,懒癌晚期的作者想溜了。不过u1s1,搞懂了标记-清除,其实也就搞懂了标记-整理,最后就用一张图来结束吧(图中黑色表示被内存使用,白色表示空闲,即不存在内存碎片,可用空间是连续的)~
    图7 标记-整理

三、总结

算法优点缺点
复制算法实现简单、无内存碎片内存利用率较低
标记-清除算法速度快有内存碎片
标记-整理算法实现较复杂无内存碎片

作者水平有限,文中存在的问题还望大家能够指出~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值