浅谈go垃圾回收与竞争检测

本文涉及的产品
可观测链路 OpenTelemetry 版,每月50GB免费额度
函数计算FC,每月15万CU 3个月
云原生网关 MSE Higress,422元/月
简介: 【5月更文挑战第16天】Go语言的运行时聚焦于垃圾回收(GC)和并发特性。GC通过微小和小对象分配器管理内存,大于32KB的大对象直接分配。GC是并发的,使用写屏障和非压缩策略,分为扫描终止、标记、标记终止和扫除四个阶段。竞争检测用于查找数据竞争,debug包提供运行时调试功能,如堆栈跟踪。内部的atomic包提供原子操作保证线程安全,math包检测数学溢出。sys包包含系统特定常量,NotInHeap结构确保某些对象不被GC管理。

0 简介

比如java,python优秀的语言使用虚拟机解释一切,但是go没有。

而运行时在go开发中使用频率比较高,单独整理以下内容。

question_ans.png

1 垃圾回收 runtime GC

Go将内存对象分为以下几类:

  • 微小对象 Tiny size <16B

      使用mcache的微小分配器分配大小小于 16个字节的对象,在单个16字节块上可以完成多个微小分配
    
  • 小对象 16B ~32KB

      大小在16个字节和32k字节之间的对象被分配在G运行所在的P ncache对应 mspan size class
    

在微小型和小型 对象种秒如果mspan列表为空,分配器将从 mheap获取大量页面用于mspan
如果mheap为空或 没有足够大的页面满足分配请求。

那么它将从操作系统分配一组新的页 至少 1MB。

1.1 大对象

    大小 >32KB, 大于 32KB 的对象直接分配在 mheap的相应大小类上 size class

如果 mheap为空或 没有足够大的页面满足分配请求,则它将从操作系统分配一组新的页 至少 1MB

GC与线程同时运行,类型准确(又称精确),允许多个GC线程并行运行。它是一个使用写屏障的并发标记和扫除程序。

它是非生成性和非压缩性的。分配是使用每P分配的大小隔离来完成不同区域,以尽量减少碎片,同时消除常见情况下的锁。

该算法可分解为几个步骤。
这是对正在使用的算法的一个高级描述。对于GC的概述,一个好的开始可以是Richard Jones的gchandbook.org。 查看本文最后。

    1. GC执行扫描终止。

      a. 停止执行。这将导致所有Ps达到GC的安全点。

      b. 清理任何未扫除的对象。只有在以下情况下才会出现未清理的对象, 这个GC循环在预期时间之前被强制执行。

    1. GC执行标记阶段。

      a. 为标记阶段做准备,将gcphase设置为_GCmark(从_GCoff),启用写屏障,启用mutator辅助,并排队等待根标记作业。

      在所有的Ps都启用写屏障之前,不可以对任何对象进行在所有的Ps都启用了写屏障之前,不得对任何对象进行扫描,这是用STW完成的。

      b. 启动执行。从此时起,GC工作由调度器启动的标记工完成,并由作为分配的一部分进行的辅助工作完成。

      写入屏障会遮蔽覆盖的指针和新的指针值。写入(详见mbarrier.go)。
      新分配的对象立即被标记为黑色。

      c. GC执行根标记工作。这包括扫描所有栈,对所有球体进行标记,并对堆外数据结构中的任何堆指针进行标记。

      堆外运行时数据结构中的任何堆指针。扫描一个堆栈会停止一个进程,对其堆栈中发现的任何指针进行着色,然后恢复该程序。

      d. GC耗尽工作队列中的灰色对象,将每个灰色对象的工作队列,将每个灰色对象扫描为黑色,并对该对象中发现的所有指针进行阴影处理。
      (反过来可能会将这些指针添加到工作队列中)。

      e. 因为GC的工作分散在本地缓存中,所以GC使用一种分布式终止算法来检测何时没有更多的根标记工作或灰色对象(见gcMarkDone)。

      这时,GC过渡到标记终止。

  • 3.GC执行标记终止。

    a. 停止执行。

    b. 将gcphase设置为_GCmarktermination,并禁用worker和助手。

    c. 进行内务管理,如刷新缓存。

    • 4.GC执行扫除阶段。

      a. 通过将gcphase设置为_GCoff,为扫除阶段做准备。

                a. 通过将gcphase设置为_GCoff,为清扫阶段做准备,设置清扫状态并禁用写屏障。
      
                b. 启动执行。从这时起,新分配的对象是白色的,必要时在使用前分配扫频对象。
      

      c. GC在后台进行并发的清扫,并响应响应分配。见下面的描述。

  • 5.当有足够的分配发生时,重放上面的序列从上面的1开始。参见下面关于GC执行频率的讨论。

并行的清理。

扫除阶段与正常的程序执行同时进行。堆被逐个对象地扫过(当一个goroutine需要另一个对象时)。

