问题

Go1.6中的gc pause已经完全超越JVM了吗?

回答
Go 1.6 中关于 GC(垃圾回收)的暂停时间(pause time)以及它与 JVM(Java Virtual Machine)的对比,是一个值得深入探讨的话题。要回答“Go 1.6 中的 GC pause 是否完全超越了 JVM?”这个问题,需要从多个维度进行详细分析,并且要理解“超越”的含义是相对的,取决于具体的应用场景和性能要求。

核心观点:

Go 1.6 的 GC 已经取得了显著的进步,尤其是引入了三色标记(threecolor marking)和写屏障(write barrier)的组合,极大地降低了 GC 暂停时间,使其在许多场景下可以与甚至优于许多 JVM 版本。然而,说“完全超越”可能有些绝对,因为 JVM 也在不断发展,并且对于非常复杂和高吞吐量的应用,JVM 的 GC 策略(如 G1、ZGC、Shenandoah)仍然有其优势和适应性。

详细分析:

1. Go GC 的演进与 Go 1.6 的关键进步:

在 Go 1.5 之前,Go 的 GC 是一个标记清除(markandsweep)的垃圾回收器,其暂停时间相对较长,尤其是在堆比较大的情况下。这导致了一些对延迟敏感的应用开发者感到不满。

Go 1.5 是一个重要的里程碑,引入了并发标记(concurrent marking)的 GC。这基本上意味着 GC 的大部分工作是在用户程序(goroutines)运行时进行的,只有在标记阶段的短暂暂停(stoptheworld, STW)来设置或清理标记,以及在清扫阶段的短暂暂停。

Go 1.6 在 Go 1.5 的基础上进一步优化了 GC 算法和实现,主要体现在以下几个方面:

三色标记算法(ThreeColor Marking)的优化: 这是 Go GC 能够实现如此短暂停顿的核心技术之一。
基本原理: 三色标记将堆中的对象分为三种颜色:
白色(White): 对象尚未被 GC 访问到,可能已经被回收。
灰色(Gray): 对象已经被 GC 访问到,但其子对象尚未被完全扫描。
黑色(Black): 对象已经被 GC 访问到,且其子对象也已经被完全扫描。
并发标记: GC 启动时,所有对象都是白色的。根对象(全局变量、栈上的变量)被标记为灰色。GC 会不断地从灰色对象集合中取出一个对象,扫描其指向的其他对象。如果扫描到的对象是白色,就将其标记为灰色。如果对象是灰色, GC 会将其标记为黑色。
写屏障(Write Barrier): 在并发标记过程中,用户程序可能会修改对象之间的引用关系。为了保证标记的正确性,需要写屏障。写屏障是在程序修改指针时触发的一小段代码,它会确保 GC 的可见性。Go 1.6 使用了混合写屏障(hybrid write barrier),这是一种相对更优化的写屏障实现,能够在更小的开销下保证标记的正确性。
暂停原因的最小化: Go 1.6 的 GC 设计目标是最小化 STW 暂停。即使在标记阶段,暂停也只发生在几个关键点:
1. 开始标记: 标记阶段开始时需要短暂暂停,将根对象标记为灰色。
2. 标记完成: 在标记完成之前,需要短暂暂停,以确保所有可达对象都被标记为黑色,并处理写屏障可能产生的漏网之鱼(dangling pointers)。
3. 清扫阶段的短暂暂停: 在清扫阶段,可能会有非常短暂的暂停来释放内存页。

GC 调度和调优: Go 1.6 的 GC 调度器更加智能,能够更好地平衡 GC 对程序性能的影响和内存释放的效率。它会根据系统负载和内存使用情况动态调整 GC 的触发时机。

内存分配器优化: Go 1.6 的内存分配器(malloc)也进行了优化,以减少内存碎片和提高分配速度,这间接也会影响 GC 的效率。

2. Go GC 相比 JVM 的优势(在 Go 1.6 版本及其之后):

