JVM 基础篇05 —— *JVM(ZGC为例)垃圾收集器

我们熟知的如G1收集器是2006年时引入Hotspot VM的。当时最大的亚马逊网络服务实例只有1vCPU和1.7GB内存,而如今亚马逊已经推出了16TB内存的虚拟服务器。(对,你没看错是内存!单位是TB!还是两位数!啥样的可以自行了解。反正我也没用过,更用不起😂)。这让以前Hotspot VM中如G1等传家宝们等倍感压力,所以在JDK 11发布了一个全新的垃圾收集器 —— The Z Garbage Collector

The Z Garbage Collector,简称ZGC,是一种可扩展的低延迟垃圾收集器,旨在实现以下目标:

  • 暂停时间不超过10毫秒
  • 暂停时间不会随堆或实时设置大小而增加
  • 处理堆范围从几百MB到几TB字节大小
  • 吞吐量下降不超过15%
  • 为未来GC奠定基础

ZGC的特点:

  • 并发(低STW,大部分时候不用暂停)
  • 基于Region
  • 标记整理
  • NUMA感知
  • 着色指针
  • 读屏障

为了实现其目标ZGC中用到的关键技术:

  • 加载(读)屏障(Load barriers)技术
  • 着色(有色对象)指针(Colored pointers)
  • 单一分代内存管理
  • 基于区域的内存管理
  • 部分内存压缩;
  • 即时内存复用。

(除了最关键的着色指针和读屏障,剩下的如内存压缩和go的gc很像。这里只对这两部分进行简单概况)

着色指针

着色指针可以直接在引用中存储对象信息中。因为在64位平台上(ZGC仅支持64位平台),一个指针可以保存更多信息,因此ZGC从中取特定位来存储状态。 ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位: finalizable, remap, mark0和mark1位。 着色的这个过程相当于对对象进行了标记

64位指针结构:
ZGC使用41-0存储对象实际地址的前42位, 42位地址为应用程序提供了理论4TB的堆空间; 45-42位为metadata比特位, 对应于如下状态: 
finalizable,remapped,marked1和marked0; 46位为保留位,固定为0; 63-47位固定为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      (Address view 4-8TB)
//  |                   |                                 0010 = Marked1      (Address view 8-12TB)
//  |                   |                                 0100 = Remapped     (Address view 16-20TB)
//  |                   |                                 1000 = Finalizable  (Address view N/A)
//  |                   |
//  |                   * 46-46 Unused (1-bit, always zero)
//  |
//  * 63-47 Fixed (17-bits, always zero)

但是着色指针面临一个问题,当取消着色时,需要额外的工作(因为需要屏蔽信息位)。 像SPARC这样的平台有内置硬件支持指针屏蔽所以不是问题,而对于x86平台来说,ZGC团队使用了简洁的多重映射技巧。

多重映射

多重映射涉及将不同范围的虚拟内存映射到同一物理内存。

物理内存是系统可用的实际内存,通常是安装的DRAM芯片的容量。 虚拟内存是抽象的,这意味着应用程序对(通常是隔离的)物理内存有自己的视图。 操作系统负责维护虚拟内存和物理内存范围之间的映射,它通过使用页表和处理器的内存管理单元(MMU)和转换查找缓冲器(TLB)来实现这一点,后者转换应用程序请求的地址。

由于设计中只有一个remap,mark0和mark1在任何时间点都可以为1,因此可以使用三个映射来完成此操作。

void ZPhysicalMemoryBacking::map(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    map_view(pmem, ZAddress::good(offset), AlwaysPreTouch);
  } else {
    // Map all views
    map_view(pmem, ZAddress::marked0(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::marked1(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::remapped(offset), AlwaysPreTouch);
  }
}


void ZPhysicalMemoryBacking::unmap(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    unmap_view(pmem, ZAddress::good(offset));
  } else {
    // Unmap all views
    unmap_view(pmem, ZAddress::marked0(offset));
    unmap_view(pmem, ZAddress::marked1(offset));
    unmap_view(pmem, ZAddress::remapped(offset));
  }
}

完成堆结构映射后