这将与在后台goroutine中同时进行(这有助于不受CPU约束的程序)。在STW标记终止时,所有span对象都被标记为 "需要清理"。

背景扫频的goroutine简单地逐一执行扫描。

为了避免在有未扫的对象时请求更多的操作系统内存,当一个时,它首先会试图通过清扫来获取更多的内存。

通过清扫,当一个goroutine需要分配一个新的小对象对象时,它会扫除相同对象大小的小对象对象,直到它至少释放了一个对象。

当一个goroutine需要从堆中分配大对象对象时。它就会扫描对象,直到它至少释放了那么多页到堆中。

有一种情况,这可能是不够的:如果一个goroutine清扫并释放了两个不相邻的一页对象到堆中,它将分配一个新的两页的。

但仍有其他未扫除的单页对象可以被合并为一个双页范围。合并成一个两页的对象范围。

确保不对未扫除的跨页进行任何操作是非常重要的(这将破坏GC位图中的标记位)。

在GC过程中,所有的缓存都被刷入中央缓存。所以它们是空的。当一个goroutine在mcache中抓取一个新的对象时,它就会对其进行扫描。

当一个goroutine明确释放一个对象或者设置一个finalizer时,它将确保该对象被扫除(要么扫除,要么等待并发的扫除结束)。

只有当所有对象都被扫过之后,finalizer goroutine才会被启动。

当下一次GC开始时,它会清扫所有尚未清扫的对象(如果有的话)。

  • GC执行频率。

下一次GC是在我们分配了一个额外的内存量之后,这个内存量与已经在使用的数量。这个比例是由GOGC环境变量控制的(默认为100)。

如果GOGC=100,并且我们正在使用4M,那么当我们达到8M时,我们将再次分配GC。
(这个标记是由gcController.heapGoal方法计算的)。

这使GC成本与分配成本保持在与分配成本成线性比例。调整GOGC只是改变了这个线性常数(也包括使用的额外内存量)。

为了防止在扫描大型对象时出现长时间的停顿,并提高并行性,垃圾收集器将扫描对象的工作分割开来。

大于maxObletBytes的对象的扫描工作分成 "blets",最多不超过maxObletBytes。

当扫描遇到一个大的时,它只扫描第一个blet,并将其余的blet排队作为新的扫描任务。
oblets作为新的扫描任务。

调用后运行一次垃圾回收,并阻止调用者,直到垃圾收集完成。它也可能阻塞整个程序。

我们认为一个周期是:扫描终止、标记、标记 终止,以及扫描。
这个函数不应该返回, 直到一个完整的周期完成,从开始到结束全部完成。
因此,我们总是希望完成当前的周期并开始一个新的周期。

        1.在扫频终止、标记、或标记终止的周期中N时,等待标记终止N的完成,并转入到扫频N。

        2.在扫频N中,帮助扫频N。这时我们可以开始一个完整的循环N+1。

        3.通过启动扫频终止N+1来触发N+1循环。

        4.等待标记终止N+1完成。

        5.帮助扫频N+1,直到它完成。

这一切都要标记出来,以应对GC可能会自己向前推进。例如,当我们封锁直到标记终止N,我们可能会在N+2周期中被唤醒。

等到当前的清扫终止、标记、以及标记终止完成。

2 竞争检测 runtime race

文件只是确保我们在竞争构建中链接到 runtime/cgo。 这又确保了运行时使用pthread_create来创建线程。

预建的竞争运行时位于race_GOOS_GOARCH.syso中。 对运行时的调用是直接从 src/runtime/race.go 中进行的。

在进化的系统中,我们总是使用系统DLLs来创建线程。

所以我们使用 racedarwin$GOARCH.go 来提供源自 syso 的 符号信息,而不需要调用cgo。

这使得-race可以在没有C工具链的Mac系统上使用。

实现数据竞争检测逻辑。 没有提供公共接口。 关于竞争检测器的细节,见:

       https://golang.org/doc/articles/race_detector.html

3 运行时调试 runtime debug

软件包debug包含了程序在运行时进行自我调试的功能。 它们正在运行。

  • 函数 Stack()

Stack返回调用它的goroutine的格式化的堆栈跟踪。
它调用runtime.Stack,有足够大的缓冲区来捕捉整个跟踪。

  • 函数 ReadGCStats()

func ReadGCStats(stats *GCStats)

ReadGCStats将有关垃圾收集的统计数据读入统计信息。 暂停历史的条目数与系统有关。
stats.Pause slice如果足够大将被重复使用,否则将被重新分配。

ReadGCStats 可以使用 stats.Pause slice 的全部容量。

如果stats.PauseQuantiles非空,ReadGCStats将用量值填充它总结暂停时间的分布。

例如,如果 len(stats.PauseQuantiles)是5,它将被填入最小。
25%、50%、75%和最大停顿时间。

4 运行时原子操作 runtime internal atomic