更低的、更可预测的暂停时间: Go 1.6 的并发 GC 和三色标记算法使得 STW 暂停时间通常在毫秒级别甚至微秒级别,即使在非常大的堆上。对于许多需要低延迟的服务(如 API 网关、实时通信服务),这是一个巨大的优势。
更小的 GC 开销: 相较于一些 JVM 的老一代 GC,Go 的 GC 在用户程序运行时执行大量工作,因此整体的 CPU 开销分布得更均匀,对应用程序的性能影响可能更小。
设计哲学: Go 的设计哲学是简单性和效率。GC 的设计也遵循这一原则,追求易于理解和易于优化的实现。

3. JVM GC 的多样性与发展:

JVM 的 GC 生态系统非常丰富,并且在不断发展,以应对各种不同的应用场景和性能需求。

JVM GC 的类型:
Serial GC: 单线程,简单,适用于小堆或单核机器。暂停时间较长。
Parallel GC (Throughput Collector): 多个 GC 线程,吞吐量高,但暂停时间相对较长。
CMS (Concurrent Mark Sweep): 曾经是主流的低暂停 GC,但存在碎片化问题,且在 Go 1.6 时代其暂停时间可能不如 Go 的并发 GC。
G1 GC (GarbageFirst): 成为 JVM 默认的 GC。它将堆划分为多个区域,优先回收垃圾最多的区域,以达到目标暂停时间。G1GC 在 Go 1.6 时代也已经非常成熟,并且在平衡吞吐量和延迟方面做得很好。
ZGC 和 Shenandoah GC: 这些是低延迟(ultralow latency) GC,它们通过更精细的并发标记和写屏障技术,将 STW 暂停时间缩短到亚毫秒级别,甚至纳秒级别(理论上)。这些 GC 通常用于对延迟要求极其严苛的应用,例如金融交易系统、实时数据处理。

JVM GC 的优势:
成熟度和生态系统: JVM GC 经过了数十年的发展和优化,拥有非常成熟的调优工具和社区支持。
灵活性和可配置性: JVM GC 提供了极其丰富的调优参数,可以针对不同的应用场景进行精细配置。
应对复杂场景: 对于一些极其复杂的内存模型和对象图,JVM 的一些 GC 可能有更强的适应性。

4. Go 1.6 GC 是否“完全超越”了 JVM?

答案是:在某些方面“超越”,但在整体“完全超越”则不准确。

Go 1.6 在“平均暂停时间”和“可预测性”方面,可能已经优于许多 JVM 的默认配置(尤其是老版本或没有启用 G1 GC 的情况)。 对于许多常见的服务类应用,Go 1.6 的 GC 表现已经非常出色,能够满足低延迟的需求。
然而,JVM 的 G1 GC、ZGC、Shenandoah GC 等现代 GC,也在不断追求更低的暂停时间。 如果将 Go 1.6 的 GC 与 JVM 最先进的 ZGC 或 Shenandoah 进行比较,虽然 Go 1.6 的 GC 暂停时间已经很短,但这些 JVM GC 在追求极致低延迟方面可能还有优势,并且其设计也经过了更长时间的考验。
“超越”的定义很重要:
如果“超越”指的是平均 STW 暂停时间更短、更稳定且对开发者更友好,那么 Go 1.6 在很多情况下确实实现了这一点。
如果“超越”指的是在所有场景下都能提供比 JVM 任何 GC 都低的暂停时间,那可能不准确。JVM 也在不断进步,且其调优的灵活性允许其在特定场景下达到非常好的效果。

总结:

Go 1.6 的 GC 通过引入并发标记、三色标记算法和优化的写屏障,将暂停时间显著降低到了毫秒级别,在很多应用场景下表现出色,甚至优于许多 JVM 的默认 GC 配置。这使得 Go 成为开发低延迟服务的有力竞争者。

然而,JVM 的 GC 技术也在飞速发展,特别是 G1 GC、ZGC 和 Shenandoah GC,它们在追求极低延迟方面也取得了巨大成就。因此,说 Go 1.6 的 GC “完全超越”了 JVM 的所有 GC 策略,可能过于绝对。更准确的说法是,Go 1.6 的 GC 在降低 GC 暂停时间方面取得了革命性的进展,使其在许多常见的并发和低延迟场景下,能够与甚至优于许多主流的 JVM GC 方案,并且其设计哲学带来的简洁性也是其一大优势。

