「字节跳动」厉害!代表Java未来的ZGC深度剖析( 二 )


接下来 , 再看一下ZGC的垃圾回收过程 , 如下图所示 。 由图我们可知 , ZGC依然没有做到整个GC过程完全并发执行 , 依然有3个STW阶段 , 其他3个阶段都是并发执行阶段:

  • Pause Mark Start
这一步就是初始化标记 , 和CMS以及G1一样 , 主要做Root集合扫描 , 「GC Root是一组必须活跃的引用 , 而不是对象」 。 例如:活跃的栈帧里指向GC堆中的对象引用、Bootstrap/System类加载器加载的类、JNI Handles、引用类型的静态变量、String常量池里面的引用、线程栈/本地(native)栈里面的对象指针等 , 但不包括GC堆里的对象指针 。 所以这一步骤的STW时间非常短暂 , 并且和堆大小没有任何关系 。 不过会根据线程的多少、线程栈的大小之类的而变化 。
  • Concurrent Mark/Remap
第二步就是并发标记阶段 , 这个阶段在第一步的基础上 , 继续往下标记存活的对象 。 并发标记后 , 还会有一个短暂的暂停(Pause Mark End) , 确保所有对象都被标记 。
  • Concurrent Prepare for Relocate
即为Relocation阶段做准备 , 选取接下来需要标记整理的Region集合 , 这个阶段也是并发执行的 。 接下来又会有一个Pause Relocate Start步骤 , 它的作用是只移动Root集合对象引用 , 所以这个STW阶段也不会停顿太长时间 。
relocate
  • Concurrent Relocate
最后 , 就是并发回收阶段了 , 这个阶段会把上一阶段选中的需要整理的Region集合中存活的对象移到一个新的Region中(这个行为就叫做「Relocate」 , 即重新安置对象) , 如上图所示 。 Relocate动作完成后 , 原来占用的Region就能马上回收并被用于接下来的对象分配 。 细心的同学可能有疑问了 , 这就完了?Relocate后对象地址都发生变化了 , 应用程序还怎么正常操作这些对象呢?这就靠接下来会详细说明的Load Barrier了 。
Single Generation单代 , 即ZGC「没有分代」 。 我们知道以前的垃圾回收器之所以分代 , 是因为源于“「大部分对象朝生夕死」”的假设 , 事实上大部分系统的对象分配行为也确实符合这个假设 。
那么为什么ZGC就不分代呢?因为分代实现起来麻烦 , 作者就先实现出一个比较简单可用的单代版本 。 用符合我们国情的话来解释 , 大概就是说:工作量太大了 , 人力又不够 , 老板 , 先上个1.0版本吧!!!
Region Based这一点和G1一样 , 都是基于Region设计的垃圾回收器 , ZGC中的Region也被称为「ZPages」 , ZPages被动态创建 , 动态销毁 。 不过 , 和G1稍微有点不同的是 , G1的每个Region大小是完全一样的 , 而ZGC的Region大小分为3类:2MB , 32MB , N×2MB , 如此一来 , 灵活性就更好了:
Partial Compaction部分压缩 , 这一点也跟G1类似 。 以前的ParallelOldGC , 以及CMS GC在压缩Old区的时候 , 无论Old区有多大 , 必须整体进行压缩(CMS GC默认情况下只是标记清除 , 只会发生FGC时才会采用Mark-Sweep-Compact对Old区进行压缩) , 如此一来 , Old区越大 , 压缩需要的时间肯定就越长 , 从而导致停顿时间就越长 。
而G1和ZGC都是基于Region设计的 , 在回收的时候 , 它们只会选择一部分Region进行回收 , 这个回收过程采用的是Mark-Compact算法 , 即将待回收的Region中存活的对象拷贝到一个全新的Region中 , 这个新的Region对象分配就会非常紧凑 , 几乎没有碎片 。 垃圾回收算法这一点上 , 和G1是一样的 。
NUMA-awareNUMA对应的有UMA , UMA即Uniform Memory Access Architecture , NUMA就是Non Uniform Memory Access Architecture 。 UMA表示内存只有一块 , 所有CPU都去访问这一块内存 , 那么就会存在竞争问题(争夺内存总线访问权) , 有竞争就会有锁 , 有锁效率就会受到影响 , 而且CPU核心数越多 , 竞争就越激烈 。 NUMA的话每个CPU对应有一块内存 , 且这块内存在主板上离这个CPU是最近的 , 每个CPU优先访问这块内存 , 那效率自然就提高了:


推荐阅读