在上一次的《从回收算法理解JVM》 中只是简单的提到了分区 GC
,本篇文章针对 G1
做了进一步的总结,并解释了无停顿 GC
的部分实现原理
G1 在并发标记和记忆集维护中的优化
并发标记中的优化
介绍三色标记漏标时曾说过 CMS
选择的即不是前进法也不是后退法,而是复用了卡表 card Table
,这种方式可以复用 cardTable
的代码逻辑是其中一个原因,另外还有一个重要原因是写屏障是需要业务线程来完成的,并且写屏障本身的效率不高,容易导致性能问题。
G1
在使用删除写屏障时也考虑到了这个问题,并发标记开始后,如果识别到对象的引用关系被删除,那么业务线程会将需要标记为灰色的对象丢入到本地的 SATB
队列中(每个业务线程都有自己的 SATB
队列),然后由 GC
线程在合适的时机去做标记。
如果本地 SATB
队列放满了。就会将对象转移到全局的 SATB
集合中,然后申请一个空的 SATB
,如下所示:
在这种维护方式下,业务线程无需关心对象的标记操作,减轻了写屏障的负担,效率更高。
记忆集优化
①记忆集的维护时机
上一篇文章介绍过,因为 G1
的整个内存区域分为很多个 Region
,为了避免通用记录集导致的很多不必要查找,G1
实际使用的是专属记忆集的方式,即每个 Region
都有自己的专属记忆集。
G1
在维护记忆集时依旧是使用写屏障,并且使用到了本地 SATB
队列相似的方式。写屏障只负责将需要记录到 Rset
中的内容写入到 DirtyCardQueue
中,最后再由特殊的 GC
线程(又称为 Refine 线程)来完成 Rset
的写入操作
②记忆集的存放形式
对于一个 Region
来说,它的记忆集可能会因为引用关系比较多,而变得很大。根据另一个 Region
对这个 Region
的引用数量,可以分为少、中、多三种情况。针对这三种情况,有三种不同的数据结构来应对,分别是稀疏表、细粒度表和粗粒度表。三种表之间的关系是不断粗化的,如下图所示:
关于安全点的补充
当 GC
线程需要进行 STW
时,会等待所有业务线程都运行到最近的安全点,我们知道,标记根对象 GCRoot
是追踪式回收算法必须的阶段, GC
线程在寻找 GCRoot
时并不会从头查找整个上下文和全局的引用位置去做遍历,因为这样做的效率太低了,而且根对象的标记时间也会随着数量变长。所以在 HotSpot
中借助了一种名叫 oopMap
的辅助结构来记录根对象。
对于静态数据来说,编译器会在解释阶段将 oopMap
维护进去。而对于运行中动态产生的对象。JIT
解释器会负责将它维护到 oopMap
中。
所以说 oopmap 的作用,就是避免直接查找根对象,但是这里有个新问题就是,运行时可以改变 oopMap
的指令太多了。不太可能每个地方都去维护一次 oopmap
。
所以 HotSpot
仅在一些关键地方来统一维护 oopMap
,这些地方就被称为安全点 safe point
。 这些特定的位置主要在:
- 1、循环的末尾
- 2、方法临返回前 / 调用方法的call指令后
- 3、可能抛异常的位置
Pauseless GC 是如何实现的
介绍完前面的 CMS
、 G1
等收集器之后可以发现,在原有的思想下,我们已经把能够实现与业务线程并发工作的地方都做了并发处理了。只剩下复制(G1
中称为转移)对象的地址时,仍然需要 STW
。下面我们看看 HotSpot
中无暂停 GC
的代表 ZGC
是如何做的。(并不是真的说完全无暂停,在进行 GCRoot
标记时还是需要停下来的)
ZGC
和 G1
的思想很相似,它们都使用将存活对象拷贝到空闲区域的方式进行回收,ZGC
也将整个对区域划分为多个小块,回收时也会优先选择一部分区域,而它最主要的区别在于,ZGC 可以实现并发的活跃对象对象复制,而这依赖于两个技术:读屏障+染色指针
在了解读屏障和染色指针之前,我们需要复习一下计算机基础知识→什么是虚拟地址,以及什么是物理地址。
首先,物理地址指的是我们 PC 上的那根内存条,例如我的内存条为 8G
,那么它所能存储的地址就是从 0~8G
。在原始时期,程序员想要对数据进行读写操作都得操作物理内存,这就导致每个数据的地址分配在什么地方、如何合理地进行内存划分、如何保证数据的正确性,都需要编程人员来管理,效率十分低下。为了解决这个问题,CPU
的设计人员 基于局部性原理进行了一层虚拟化,这就是虚拟内存。这样一来,我们编写程序时就只需要操作虚拟内存就可以了。
CPU
内部通过内存映射单元(MMU
)为我们自动完成虚拟内存到物理内存的映射关系,大致如下:
图中的物理空间即我们的内存条,上面的可分配空间并不一定是连续的.而虚拟空间则是根据进程划分,每个进程都对应这一个虚拟空间
这里有几个重要的知识点需要掌握:
- 1.
CPU
为每个进程都虚拟化了同样大小的虚拟内存空间 - 2.虚拟内存是有大小的,例如
32
位系统是2^32
,也就是4G
大小。而64
系统只使用了低48
位,大小为2^48
,即256T
- 3.虽然虚拟内存很大,但要实际使用这些内存,还是需要分配在物理内存上能使用。
虚拟内存与物理内存的映射可以用人数和酒店的房间数来做类比,酒店只有 100
间房,在 100
天之内可以住 10000
人,而同时住房的→只有 100
人。(所以如果需要申请的内存超出了 100
,就会发生 OOM
)
读屏障
在 CMS
和 G1
中都用到了写屏障(write barrier
)来解决漏标问题,而 ZGC
中则使用到了与之相对的读屏障(read barrier
)技术。
当业务线程触发访问动作时,如果该对象正在转移过程中,那么访问该对象的原地址。如果对象已经转移完成,那么则访问该对象的新地址。read barrier 用于判断该对象是否已经转移完成
如上图所示,线程 a
访问时对象 A
还没有完成地址转移,所以访问的是原地址,当线程 b
访问时,对象已经转移完成了,此时就会触发 write barrier
访问转移后的 A'
新地址
对于一个常规的程序来说,对象的读操作数量通常都会高于写操作的数量,所以读屏障对性能的要求更高,为了解决这个问题,ZGC 使用了染色指针
染色指针
前面说到,在 64
位机器上,CPU
为每个进程都虚拟化了 256T
大小的虚拟地址,而我们的程序是用不了这么大内存的,于是 ZGC
将虚拟地址的第 42-45
四个位置作为标记位,把 0-41
位的地址留给程序作为堆空间来使用(依然还能使用 4T
)
ZGC
将不同的标记位置为 1
,用于代表对象所处的不同状态,目前包括 Marked0
、Marked1
、Remapped
、Finalizable
四个状态,我们只需关注前三个状态即可,Finalizable
说的是 与弱引用有关(我也没去细看,忽略忽略),我们需要关注的重点是,不管这个标记位的值是什么,它们都指向的是同一个物理地址,为什么要这么做呢?
试想一下,在以前如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段。例如对象的 Hashcode
、分代年龄、forwarding
指针、锁标记位等等。这种记录方式在有对象访问的场景下是很自然流畅的,不会有什么额外负担。但是如果对象存在被移动的可能性,在不知道是否能成功访问到对象的情况下,甚至是不访问对象,但是却希望得知该对象的某些信息的情况下,对象头中的信息就排不上用场了, 于是乎,ZGC
把主意打在了对象的虚拟地址上。
下面为染色指针技术带来的一些优势总结:
- 1.有了染色指针的支持后,无需直接访问对象,就可以根据地址的标记位判断是否发生了迁移,也就解决了读屏障的性能问题。并且,一旦某个
Region
中的存活对象被移走之后,原来的Region
就能够释放和重用; - 2.可以减少收集过程中内存屏障的使用数量,在此之前写屏障的目的通常是为了记录对象引用的变动情况,而现在可以直接维护在指针中,并且目前
ZGC
没有使用任何写屏障,一部分是指针带来的优势,另一部分是ZGC
目前还不支持分代收集,不存在跨代引用。 - 3.这种类似的使用地址位进行标记的方式,可以作为一种扩展的存储结构来记录更多的一些信息,以便日后进一步提高性能。
下面来看看 ZGC 的回收流程:
在 GC
线程开始工作前,所有的对象都处于 Remapped
状态,经过 GCRoot
标记之后,会把存活对象的状态标记修改为 Marked0
状态,仍然处于 Remapped
状态的对象则会被当做垃圾对象。在这个过程中新产生的对象也像 G1 那样,均当做存活对象处理。
正因为不同标记位指向的是同一个物理地址,所以,从地址上业务线程就能够判断出该对象是否发生了转移
标记结束后,接下来 GC
线程会把存活对象转移到空闲区域,并为他们分配新的地址空间,此时会涉及到对象的移动。这个过程总体分为以下两个步骤:
- 选择一个区域 ,将其中存活的对象,迁移到另一个区域
- 将这些存活对象,放到
forwarding table
中(记录了对象的新地址)
由于业务线程访问对象的时机不确定
- 如果访问时还没有完成转移(还处于
Marked0
状态),则业务线程会主动完成转移操作(也称为“自愈”过程),并将信息维护到forwarding table
中。 - 如果已经完成了转移,则直接去
forwarding table
中查找映射,修改引用即可
那 Marked1 状态是什么时候使用的呢?
还记得年轻代,区域划分中的 S0
和 S1
两个 survicor
区域么- -,Marked0
与 Marked1
的关系与它们类似,会在每一次 GC
时互换身份。
参考
- 周志明-《深入理解java虚拟机第三版》
- 海纳-《编程高手必学的内存知识》