开发者在选择语言和 GC 策略时,应该根据具体的应用需求(吞吐量、延迟要求、内存模型复杂度等)来综合评估。对于大多数需要低延迟的 Go 应用,Go 1.6 及之后版本的 GC 已经足够强大。而对于需要极致低延迟、对堆大小和对象生命周期有更复杂控制的应用,可能需要深入了解并调优 JVM 的现代 GC 选项。

网友意见

user avatar

Go的GC完胜JVM GC?

作为在2TB的GC堆上能维持在< 10ms GC暂停时间的Azul Systems的Zing JVM…

而且Zing JVM的C4 GC是一种完全并发的、会整理堆内存的GC(Fully Concurrent Mark-Compact GC),不但mark阶段可以是并发的,在整理(compaction)阶段也是并发的,所以在GC堆内不会有内存碎片化问题;而Go 1.5/1.6GC是一种部分并发的、不整理堆内存的GC(Mostly-Concurrent Mark-Sweep),虽然实现已经做了很多优化但终究还是能有导致堆内存碎片化的workload,当碎片化严重时Go GC的性能就会下降。

简短回答是:不,Go 1.6的GC并没有在GC pause方面“完胜”JVM的GC。

我们有实际客户在单机十几TB内存的服务器上把Zing JVM这2TB GC堆的支持推到了极限,有很多Java对象,外带自己写的基于NIO的native memory内存管理器,让一些Java对象后面挂着总共10TB左右的native memory,把这服务器的能力都用上了。在这样的条件下Zing JVM的GC还是可以轻松维持在< 5ms的暂停时间,根本没压力;倒是Linux上自带的glibc的ptmalloc2先“挂”了——它不总是及时归还从OS申请来的内存,结果把这没开swap的服务器给跑挂了…

(注意上面的单位都是TB。)

Zing JVM的C4 GC跟其它JVM GC相比,最大的特征其实还不是它“不暂停”(或者说只需要暂停非常非常短的时间),而是它对运行的Java程序的特征不敏感,可以对各种不同的workload都保持相同的暂停时间表现。这样要放在前面强调,因为下面的讨论就要涉及workload了。

后面再补充点关于Zing JVM的GC的讨论。先放几个传送门:

要跟JVM比GC性能的话不要光看HotSpot VM啊。

Go的低延迟GC的适用场景和实际性能如何?

其实很重要的注意点就是:每种GC都有自己最舒服的workload类型——Zing的C4 GC是少有的例外。

题主给的那张演示稿没有指出这benchmark测的是啥类型的workload,也没有说明这个workload运行了多长时间,这数据对各种不同情况到底有多少代表性还值得斟酌。最公平的做法是把benchmark用的Go程序移植到Java,然后用HotSpot VM的CMS GC也跑跑看,对比一下。

作为一种CMS(Mostly-Concurrent Mark-Sweep)GC实现,Go的GC最舒服的应用场景是当程序自身的分配行为不容易导致碎片堆积,并且程序分配新对象的速度不太高的情况。

而如果遇到一个程序会导致碎片逐渐堆积,并且/或者程序的分配速度非常高的时候,Go的CMS就会跟不上,从而掉进长暂停的深渊。这就涉及到低延迟模式能撑多久多问题。

具体怎样的情况会导致碎片堆积大家有兴趣的话我回头可以来补充。主要是跟对象大小的分布、对象之间的引用关系的特征、对象生命期的特征相关的。

这里让我举个跟Go没关系的例子来说明讨论这类问题时要小心的陷阱。

要评测JVM/JDK性能,业界有几个常用的标准benchmark,例如SPECjvm98 / SPECjvm2008,SPECjbb2005 / SPECjbb2013,DaCapo等。其中有不少benchmark都是,其声称要测试的东西,跟它实际运行中的瓶颈其实并不一致。

SPECjbb2005就是个很出名的例子。JVM实现者们很快就发现,这玩儿实际测的其实是GC暂停时间——如果能避免在测试过程中发生full GC,成绩就会不错。于是大家一股脑的都给自己的GC添加启发条件,让JVM实现们能刚刚好在SPECjbb2005的测试时间内不发生full GC——但其实很多此类“调优”的真相是只要在多运行那么几分钟可能就要发生很长时间的full GC暂停了。

