没有合适的资源?快使用搜索试试~ 我知道了~
如何实现一个垃圾收集器 - 知乎1
需积分: 0 0 下载量 116 浏览量
2022-08-03
22:09:21
上传
评论
收藏 780KB PDF 举报
温馨提示
试读
22页
介绍构建高可靠、高性能、可观察、可调试,有故障预测的运行时非常难,但造一个简单的轮子还是可以的。我们今天实现一个简单的垃圾收集器。Roman Kennke曾使用
资源详情
资源评论
资源推荐
12/27/2019 如何实现一个垃圾收集器 - 知乎
https://zhuanlan.zhihu.com/p/57588075 1/22
如何实现一个垃圾收集器
我本具足,超越范式。http://chukai.link
关注他
18 人赞同了该文章
初开
原文:Do It Yourself (OpenJDK) Garbage Collector
作者:Aleksey,Red Hat,OpenJDK开发者,shipilev.net/
译者:初开,http://chukai.link
介绍
构建高可靠、高性能、可观察、可调试,有故障预测的运行时非常难,但造一个简单的轮子还是可
以的。我们今天实现一个简单的垃圾收集器。Roman Kennke曾使用本文的早期版本,参加了
FOSDEM 2019的演讲“20分钟构建GC”。虽然生产的实现很复杂,但原理是类似的。
为了改善阅读体验,请确保你了解过垃圾收集器的基本原理。本文虽然有一些基础知识和实现细
节,但并不是入门的地方。可以阅读GC Handbook的第一章,或了解维基百科Tracing garbage
collection。
1.构建块
现在实现新的GC要简单得多,很多其他GC实现的模块可以复用。
1.1 Epsilon GC
OpenJDK 11的JEP 318:“Epsilon:无操作垃圾收集器(实验版)”,是为不需要或禁止GC的
场景提供最小实现,可以阅读JEP以获得更多信息。
从实现的角度来看,“垃圾收集器”这个术语用词不当,正确的术语是“自动内存管理器”,它实
际上负责内存的分配和回收。
Epsilon GC仅实现了“分配”部分,所以我们可以在它上面来实现回收功能。
12/27/2019 如何实现一个垃圾收集器 - 知乎
https://zhuanlan.zhihu.com/p/57588075 2/22
1.1.1 分配
Epsilon GC的核心是分配算法,分配到堆,或者分配到线程局部分配缓冲区(TLAB)。因为没有
回收,所以它没怎么扩展TLAB。
1.1.2 屏障
一些垃圾收集器需要维护GC不变量,方法是在访问堆时强制执行GC屏障,所有并发收集器,包括
分代收集器都是如此。Epsilon不需要屏障,但运行时和编译器仍然需要知道,哪怕是空实现,也
要假装把它们打包起来。而Ope nJDK 11的JEP-304:“垃圾收集接口”的定义使插入屏障更简
单。
正因为Epsilon的GC屏障是空实现,所有的琐碎工作:加载,存储,CAS,数组复制,被委托给默
认屏障。如果要开发一个仍然不需要障碍的GC,可以简单地重用Epsilon已有的。
1.1.3 JVM工具
实现GC的最后一项工作是能将它整合到JVM的各种监视工具中:MX bean、诊断命令等等。
Epsilon 已经为我们处理了这个问题。
1.2 运行时和GC
1.2.1 Roots
垃圾收集器通常需要知道哪些部分包含对堆的引用,即GC Roots,包括线程堆栈和局部变量(JIT
编译代码),本地类和类加载器,JNI句柄等等。GC Roots虽然听起来很复杂,但是在Hotspot
中,每个VM子系统都会跟踪这些位置,我们可以用现有的GC实现。
1.2.2 对象遍历器
垃圾收集器需要遍历对象的引用,运行时已经提供了遍历器,无需自己实现。你将在下面的
obj→oop_iterate 中看到这些。
1.2.3 转发数据
12/27/2019 如何实现一个垃圾收集器 - 知乎
https://zhuanlan.zhihu.com/p/57588075 3/22
垃圾收集器需要记录GC后的对象的新地址,称之为转发数据或转发地址,有几个地方存储地址
值:
复用对象中的“markword”字段(Serial, Parallel等)。当GC暂停时,对对象字段的所有访问都
停止,没有线程知道我们偷偷在“markword”中的存了数据。
维护单独的本地转发表(ZGC,C4等)。这将完全隔离GC与运行时和应用程序的其余部分,只有
GC才会知道存在转发表。并发GC为了避免混淆通常采用这种方案。
在对象上添加另一个字段(Shenandoah等)。这是结合前两种方法,让运行时和应用程序与现有
标头一起工作,同时保持转发数据。
1.2.4 标记数据
垃圾收集器需要在某处记录可达性标记。同样,有几种方法可以存储它:
复用对象中的“markword”(Serial, Parallel等)。同样,在GC暂停时,使用“markword”来
编码“已标记”属性。然后,如果需要遍历所有活动对象,可以利用堆可解析性遍历。
维护单独的标记数据结构(G1,Shenandoah等)。这通常使用位图,将Java堆的每N个字节映
射到1位标记位图。通常,Java对象按8个字节对齐,因此标记位图将64位堆映射到1位标记位图,
占本机堆大小的1/64。这在扫描堆以获取对象实例(尤其是稀疏对象)时很管用:遍历位图通常
比遍历分析堆快得多。
在引用本身(ZGC,C4,其他)中编码标记信息。这需要与应用协调以从引用中去除标记信息,
或者做其他工作以保持正确性。换句话说,它需要GC屏障或更多的GC工作。
2.大计划
最容易在Epsilon上实现的GC算法是LISP2风格的标记-整理算法。该算法的原理在相关的维基百科
条目或GC手册的第3.2章中给出。下面概述了算法的部分原理,可以阅读维基百科或GC手册进一
步了解。
算法的关键是滑动GC:它通过将对象滑动到堆的开头来移动对象。它具有以下特征:
维护分配顺序。这对于控制内存布局非常有用,如果你是控制狂会很开心,但缺陷是,没法得到引
用位置。
对象数是O(n)。但是,这种线性需要付出代价,每个GC循环需遍历堆4次。
它不需要任何额外的堆内存!无需保留活动对象的堆内存,因此即使是占有率99.9%的堆也可以操
作。如果我们实现其他算法,例如标记-复制,我们需要重新划分堆的结构,并为复制预留一些空
间,这超出了本文的范围。
通过一些技巧,当GC未启用时,可以不产生空间和时间开销。密集整理分配区域,这用来形容
12/27/2019 如何实现一个垃圾收集器 - 知乎
https://zhuanlan.zhihu.com/p/57588075 4/22
Epsilon很贴切:它将持续从整理指针开始分配。但这也是它的缺点:堆开始时的一些死对象会频
繁移动。
它不需要任何新的屏障来防止EpsilonBarrierSet受影响。
为简单起见,本文的GC实现将完全暂停,而非分代和单线程。在这种情况下,使用标记位图来存
储标记数据,并重用标记字来存储转发数据。
3 实现GC的关键
一口气阅读完整实现太复杂了,我们逐个击破它。
3.1 准备
GC通常需要做一些准备工作,阅读以下注释,它们应该不难:
{
GCTraceTime(Info, gc) time("Step 0: Prologue", NULL);
//提交标记位图内存。 这样做有几个好处
//循环之前:如果没有发生GC,则不会占用内存
//在第一次调用时清理并映射未使用的位图部分
//提交到零页,提高稀疏堆的性能。
if (!os::commit_memory((char*)_bitmap_region.start(), _bitmap_region.byte_size(), fa
log_warning(gc)("Could not commit native memory for marking bitmap, GC failed");
return;
}
//我们不需要可解析堆来让算法工作,但我们想让线程放弃现有的TLAB。
ensure_parsability(true);
//通知运行时的各个部分我们将开始GC。
CodeCache::gc_prologue();
BiasedLocking::preserve_marks();
//在标记期间将重置派生指针。
//清除并激活表。
DerivedPointerTable::clear();
}
12/27/2019 如何实现一个垃圾收集器 - 知乎
https://zhuanlan.zhihu.com/p/57588075 5/22
由于我们使用标记位图来跟踪哪些对象可以访问,因此需要在使用之前清理。或者,在这种情况
下,当我们追求从不占用资源直到GC循环命中,则需要首先将位图提交到内存。这带来了一些优
势,至少在Linux上,大多数位图都映射到零页,特别是对于稀疏堆。GC完成后,线程需要放弃现
有的TLAB并向GC请求新的TLAB。
运行时的某些部分,特别是处理Java堆引用的部分,会被GC过程修改,因此需要通知它们GC即将
开始,请准备/保存某些状态。
3.2 标记
一旦准备好所有的组件,GC暂停标记就非常简单,这是许多GC实现的第一步。
{
GCTraceTime(Info, gc) time("Step 1: Mark", NULL);
//标记堆栈和对象闭包(OopClosure)。通过对象闭包
//扫描对象的引用,标记它们,并推送新标记对象
//到堆栈以进行下一步处理。
EpsilonMarkStack stack;
EpsilonScanOopClosure cl(&stack, &_bitmap);
// 在Roots上设置标记
process_roots(&cl);
stat_reachable_roots = stack.size();
// 扫描堆的其余部分,终止是有保证的,最终会标记所有可访问对象。
while (!stack.is_empty()) {
oop obj = stack.pop();
obj->oop_iterate(&cl);
stat_reachable_heap++;
}
//标记完成后,就没有其他派生指针了。
DerivedPointerTable::set_active(false);
}
就像图遍历一样:从最初的可到达顶点开始,遍历所有边,记录访问过的顶点,并执行此操作,直
到走完未访问的顶点。在GC中,“顶点”是对象,“边”是它们之间的引用。
剩余21页未读,继续阅读
苗苗小姐
- 粉丝: 40
- 资源: 328
上传资源 快速赚钱
- 我的内容管理 展开
- 我的资源 快来上传第一个资源
- 我的收益 登录查看自己的收益
- 我的积分 登录查看自己的积分
- 我的C币 登录后查看C币余额
- 我的收藏
- 我的下载
- 下载帮助
最新资源
- SI4947ADY-T1-E3-VB一款SOP8封装2个P-Channel场效应MOS管
- TeeChart ProFS 2023.38 源码版, 本人一直在用,稳定可靠
- python程序设计:数字类型 转换 运算
- doubleball1.m
- 二层半独栋别墅-12.00&10.80米-施工图.dwg
- SI4940DY-T1-E3-VB一款SOP8封装2个N-Channel场效应MOS管
- 端午节相关庆祝代码案例.zip
- SaaS 短链接系统,承载高并发和海量存储等场景难题 专为实习、校招以及社招而出的最新项目,项目质量不亚于12306铁路购票项目
- TeeChart ProFS 2023.38 VCL 试过各种版本,这个应该是最新最全源码的,本人一直在用 稳定运行
- 嵌入式产品开发.xmind
资源上传下载、课程学习等过程中有任何疑问或建议,欢迎提出宝贵意见哦~我们会及时处理!
点击此处反馈
安全验证
文档复制为VIP权益,开通VIP直接复制
信息提交成功
评论0