垃圾回收是Java的一项重要机制,是二月份的一项学习计划之一。
ZGC是一种较新的垃圾回收机制,在JDK11中实验性引入,看了R大的几篇文章,依然不懂,之后发现了一篇介绍地比较易懂的文章:
希望对你有帮助。
英语文章,特此翻译总结一下,以便后续我自己迅速浏览:
ZGC的背景:
GC需要了解一个概念,S-T-W: Stop the world,即整个应用程序暂停,等待GC完成。
这个时间内,应用程序暂停,因此需要越短越好。
那么,有哪些将STW变短的方法?
GC可以在压缩时使用多个线程。(并行压缩)
压缩工作也可以分解为多个阶段(增量压缩)。
将堆压缩,却不停止正在运行的应用程序(或只是短时间停止)(并发压缩)。
完全不压缩。
这里的压缩,指的就是:
moving the still-alive objects to the start (or some other region) of the heap.
将依然存活的对象移动到堆的起始端(或者其他区域)
ZGC在STW很短的时间下,需要解决的问题
需要把一个对象拷贝到另一个内存地址,同时其他线程还可以对老对象进行读取与修改。
拷贝成功以后,需要把所有老的引用更新。
ZGC性能评测:
很快,很强,接下来我们看一下ZGC的设计思路。
load barrier
ZGC使用read barrier,即对指向堆的引用进行读取时,会发生read barrier,比如
obj.field //加载堆中对象的引用,触发load barrier,干什么事儿下文有介绍
ZGC不使用write barrier:
obj.field = value //不使用write barrier,此时不会触发load barrier
注意,这都是针对堆中的引用。
着色指针
ZGC对于引用指针使用了一些标记,在地址的42~45位,如下:
6 4 4 4 4 4 0
3 7 6 5 2 1 0
+——————-+-+—-+———————————————–+
|00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
+——————-+-+—-+———————————————–+
| | | |
| | | * 41-0 Object Offset (42-bits, 4TB address space)
| | |
| | * 45-42 Metadata Bits (4-bits) 0001 =
Marked0
| | 0010 =
Marked1
| | 0100 =
Remapped
| | 1000 =
Finalizable
| |
| * 46-46 Unused (1-bit, always zero)
|
* 63-47 Fixed (17-bits, always zero)
Marked0,Marked1,Remapped,Finalizable,四位元数据。
内存分区
将内存分解成3种大小的Region(Page):
small:2MB,存放256k大小以内的对象
medium:32MB,存放最大4MB的对象
large:2MB * N,只能存放一个大于4MB的对象(large region最小只有6MB,比medium的region还小)
ZGC可以区分物理内存与虚拟内存,可以使用4TB的虚拟内存以及最大堆大小的物理内存。在物理内存中找到连续的32MB空间是不容易的,因此ZGC做了从物理内存到虚拟内存的映射。
ZGC能够将这些非连续的物理页面映射到单个连续的虚拟内存空间。如果这是不可能的,我们就会耗尽内存。
在Linux上,物理内存基本上是一个匿名文件,只存储在RAM中(而不是存储在磁盘上),ZGC使用memfd_create来创建它。然后可以使用ftruncate扩展该文件,允许ZGC将物理内存(=匿名文件)扩展到最大堆大小。然后将物理内存映射到虚拟地址空间中。
Marking & Relocating objects
标记与重新分配,是GC的2个主要阶段
标记阶段:标记所有可到达的对象。
在这个阶段结束时,我们知道哪些对象仍然存在,哪些对象是垃圾。
ZGC将此信息存储在每个内存page的所谓实时地图(live map)中。实时映射是一个位图,用于存储给定索引处的对象是否可以高可达和/或最终可达(对于具有finalize方法的对象)。这里的实时地图是一个位图,用于存储给定索引处的对象,是否strongly-reachable and/or final-reachable(for objects with afinalize-method).
在标记阶段,应用程序线程中的load-barrier将未标记的引用推送到线程局部标记缓冲区。只要此缓冲区已满,GC线程就可以获得此缓冲区的所有权,并以递归方式遍历此缓冲区中的所有可到达对象。在应用程序线程中标记只是将引用推送到缓冲区,GC线程负责遍历对象图并更新实时映射。
重分配阶段:
在进行标记后,GC统计了垃圾最多的若干内存page(即上文的region),将它们称作:relocation set。对于这些page,GC需要将它们中所有的存活对象进行重新分配,并生成一个映射转发表,存放被relocated的对象的新地址。
一个对象的重分配可能由GC线程完成,也可能是应用程序线程(比GC线程更早访问对象时,load barrier进行)。当这两个线程同时尝试重分配同一个对象时,通过原子性的CAS操作,ZGC会找到第一个尝试的线程,该线程完成relocate。
完成relocate(GC线程走完所有的relocation set)之后,load barrier会将指向relocation set中的引用修正为新的地址。即Remapping
这篇文章写到这里,我依然是一头雾水,于是去找了另1篇文章
以及1份官方PPT:
让我们再重温一下概念:
Load Barrier:
当从堆内存中加载引用对象时使用
观察指针的颜色
当颜色是bad color:需要进行mark/relocate/remap等措施
把颜色修改为good color(repair/heal)
针对常见情况进行了优化
绝大多数对象的引用都会是good color
这里就提到指针的颜色,其实是几个标志位的组合:
marked0,marked1:该对象是否被mark
Remapped:判断该对象是否在relocation set中
Finalizable:判断一个对象只能通过finalizer机制到达了
根据这些bit,ZGC可能会在我们获取引用前执行一些处理。
因此,它可能返回完全不同的引用。
好坏的依据,在第一篇文章里有,这里感觉不需要过多理解。
最后总结一下ZGC的三个大步骤:Mark、Relocate、Remap
Mark:寻找可到达的对象
Marking is the process when the garbage collector
determines which objects we can reach. The ones we can’t reach are considered garbage. ZGC breaks marking to
three phases:
第一阶段,S-T-W,Stop the world,ZGC找寻根引用。这个数目往往很少,因此耗时很短。
第二阶段:并发阶段,ZGC并发的从根引用出发,找寻所有的可以到达的对象,并mark它们。这一阶段中,如果load barrier感知到了未mark的对象,也会mark这个对象。(使用marked0与marked1来mark一个对象)
第三阶段:S-T-W,处理一些边缘case,如弱引用
完成之后,我们已经得知了哪些对象可以到达,
Relocation:Concurrent and Parallel
ZGC统计垃圾最多的page,放进relocation set
ZGC遍历relocation set,将它们relocate
load barrier如果访问到了需要relocate的对象,也会将其relocate
ZGC与load barrier同时访问,通过CAS方式决定谁来改变
每个page都有forward table,记录新老地址
Remapping:load barrier完成
Relocation阶段没有修改引用,因此我们会访问老的对象,甚至是垃圾,修改引用的工作是通过load barrier完成的。
这里几乎综合用到了着色指针、load barrier、relocation set等知识,参考该文章的一张图片:
看该图,当应用程序访问一个引用,并触发了load barrier时:
首先检查着色指针的Remap位置
如果Remap = 1,不用做任何事,返回引用
如果Remap = 0,判断该引用是否在relocation set中
Remap = 0,不在relocation set中:直接返回引用
Remap = 0,在relocation set中,判断是否已经relocate
Remap = 0,在relocation set中,已经relocate:更新引用至新地址,返回
Remap = 0,在relocation set中,还未relocate:relocate该对象,返回更新过的reference
至此,GC完成,可以看出ZGC不需要大的STW阶段,而只是单个变量的访问时间变慢(通过指针颜色,判断触发GC),因此大幅提升了效率。
可能还是有一些错误,还请指出。