所以说要讨论一个GC的性能水平如何,不能只靠看别人说在某个没有注明的workload下的表现,而是得具体看这个workload的特征、运行时间长度以及该GC的内部统计数据所表现出的“健康程度”再来综合分析。

Go CMS GC与HotSpot CMS GC的实现的比较

Go GC目前的掌舵人是Richard L. Hudson大大,是个靠谱的人。

他之前就有过设计并发GC的经验,设计了Sapphire GC算法。

Sapphire: Copying GC Without Stopping the World

ftp://ftp.cs.umass.edu/pub/osl/papers/sapphire-2003.pdf

设计了并发Copying GC的他在Go里退回到用CMS感觉实属无奈。虽然

未来Go可能会尝试用能移动对象的GC

,在Go 1.5的时候它的GC还是不移动对象的,而外部跟Go交互的C代码也多少可能依赖了这个性质。要不移动对象做并发GC,最终就会得到某种形式的CMS。

Go的CMS实现得比较细致的地方是它的pacing heuristics,或者说“并发GC的启动时机”。这是属于“策略”(policy)方面做得细致。HotSpot VM的CMS GC则这么多年来都没得到足够多的关爱,其实尚未发挥出其完全的能力,还有不少改进/细化的余地,特别是在策略方面。

而在“机制”(mechanism)方面,Go的CMS GC其实与HotSpot VM的CMS GC相比是非常相似的。都是只基于incremental update系write-barrier的Mostly-Concurrent Mark-Sweep。两者的工作流程中最核心的步骤都是:

  1. Initial marking:扫描根集合
  2. Concurrent marking:并发扫描整个堆
  3. Re-marking:重新扫描在(2)的过程中发生了变化/可能遗漏了的引用
  4. Concurrent sweeping

具体到实现,两者在上述核心工作流程上有各自不同的扩展/优化。

两者的(1)都是stop-the-world的,这是两者的GC暂停的主要来源之一。

HotSpot VM的CMS GC的(3)也是stop-the-world的,而且这个暂停还经常比(1)的暂停时间要更长;Go 1.6 CMS GC则在此处做了比较细致的实现,尽可能只一个个goroutine暂停而不全局暂停——只要不是全局暂停都不算在用户关心的“暂停时间”里,这样Go版就比HotSpot版要做得好了。

(无独有偶,Android Runtime(ART)也有一个CMS GC实现,而它也选择了把上述两种暂停中的一个变为了每个线程轮流暂停而不是全局暂停,不过它是在(1)这样做的,而不是在(3)——这是Android 5.0时的状况。新版本我还没看)

HotSpot版CMS对(3)的细化优化是,在真正进入stop-the-world的re-marking之前,先尝试做一段时间的所谓并发的“abortable concurrent pre-cleaning”,尝试并发的追赶应用程序对引用关系的改变,以便缩短re-marking的暂停时间。不过这里实现得感觉还不够好,还可以继续改进的。

有个有趣的细节,Go版CMS在(3)中重新扫描goroutine的栈时,只需要扫描靠近栈顶的部分栈帧,而不需要扫描整个栈——因为远离栈顶的栈帧可能在(2)的过程中根本没改变过,所以可以做特殊处理;HotSpot版CMS在(3)中扫描栈时则需要重新扫描整个栈,没抓住机会减少扫描开销。Go版CMS就是在众多这样的细节上比HotSpot版的更细致。

再举个反过来的细节。目前HotSpot VM里所有GC都是分代式的,CMS GC在这之中属于一个old gen GC,只收集old gen;与其配套使用的还有专门负责收集young gen的Parallel New GC(ParNew),以及当CMS跟不上节奏时备份用的full GC。分代式GC很自然的需要使用write barrier,而CMS GC的concurrent marking也需要write barrier。HotSpot VM就很巧妙的把这两种需求的write barrier做在了一起,共享一个非常简单而高效的write barrier实现。

Go版CMS则需要在不同阶段开启或关闭write barrier,实现机制就会稍微复杂一点点,write barrier的形式也稍微慢一点点。

从效果看Go 1.6的CMS GC做得更好,但HotSpot VM的CMS GC如果有更多投入的话也完全可以达到一样的效果;并且,得益于分代式GC,HotSpot VM的CMS GC目前能承受的对象分配速度比Go的更高,这算是个优势。

(待续)

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有