// Address Space & Pointer Layout
// ------------------------------
//
//  +--------------------------------+ 0x00007FFFFFFFFFFF (127TB)
//  .                                .
//  .                                .
//  .                                .
//  +--------------------------------+ 0x0000140000000000 (20TB)
//  |         Remapped View          |
//  +--------------------------------+ 0x0000100000000000 (16TB)
//  |     (Reserved, but unused)     |
//  +--------------------------------+ 0x00000c0000000000 (12TB)
//  |         Marked1 View           |
//  +--------------------------------+ 0x0000080000000000 (8TB)
//  |         Marked0 View           |
//  +--------------------------------+ 0x0000040000000000 (4TB)
//  .                                .
//  +--------------------------------+ 0x0000000000000000

读屏障

从堆中读取引用时,ZGC需要一个所谓的load barrier(也称为read-barrier)。每次Java程序访问对象字段时,ZGC都会执行load barrier的代码逻辑,例如Player .name。访问原始类型的字段不需要屏障。

由于着色指针的存在,在程序运行时访问对象的时候,可以轻易知道对象在内存的存储状态(通过指针访问对象),若请求读的内存在被着色(标记)了,那么则会触发读屏障。读屏障会更新指针再返回结果,此过程有一定的耗费,从而达到与用户线程并发的效果。

与标记对象的传统算法相比,ZGC在指针上做标记,在访问指针时加入Load Barrier(读屏障),比如当对象正被GC移动,指针上的颜色就会不对,这个屏障就会先把指针更新为有效地址再返回,也就是,永远只有单个对象读取时有概率被减速,而不存在为了保持应用与GC一致而粗暴整体的Stop The World(应用线程暂停)。 —————— RednaxelaFX

GC 各阶段

ZGC包含10个阶段,但是主要是两个阶段标记relocating

GC循环从标记阶段开始,递归标记所有可达对象,标记阶段结束时,ZGC可以知道哪些对象仍然存在哪些是垃圾。ZGC将结果存储在每一页的位图(称为live map)中

在标记阶段,应用线程中的load barrier将未标记的引用压入线程本地的标记缓冲区。一旦缓冲区满,GC线程会拿到缓冲区的所有权,并且递归遍历此缓冲区所有可达对象。注意:应用线程负责压入缓冲区,GC线程负责递归遍历

标记阶段后,ZGC需要迁移relocate集中的所有对象。relocate集是一组页面集合,包含了根据某些标准(例如那些包含最多垃圾对象的页面)确定的需要迁移的页面。对象由GC线程或者应用线程迁移(通过load barrier)。ZGC为每个relocate集中的页面分配了转发表。转发表是一个哈希映射,它存储一个对象已被迁移到的地址(如果该对象已经被迁移)。

GC线程遍历relocate集的活动对象,并迁移尚未迁移的所有对象。有时候会发生应用线程和GC线程同时试图迁移同一个对象,在这种情况下,ZGC使用CAS操作来确定胜利者。

一旦GC线程完成了relocate集的处理,迁移阶段就完成了。虽然这时所有对象都已迁移,但是旧地引用址仍然有可能被使用,仍然需要通过转发表重新映射(remapping)。然后通过load barrier或者等到下一个标记循环修复这些引用

因为仅root扫描时会有停止其他线程的操作,因此GC暂停时间不会随堆的大小而增加。

详细的GC过程可以看PPT演示文档

推荐视频:

Stefan Karlsson和Per Liden的Jfokus演讲

推荐文档:
openJDK 官方文档

正如前面介绍的那样ZGC的目标之一是位未来GC奠定基础,而未来GC的设想则有很多。。。

多层堆和压缩

随着ssd等存储器的日益普及,一种设想是JVM中允许多层堆,可以让很少使用的对象存储在较慢的存储层上。

该功能可以通过扩展指针元数据来实现,指针可以实现计数器位并使用该信息来决定是否需要移动对象到较慢的存储上。如果将来需要访问,则读屏障可以从存储中检索到对象。

或者对象可以以压缩形式保存在内存中,而不是将对象重定位到较慢的存储层。当请求时,可以通过读屏障将其解压并重新分配

更新时间:2020-07-16 22:31:40

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/jvm基础篇05jvmzgc为例垃圾收集器
最后更新:2020-07-16 22:31:40

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×