runtime internal atomic 提供原子操作,独立于sync/atomic。
向运行时提供原子操作。

在大多数平台上,编译器都知道这个包中定义的函数中定义的函数,它们被替换成特定平台的本征。
在其他平台上,通用的实现是可用的。

除非另有说明,本包中定义的操作在顺序上是在它们所操作的值方面,在不同的线程之间是一致的。

更具体地说具体来说,在一个线程上以特定顺序发生的操作。

将总是被另一个线程观察到以完全相同的顺序发生。

5 数学溢出检测 runtime internal math

在支持的平台上,这是一个由编译器降低的内在因素。

MulUintptr(a, b uintptr) (uintptr, bool) 
MulUintptr返回a * b以及乘法是否溢出。

乘积位的上半部分在hi中返回,下半部分在lo中返回。

Mul64(x, y uint64) (hi, lo uint64)
Mul64返回x和y的128位乘积:(hi, lo)= x * y

上半部分返回到lo中。

这是从math/bits.Mul64复制过来的。

在支持的平台上,这是由编译器降低的一个内在因素。

6 帮助函数 runtime internal startlinetest

包含 runtime_test.TestStartLineAsm 的帮助程序。

7 系统运行时 runtime internal sys

包含系统、配置和架构特定的运行时使用的常量。

    type NotInHeap struct{ _ nih }

NotInHeap是指一个类型决不能从GC'd堆或堆栈上分配。并且被称为不在堆中。

1 其他类型可以嵌入NotInHeap以使其不在堆中。

具体来说,指向这些类型的指针指向这些类型的指针必须总是不能通过runtime.inheap检查。

该类型可以被用于用于全局变量,或用于非托管内存中的对象(例如,用
sysAlloc, persistentalloc, rfixalloc, 或从人工管理的对象中分配)。

new(T), make([]T), append([]T, ...)

隐式堆T的//分配是不允许的。(尽管隐式分配在
隐式分配在运行时也是不允许的)。

  1. 常规类型的指针

    (除 "unsafe.Pointer "外)不能被转换为普通类型的指针。
    转换成一个非堆内类型的指针,即使它们的底层类型相同,相同的底层类型。

3.任何包含非堆内类型的类型本身被认为是非堆内类型。

- 如果结构和数组的元素不在堆内,那么它们就是不在堆内。
- 不允许地图和通道包含非堆内类型。

4.可以省略指向非堆内类型的指针的写入障碍。

最后一点才是NotInHeap的真正好处。
运行时使用它用于低级别的内部结构,以避免在调度器和内存分配器中出现内存障碍。

因为它们是非法的,或者仅仅是低效率的地方。这种机制是相当安全的,并且不会影响到运行时的可读性。

目录
相关文章
|
4月前
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
63 0
|
6月前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
3月前
|
算法 安全 Java
|
6月前
|
算法 Java 大数据
深入理解Go的垃圾回收机制
深入理解Go的垃圾回收机制
106 0
|
6月前
|
数据可视化 Java 测试技术
【Go语言专栏】Go语言中的内存泄漏检测与修复
【4月更文挑战第30天】Go语言内存泄漏详解:概念、原因、检测与修复。内存泄漏由忘记释放内存、循环引用等引起,Go通过垃圾回收机制管理内存,但仍有泄漏风险。检测方法包括pprof、可视化工具、代码审查和单元测试。修复策略涉及优化代码、使用defer、减少全局变量、弱引用及及时释放资源。实践案例分析有助于理解和解决问题。了解内存管理,防止泄漏,提升Go应用性能和稳定性。
309 0
|
人工智能 Linux 程序员
go vet中的那些检测项(1)
go vet中的那些检测项(1)
178 0
|
6月前
|
存储 算法 Java
Go语言GC(垃圾回收)的工作原理
【2月更文挑战第23天】
151 0
|
6月前
|
算法 Java 程序员
没想到,Go语言垃圾回收是这样工作的!
没想到,Go语言垃圾回收是这样工作的!
72 0
没想到,Go语言垃圾回收是这样工作的!
|
6月前
|
监控 Java 编译器
优化Go语言程序中的内存使用与垃圾回收性能
【2月更文挑战第5天】本文旨在探讨如何优化Go语言程序中的内存使用和垃圾回收性能。我们将深入了解内存分配策略、垃圾回收机制,并提供一系列实用的优化技巧和建议,帮助开发者更有效地管理内存,减少垃圾回收的开销,从而提升Go程序的性能。
|
6月前
|
算法 Java Go
Go语言中的垃圾回收机制:原理、优化与影响
【2月更文挑战第5天】本文深入探讨了Go语言中的垃圾回收机制,包括其工作原理、性能优化方法以及对程序运行的影响。通过了解这些内容,读者可以更好地理解Go语言的内存管理特点,并在实际开发中更好地应对垃圾回收带来的挑战。
下一篇
无影云桌面