这是Alexey Shipilev的文章“自己动手(OpenJDK)垃圾收集器”的翻译,该文本经作者同意发表。 报告PM中的任何错别字和其他错误-我们将修复它们。
在运行时创建某些东西的过程是一个有趣的练习。 至少是第一个版本的创建! 要构建一个可靠,高性能,故障安全的运行时子系统,其行为可以方便地进行观察和调试,这是非常非常困难的任务。
从表面上看,制作一个简单的垃圾收集器非常简单,现在我想在本文中做到这一点。 使用此修补程序的早期版本,Roman Kennke在FOSDEM 2019上进行了名为“在20分钟内编写GC”的演讲和演示。 尽管在那里实现的代码展示了很多东西,并且得到了很多评论,但是仍然需要对正在发生的事情有一个很好的高级描述-这就是本文的样子。
对垃圾收集器的工作有一个基本的了解将极大地帮助理解这里写的内容。 本文将在HotSpot的特定实现中使用细节和思想,但此处不会提供有关GC设计的入门课程。 阅读GC手册并阅读有关GC基本知识的第一章,甚至更快地开始阅读Wikipedia 。

目录内容
1. GC由什么组成
既然已经编写了许多不同的GC,就可以轻松制作自己的GC-可以使用(重新)使用许多已经编写的元素,将对实现细节的某些担忧转移到经过验证的代码上。
1.1。 Epsilon gc
OpenJDK 11引入了新的JEP 318: “ Epsilon:无操作垃圾收集器(实验性) 。 ” 它的任务是为不需要甚至禁止释放内存的情况提供最小的实现。 JEP更详细地讨论了为什么它可能有用。
从实现的角度来看,“垃圾收集器”是个坏名字,使用术语“自动内存管理器”来负责分配和释放内存会更正确。 Epsilon GC仅实现“分配”,而根本不处理“发布”。 因此,您可以使用Epsilon GC并从头开始实施“发布”算法。
1.1.1。 内存分配
Epsilon GC的最发达部分负责分配内存 。 它为外部请求提供服务,以分配任意大小的内存并创建所需大小的线程本地分配缓冲区(TLAB) 。 由于没有可用内存,而且没有人将返回丢失的字节,因此实现本身试图不对TLAB进行过多扩展。
1.1.2。 壁垒
一些垃圾收集器需要与应用程序交互以维护GC不变性,从而在尝试访问堆时迫使运行时和应用程序创建所谓的障碍 。 对于所有多线程收集器以及许多世代相传的收集器而言,都是如此。
Epsilon不需要障碍,但是运行时和编译器仍然想知道障碍无济于事。 每次到处处理都会很累。 幸运的是,从OpenJDK 11开始,有一个新的JEP-304:“垃圾收集接口” ,它使插入障碍变得非常容易。 特别是, Epsilon中的障碍集为空 ,并且所有琐碎的工作(保存,加载,CAS,数组复制)都可以委派给现有超类中的琐碎障碍的实现。 如果您要制作也不需要障碍的GC,则可以简单地重用Epsilon中的代码。
1.1.3。 监控连接
GC实现的最后一个乏味的部分是挂钩到JVM内部的一系列监视机制:MX-bin,诊断命令等应该起作用。 Epsilon 已经为您完成了所有这一切。
1.2。 Rantime和GC
1.2.1。 根元素
通常,垃圾收集器需要知道Java运行时中确切具有堆引用的内容。 这些根元素(称为GC根 )可以是流堆栈和局部变量(包括在JIT编译的代码中发现的那些!),本机类和类加载器,JNI中的引用等插槽。 尝试识别这些元素可能非常复杂且乏味。 但是在Hotspot中,它们都使用适当的VM子系统进行跟踪,因此您可以简单地了解现有GC实施如何与它们一起使用。 在本文的进一步部分,我们将看到它。
1.2.2。 对象抓取
垃圾收集器应绕过Java对象中的出站链接。 该操作随处可见,因此运行时的公共部分提供了现成的解决方法;您无需自己编写任何内容。 下面将有一个具有特定实现的部分,您可以在其中找到例如obj→oop_iterate
调用。
1.2.3。 排量
移动垃圾收集器需要在某处写下移动对象的新地址。 您可以在几个地方写入此转发数据 。
- 您可以在对象本身 (串行,并行等)中重用“标记词” 。 在世界停止之后,对对象的所有访问都将受到控制,并确保没有Java线程能够看到我们决定在标记词中输入的临时数据。 您可以重复使用它来存储转发数据。
- 您可以维护一个单独的本机移动表( ZGC ,C4等)。 这将GC与运行时以及应用程序的其余部分完全隔离开来,因为只有GC知道这种表的存在。 竞争性汇编程序通常只使用这种方案-他们不想遭受很多不必要的问题。
- 您可以向对象添加另一个单词( Shenandoah等)。 先前两种方法的这种结合不仅使运行时和应用程序可以毫无问题地与现有标头一起使用,而且还保存了转发数据。
1.2.4。 标记数据
垃圾收集器需要在某处写入标记数据 。 同样,有几种方法可以保存它们:
- 您可以在对象本身(串行,并行等)中重用标记词。 同样,在世界停止模式下,您可以使用标记词中的位对标签的事实进行编码。 此外,如果您需要遍历所有活动对象,那么我们将逐个对象地处理堆-由于堆是可解析的 ,因此这是可能的。
- 您可以维护单独的结构来存储标记数据(G1,Shenandoah等)。 通常使用单独的位图完成此操作,该位图将堆的每N个字节映射到卡的1位。 通常,Java对象按8个字节对齐 ,因此卡将堆中的每64位映射到卡的1位,在本机内存中占堆大小的1/64。 在扫描堆中是否存在活动对象(尤其是稀疏对象)时,这些开销会得到很好的回报:绕过映射通常比绕过正在逐对象分解的堆快得多。
- 将标签编码成链接本身(ZGC,C4和其他)。 这需要与应用程序协调,然后您需要从链接中删除所有这些标签,或者执行一些其他技巧来保持正确性。 换句话说,我们需要GC的障碍或其他工作。
2.总体规划
最有可能在Epsilon上最容易实现的是LISP2样式的Mark-Compact。 该GC的基本思想在维基百科和GC手册 (第3.2章)中都有描述。 该算法的草图将在下面的实现部分中,但我强烈建议您阅读一点Wikipedia或GC手册,以了解我们将要做什么。
讨论中的算法是移动 GC:移动对象以数组的形式移动到堆的最开始。 它有其优点和缺点:
- 它维护内存分配的顺序。 如果对您来说很重要,那么这对于控制内存中的布局非常有用(控制怪胎,这是您的时间!)。 缺点是您不会以这种方式获得自动链接的位置信息 。
- 它的复杂度是对象数的O(N)。 但是,线性度是有代价的:每个构建周期都需要GC绕过4次。
- 它不需要堆上的可用内存! 无需在堆上保留内存来撤离活动对象,因此您甚至可以使用被99%(9)%溢出的堆。 如果我们采用其他简单收集器的想法,例如使用半空间的清除器(半空间的清除器),我们将不得不稍微重写堆的表示并保留一些空间用于疏散,但这超出了本练习的范围。
- 如果您对此问题进行一些工作,则在GC处于不活动状态时,可以实现零内存和时间消耗。 它以任意状态在内存上启动,然后停止,从而极大地压缩了内存。 这非常适合Epsilon的工作方式:仅在最后一个对象之后一直突出显示。 这也是一个缺点:堆开始时一些死对象导致大量移动。
- 它只是不需要新的障碍,您可以
EpsilonBarrierSet
原样重用EpsilonBarrierSet
。
为简单起见,GC实现将使用世界的句点(stop-the-world,STW),它将没有代数或多线程。 对于这种情况,使用位图存储标记并重用标记词存储运动数据是有意义的。
3. GC核心的实施
对于一个无知的人来说,阅读和理解整个实现可能太复杂了。 在本节中,我们将逐步了解它。
3.1。 序言
垃圾收集器通常需要做几件事为收集做准备。 阅读评论,他们应该为自己说话:
{ GCTraceTime(Info, gc) time("Step 0: Prologue", NULL);
由于我们使用位图来跟踪对象的可及性,因此需要在使用前清除它。 或者在我们的情况下,由于我们的目标是在开始GC周期之前永远不要求资源,因此我们必须提前将位图提交到内存 。 至少在Linux上,这提供了许多有趣的优点,在Linux上,大多数位图将指向页面0,尤其是对于稀疏堆。
线程应释放其TLAB,并在构建完成后向GC索要新的TLAB。
不要混淆TLAB和java.lang.ThreadLocal
。 从GC的角度来看,ThreadLocal是普通对象,除非Java代码中另有特殊要求,否则它们不会由GC编译。
运行时的某些部分,尤其是那些持有Java堆链接的部分,在垃圾回收期间会中断,因此您需要特别警告它们,GC即将开始工作。 这将允许各个子系统在GC进行移动之前准备并保存其部分状态。
3.2。 打标
当几乎所有事情都已经为我们完成时,在停止模式下进行标记变得非常简单。 标记是非常标准的,并且在许多实现中,很可能GC是第一步。
{ GCTraceTime(Info, gc) time("Step 1: Mark", NULL);
这与任何其他图形完全相同:您可以通过初始一组可到达的顶点开始遍历,沿着输出的边沿走并记录所有已访问的顶点。 遍历继续直到所有未访问的峰结束。 在GC中,“顶点”是对象,“边缘”是它们之间的链接。
从技术上讲,我们可以递归地遍历对象图,但这对于直径很大的任意图来说是个坏主意。 想象一下一个十亿个峰的链接列表! 因此,为了限制递归的深度,我们使用标记堆栈来记录检测到的对象。
可访问对象的初始集合是GC根。 现在,不要再讨论process_roots
什么了,以后再介绍。 现在,我们只说它绕过了VM端所有可访问的链接。
带有标记的位图既可以用作记录标记波前的工具(已经访问过许多对象),也可以用作最终存储所有可到达对象的集合(作为所需结果的存储库)。 实际的工作发生在EpsilonScanOopClosure
,它应用于所有有趣的对象,并在所选对象的所有链接上进行迭代。
看,Java知道在流行之前如何关闭(关闭) !
class EpsilonScanOopClosure : public BasicOopIterateClosure { private: EpsilonMarkStack* const _stack; MarkBitMap* const _bitmap; template <class T> void do_oop_work(T* p) {
完成此步骤后, _bitmap
包含指示活动对象位置的位。 因此,可以绕开所有活动对象,例如:
3.3。 计算新地址
这也是一个相当简单的步骤,它完全实现了算法所说的内容。

唯一引起您注意的是,我们决定将新地址存储在Java对象的标记词中,并且该词已经被一些重要的东西占用,例如,有关锁的信息。 幸运的是,这种非平凡的标记词非常少见,如果有必要,我们可以简单地将它们分开存储:这就是PreservedMarks
用途。
真正的算法工作由EpsilonCalcNewLocationObjectClosure
完成:
class EpsilonCalcNewLocationObjectClosure : public ObjectClosure { private: HeapWord* _compact_point; PreservedMarks* const _preserved_marks; public: EpsilonCalcNewLocationObjectClosure(HeapWord* start, PreservedMarks* pm) : _compact_point(start), _preserved_marks(pm) {} void do_object(oop obj) {
forward_to
是最重要的部分,因为它将“移动地址”存储在对象的标记词中。 在接下来的步骤中将需要这样做。
3.4。 修正指针
现在,您需要再次遍历堆,并根据以下算法用新地址重写所有链接:

{ GCTraceTime(Info, gc) time("Step 3: Adjust pointers", NULL);
有两种对移位对象的引用:从堆本身上的对象或从GC根发出的引用。 您需要更新两个链接类。 一些保存的标签还存储了对对象的引用,因此您需要要求它们进行更新。 PreservedMarks
知道如何执行此操作,因为它希望在对象的标记字词中将“转发数据”保存在我们保存该数据的位置。
闭包分为两种类型:有些封闭对象并绕过它们的内容,另一些则更新这些地址。 您可以在此处进行一些小的性能优化:如果对象不移动,则可以将几条记录保存为一堆:
class EpsilonAdjustPointersOopClosure : public BasicOopIterateClosure { private: template <class T> void do_oop_work(T* p) {
完成此步骤后,我们实质上破坏了堆:链接指向对象尚未位于的“错误”地址。 让我们修复它!
3.5。 我们移动物体
根据算法将对象移动到新地址的时间:

再次EpsilonMoveObjectsObjectClosure
堆,并将EpsilonMoveObjectsObjectClosure
闭包应用于所有活动对象:
{ GCTraceTime(Info, gc) time("Step 4: Move objects", NULL);
之后,您可以立即拖动压缩点堆的堆,从而有可能在垃圾回收周期结束后立即从该位置直接分配内存。
请注意,在移位程序集中,我们可以覆盖现有对象的内容,但是由于扫描方向相同,因此已覆盖的对象已经复制到正确的位置。
同一设施的旧位置和新位置可能相交。 例如,如果将一个100字节的对象移动8个字节。 复制过程应自行解决,并且相交的内容应正确复制,请注意Copy::aligned_*conjoint*_words
。
闭包本身只会将移动的对象移动到新地址:
class EpsilonMoveObjectsObjectClosure : public ObjectClosure { public: void do_object(oop obj) {
3.6。 结语
垃圾收集完成,堆再次几乎一致,剩下最后的修饰:
{ GCTraceTime(Info, gc) time("Step 5: Epilogue", NULL);
我们通知其余的运行时,他们应该开始组装后程序。 我们恢复了先前保存的特殊标记词。 告别吻我们的标记卡-不再需要。
而且,如果您确实愿意,可以将分配的内存减少到新的大小,从而将内存返回给操作系统!
4.将GC连接到VM
4.1。 根遍历
记住,您需要绕过VM的特殊,可访问的链接吗? 您可以要求每个特殊的VM子系统绕过其他Java对象隐藏的链接。 当前热点中此类根元素的详尽列表如下所示:
void EpsilonHeap::do_roots(OopClosure* cl) {
, . GC .
4.2。
GC , VM . Hotspot VM_Operation
, GC VM- :
, GC — , .
4.3。
, GC , , GC , . , allocate_work
, GC :
HeapWord* EpsilonHeap::allocate_or_collect_work(size_t size) { HeapWord* res = allocate_work(size); if (res == NULL && EpsilonSlidingGC) { vmentry_collect(GCCause::_allocation_failure); res = allocate_work(size); } return res; }
!
5.
OpenJDK.
$ hg clone https://hg.openjdk.java.net/jdk/jdk/ jdk-jdk $ cd jdk-jdk $ curl https://shipilev.net/jvm/diy-gc/webrev/jdk-jdk-epsilon.changeset | patch -p1
OpenJDK :
$ ./configure --with-debug-level=fastdebug $ make images
:
$ build/linux-x86_64-server-fastdebug/images/jdk/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC -version openjdk version "13-internal" 2019-09-17 OpenJDK Runtime Environment (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon) OpenJDK 64-Bit Server VM (build 13-internal+0-adhoc.shade.jdk-jdk-epsilon, mixed mode, sharing
6.
, GC ? :
- . . Hotspot , JVM fastdebug , GC.
- . , . , ( ) , .
- 测试。 , , , . - , .
, , :
$ CONF=linux-x86_64-server-fastdebug make images run-test TEST=gc/epsilon/ Building targets 'images run-test' in configuration 'linux-x86_64-server-fastdebug' Test selection 'gc/epsilon/', will run: * jtreg:test/hotspot/jtreg/gc/epsilon Running test 'jtreg:test/hotspot/jtreg/gc/epsilon' Passed: gc/epsilon/TestAlwaysPretouch.java Passed: gc/epsilon/TestAlignment.java Passed: gc/epsilon/TestElasticTLAB.java Passed: gc/epsilon/TestEpsilonEnabled.java Passed: gc/epsilon/TestHelloWorld.java Passed: gc/epsilon/TestLogTrace.java Passed: gc/epsilon/TestDieDefault.java Passed: gc/epsilon/TestDieWithOnError.java Passed: gc/epsilon/TestMemoryPools.java Passed: gc/epsilon/TestMaxTLAB.java Passed: gc/epsilon/TestPrintHeapSteps.java Passed: gc/epsilon/TestArraycopyCheckcast.java Passed: gc/epsilon/TestClasses.java Passed: gc/epsilon/TestUpdateCountersSteps.java Passed: gc/epsilon/TestDieWithHeapDump.java Passed: gc/epsilon/TestByteArrays.java Passed: gc/epsilon/TestManyThreads.java Passed: gc/epsilon/TestRefArrays.java Passed: gc/epsilon/TestObjects.java Passed: gc/epsilon/TestElasticTLABDecay.java Passed: gc/epsilon/TestSlidingGC.java Test results: passed: 21 TEST SUCCESS
? fastdebug . ? - .
7.
- spring-petclinic , Apache Bench GC! , , GC .
: -Xlog:gc -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -XX:+EpsilonSlidingGC
:
:
Heap: 20480M reserved, 20480M (100.00%) committed, 19497M (95.20%) used GC(2) Step 0: Prologue 2.085ms GC(2) Step 1: Mark 51.005ms GC(2) Step 2: Calculate new locations 71.207ms GC(2) Step 3: Adjust pointers 49.671ms GC(2) Step 4: Move objects 22.839ms GC(2) Step 5: Epilogue 1.008ms GC(2) GC Stats: 70561 (8.63%) reachable from roots, 746676 (91.37%) reachable from heap, 91055 (11.14%) moved, 2237 (0.27%) markwords preserved GC(2) Heap: 20480M reserved, 20480M (100.00%) committed, 37056K (0.18%) used GC(2) Lisp2-style Mark-Compact (Allocation Failure) 20479M->36M(20480M) 197.940ms
200 ? GC! , . , , : ( — , ). - ( ).
, GC . , -Xlog:gc -XX:+UseSerialGC
— , , :
GC(46) Pause Young (Allocation Failure) 575M->39M(1943M) 2.603ms GC(47) Pause Young (Allocation Failure) 575M->39M(1943M) 2.606ms GC(48) Pause Young (Allocation Failure) 575M->39M(1943M) 2.747ms GC(49) Pause Young (Allocation Failure) 575M->39M(1943M) 2.578ms
, 2 . , , GC . -Xlog:gc -XX:+UseSerialGC
, , :
GC(3) Pause Full (Allocation Failure) 16385M->34M(18432M) 1969.694ms GC(4) Pause Full (Allocation Failure) 16385M->34M(18432M) 2261.405ms GC(5) Pause Full (Allocation Failure) 16385M->34M(18432M) 2327.577ms GC(6) Pause Full (Allocation Failure) 16385M->34M(18432M) 2328.976ms
, . .
8. ?
. , GC OpenJDK — , , .
:
. , // . . , , « » , , .
GC, java.lang.ref.Reference.referent
— Java-, , , - . FinalReference
, .
ReferenceProcessor
/ / .
VM. VM, , , . . , , , - , .
. — , GC, . , , , .
mark-compact GC Full GC fallbacks Shenandoah ( OpenJDK 8) G1 ( OpenJDK 10, JEP 307: «Parallel Full GC for G1» ).
. , «» , , , - . . , .
. , , , . , «» — «» «» , .
- GC Handbook .
9.
? GC — , , , GC.
, - GC . , , GC (, Serial GC Parallel GC), .
分钟的广告。 , 5-6 2019, JPoint — Java-. — OpenJDK, GraalVM, Kotlin . .