预留堆空间(heap reserve)是堆空间中特殊的一块小空间,无法用于java线程的常规分配;而当发生gc时需要进行对象重分配时才会使用,此举确保了空的堆区域可用,即使是在java线程角度看堆空间已满,仍可进行对象的重分配操作;
缺点:1、只可在发生gc且对象重新分配时才会被使用,并且不能保证重分配操作能否正常完成,虽然正常情况下这种问题不会发生;
2、虽然预留空间只占用较小的空间,但是毕竟还是对原堆空间的占用,进而导致可用的堆空间变小且该空间在系统大部分负载情况下不被使用;
释放连续内存块的另一种方式就是在堆中直接进行压缩,hotspot中g1收集器在执行full gc时会在堆中直接压缩,该种方法的好处就在于不需要额外的内存来释放其他内存;
存在的缺点:1、对象需要顺序移动,否则就有可能覆盖尚未移动的对象,同时需要额外开销来协调多个gc线程的同步,这种情况就不适合并行处理;这样同时还会限制java的gc线程在对象重分配时可以的操作;
综上两种算法各有优劣,在预留堆空间充足的情况下,不进行原地对象重分配通常性能表现会更好;反之在预留堆空间不足时,原地对象重分配也能够保证对象分配完成;
故而从jdk 16开始,zgc兼顾两种算法的优势,可灵活在两种模式当中进行切换;此举也摆脱了对预留堆空间的需求,同时在常见情况下保持了良好的对象重分配的性能且能成功进行分配;
在默认情况下,当预留空间足够时,zgc就不会进行原地重分配;反之zgc将切换到原地重分配,一旦预留空间足够时,zgc将会再次切换到预留堆空间的模式;
这些重定位模式之间的来回切换时无缝进行的,如果有需要可以在同一gc周期中多次进行;但是大多数工作负载永远不会遇到需要切换的情况;
zgc的日志记录也进了扩展,以显示每个组(Small/Medium/Large)的堆区域(ZPages)重分配的信息;以下为一个示例,表示需要重新分配54M的小对象,需要原地重分配3个小页面:
…
GC(15) Small Pages: 120 / 240M, Empty: 0M, Relocated: 54M, In-Place: 3
GC(15) Medium Pages: 2 / 64M, Empty: 0M, Relocated: 0M, In-Place: 0
GC(15) Large Pages: 1 / 4M, Empty: 0M, Relocated: 0M, In-Place: 0
…
当zgc重定位对象时,对象的新地址记录在转发表中,转发表是在java堆之外分配的数据结构;每个堆区域被选择为重定位集(压缩以释放内存的堆区域的集合)的一部分且都将获得与之关联的转发表;
在jdk16之前,当重定位集非常大时转发表的分配和初始化可能会占用整个gc周期时间的很大一部分;重定位集的大小与在重定位期间移动的对象数相关;假如存在一个大于100GB的堆并且因工作负载导致大量碎片,如果这些碎片均匀分布在整个堆中,那么重定位集将很大且在分配/初始化阶段会需要一段时间;因为这项工作总是在并发阶段完成,因此它从未影响过GC的暂停时间,不过这里依然存在改进的空间;
在jdk16中,zgc现在批量分配转发表;如今我们不再需要大量触发malloc/new调用(可能数以千计)来为每个转发表分配内存,转而一次调用以分配所有表所需的所有内存;此举有助于避免通常的分配开销和潜在的锁竞争并显著减少分配这些表所需的时间;
这些表的初始化是另一个瓶颈;转发表是一个哈希表,因此初始化它意味着设置一个较小的标头,并将转发表项的数组(可能很大)清零;从jdk16开始zgc使用多个线程而不是单个线程并行进行初始化;
以上这些更改大大减少了分配和初始化转发表所需的时间,尤其是在收集非常稀疏、非常大的堆时时间上可以缩短一到两个数量级;
原文链接: https://malloc.se/blog/zgc-jdk16