彪悍的ZGC

概述

jdk11的垃圾收集器为ZGC,又名Z Garbage Collector,它是被用来设计满足以下需求的可扩展、低延迟、多线程、非分代、基于region、支持numa和大页的垃圾收集器:

  • 最小的暂停时间(不超过10ms)。
  • 暂停时间不会随着堆的改变而变化。
  • 支持堆的大小从8MB16TB不等。

注意:jdk11、jdk13只有Linux支持ZGC,jdk14支持macOS、Windows。彪悍的ZGC_第1张图片

乍一看ZGC的功能,顿时感觉高大上了。不仅支持TB量级的堆,而且停顿时间不会随着堆的增大而增长。那么我们该怎么启用它呢?

最快捷的方法就是直接命令行启动:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx<size> -Xlog:gc

参数设置

ZGC支持以下的参数选项:

  • 普通GC选项:

    • -XX:MinHeapSize, -Xms:最小堆大小。
    • -XX:InitialHeapSize, -Xms:初始堆大小。
    • -XX:MaxHeapSize, -Xmx:最大堆大小。
    • -XX:SoftMaxHeapSize(jdk12):垃圾回收器会尽量避免堆内存超过指定的堆内存大小。
    • -XX:ConcGCThreads:指定与Java应用一起执行的GC线程数量,默认是Java线程的1/4。
    • -XX:ParallelGCThreads:指定GC的进程数。
    • -XX:UseLargePages:启用大内存页支持。
    • -XX:UseTransparentHugePages:启用透明大内存页支持。
    • -XX:UseNUMA:启用NUMA支持。
    • -XX:SoftRefLRUPolicyMSPerMB:决定软引用对象回收力度。
    • -XX:AllocateHeapAt:指向文件系统的文件并使用内存映射来达到在备用存储设备上进行堆分配的预期结果。
  • ZGC选项:

    • -XX:ZAllocationSpikeTolerance:内存分配速率预估的一个修正因子,默认值为2。一般不需要更改。
    • -XX:ZCollectionInterval:固定时间间隔进行GC,默认值为0。
    • -XX:ZFragmentationLimit:Relocate时,会根据当前region是否碎片化已大于ZFragmentationLimit,是则回收region,默认为25。
    • -XX:ZMarkStackSpaceLimit(jdk12):指定为标记堆栈分配的最大字节数。
    • -XX:ZProactive:是否启用主动回收策略,默认值为true。
    • -XX:ZUncommit(jdk13):返还未使用的内存给OS。当xmx>xms的情况,zgc默认会返回未使用的内存给操作系统,如果配置xms=xmx,该特性会被隐式禁用。
    • -XX:ZUncommitDelay(jdk13):如果内存在指定的时间内(以秒为单位)未使用,则将内存返回给OS。
  • ZGC诊断选项:

    • -XX:ZStatisticsInterval:指定统计数据输出之间的时间间隔(秒)。
    • -XX:ZVerifyForwarding:检验转发表。
    • -XX:ZVerifyMarking:检验标记集。
    • -XX:ZVerifyObjects(jdk13):检验对象。
    • -XX:ZVerifyRoots(jdk13):检验根节点。
    • -XX:ZVerifyViews(jdk13):检验堆视图访问。

当我们设置ZGC时需要考虑到以下几个方面:

  1. 堆大小:

    因为ZGC是并发 收集器,所以指定合适的堆大小很重要。我们在设置堆大小时,不仅要注意堆可以容纳你的应用程序的活动集,还要考虑堆中要有足够的空间允许GC运行时分配。

  2. 并发线程数:

    ZGC会根据应用运行情况自动跳转垃圾收集时的线程数,当然也可以用-XX:ParallelGCThreads指定。一般来说,如果低延迟对你的应用程序很重要,那么永远不要让你的系统超载。理想情况下,系统的CPU利用率不应该超过70%。

  3. 将未使用内存返还给OS:

    我们知道内存是操作系统很重要的资源,在以前的GC中并没有这种设置,它们不会将长时间未使用的内存返回给OS,这对内存占用敏感的服务并不友好。ZGC默认将一段时间未使用的内存返还给OS。

    ZGC的堆有若干个Region组成,每个Region被称之为ZPage。每个Zpage与数量可变的已提交内存相关联。当ZGC压缩堆的时候,ZPage就会释放,然后进入page cache,即ZPageCache。这些在page cache中的ZPage集合就表示没有使用部分的堆,这部分内存就被归还给操作系统。我们可以通过-XX:ZUncommitDelay选项来指定未使用内存存在时间。

  4. 开启大页支持:

    在Windows中大页被称为Large page,Linux中被称为Huge page。在操作系统中,采用内存映射来管理内存,逻辑页面映射到对应的物理内存,x86架构CPU默认使用4KB大小的内存页面,但是也支持较大的内存页。启用大页支持可以提供系统性能(吞吐量、延迟时间),并且没有什么缺点 ,所以推荐开启(开启大页支持需要root权限,这也是为什么ZGC默认不开启的原因)。

  5. 开启透明大页支持:

    Linux下的大页分为两种类型:标准大页(Huge Pages)和透明大页(Transparent Huge Pages)。标准大页管理是预分配的方式,而透明大页管理则是动态分配的方式。对于延迟敏感的应用程序,不建议使用透明的大型页面,因为它往往会导致不必要的延迟峰值。

  6. 开启NUMA支持:

    NUMA(非一致性内存访问)是Intel为了解决SMP多CPU结构中总线访问方式由资源竞争和一致性问题引起的性能瓶颈问题。在NUMA解决方案中,它放弃了总线的访问方式,而是将CPU划分到多个Node中,每个node有自己独立的内存空间。各个node之间通过高速互联通讯,通讯通道被成为QuickPath Interconnect即QPI。 该特性在默认情况下是启用的,Java堆分配直接分配到NUMA本地内存。但是,如果JVM检测到它被绑定到系统中的一个子cpu集,它将自动禁用。

原理分析

在进行分析前,我们先了解以下几个术语:

  • Load Barrier:加载屏障,即在应用线程从堆中加载对象应用后,执行的一段逻辑。跟内存屏障类似,但是完全没有关系。
  • Colored pointer:ZGC会在对象引用中用几个字节存储状态标记,Load Barrier会根据这些状态标记执行不同的逻辑。彪悍的ZGC_第2张图片
  • Region:ZGC和G1都是基于Region设计的垃圾回收器,在ZGC中Region被称为ZPages,不过G1的每个Region大小是完全一样的,而ZGC的Region大小分为3类:2MB,32MB,N×2MB
  • Partial compaction:在relocation阶段将Page中活的对象转移到另一个Page,并整个回收原Page。它会根据一定算法选择部分Page进行整理。

逻辑上将一次ZGC分为以下阶段:

  1. Pause Mark:初始化标记,在STW情况下主要对root集进行扫描,因为Root是一组活跃的引用,而不是对象指针,所以这一阶段的STW很短暂,并且和堆大小没有任何关系。
  2. Concurrent Mark:并发标记,这个阶段在第一步的基础上,继续往下标记存活的对象。并发标记后,还会有一个短暂的暂停(Pause Mark End),确保所有对象都被标记。
  3. Concurrent PreRelocate:并发式地选取要标记整理的Region集合。
  4. Pause Relocate:在STW情况下移动root集合对象引用。
  5. Concurrent Relocate:并发式地进行垃圾回收,将上一阶段选中的需要整理的Region集合中存活的对象移到一个新的Region中。

Relocate之后对象地址都发生了变化,那么内存地址怎么操作这些对象呢?答案就在Load Barrier上。

在标记和移动对象的阶段,每次从堆里对象的引用类型中读取一个指针的时候,都需要加上一个Load Barriers。这个屏障会把读出的指针更新到对象的新地址上,并且把堆里的这个指针修改到原本的字段里。这样就算GC把对象移动了,读屏障也会发现并修正指针,于是应用代码就永远都会持有更新后的有效指针,而且不需要STW。

JVM是怎么判断对象被移动过呢?答案就是通过Colored Pointer。

当加上读屏障时,根据Colored Pointer指针中这4个颜色位,判断当前对象是Bad/Good Color。当指针是Bad Color,那么程序需要修改指针;如果指针是Good Color,那么正常往下执行即可。

缺点

虽然ZGC有那么多且强大的优点,但是我们也不能盲目使用。俗话说得好,有失必有得,反之亦然。ZGC虽然能满足巨大的堆和极地的暂停需求,但是在吞吐速率上确有所降低,因为是并行处理,所以会占用很多的CPU时间。

你可能感兴趣的:(GC)