当前位置: 首页 > article >正文

Golang——内存(内存管理、内存逃逸、垃圾回收 (GC) 机制)

本文详细介绍Go内存相关的内容,包括内存管理、内存逃逸、垃圾回收 (GC) 机制的三色标记,写屏障。

在这里插入图片描述

文章目录

    • Go 的内存管理
      • mspan && arenas
        • arenas
        • mspan
      • mcache
      • mcentral
      • mheap
      • 内存分配逻辑
      • 内存回收逻辑
      • 优势
      • 内存碎片
    • Go 的内存逃逸
      • 什么是内存逃逸?
      • 内存逃逸的检测:逃逸分析
      • 典型的内存逃逸
      • 内存逃逸的影响
      • 优化建议
      • 总结
    • Go 的垃圾回收 (GC) 机制
      • 特点
      • 三色标记法工作原理
      • 垃圾回收的触发条件
      • GC 的影响与优化
      • Go GC 流程
      • 非分代回收 (Non-generational GC)
        • 分代回收
        • 为什么 Go 不使用分代回收?
        • 非分代回收缺点
      • Go 的写屏障 (Write Barrier)
        • 写屏障是什么?
        • 为什么需要写屏障?
        • 混合写屏障策略
          • 插入屏障 (Insertion Barrier)
          • 删除屏障 (Deletion Barrier)
          • 混合写屏障 (Hybrid Write Barrier)
          • 混合写屏障的特点
          • 混合写屏障的规则
        • 写屏障的性能优化
        • 写屏障的作用
        • 写屏障的局限性

Go 的内存管理

Go 借鉴了 Google 的 TCMalloc(高性能的、用于 C++ 的内存分配器)。其核心思想是 内存池 + 多级对象管理,能加快分配速度,降低资源竞争。

在 Go 里用于内存管理的对象结构主要是:arenas && mspan、mcache、mcentral、mheap

  • mspan 是一个基础结构,分配内存时,基本以它为单位,并通过 arena 管理更大范围的内存【每级都是多个 mspan 组成】。
  • mheapmcentralmcache 起到了多级内存池的作用,当有对应大小的对象需要分配时会先到它们这一层请求。
  • 如果当前层内存池不够用时,会按照【mcache -> mcentral -> mheap -> 操作系统】顺序一层一层地往上申请内存。

在 Go 内存管理中,构成了多层次的内存管理架构。它们分别负责不同级别的内存分配需求,从全局分配到本地缓存,确保内存分配高效且低竞争。

  • mcache: 本地缓存,为 P 提供快速分配的内存块。
  • mcentral: 中间管理层,按 sizeclass 分类管理 mspan
  • mheap: 全局内存管理器,负责大对象分配和向操作系统申请内存。

三者之间的分层设计,使得 Go 的内存管理既高效又灵活。

总体上来看,Go 内存管理也是一个金字塔结构:
在这里插入图片描述

mspan && arenas

先来看看 mspan 这个基础结构体。首先,当 Go 在程序初始化的时候,会将申请到的虚拟内存划分为以下三个部分:
在这里插入图片描述

总体上来讲,spansbitmap 区域可以看作是 arenas 区域的元数据信息,辅助内存管理。

  • arenas:
    • 表示内存池,划分为连续的页面(8KB 为单位)。
  • spans:
    • 管理一组连续页面,作为分配内存的单位。
  • bitmap:
    • 记录 arena 的分配状态和 GC 信息,辅助垃圾回收。
arenas

arenas: 动态分配的堆区,它将分配到的内存以 8k 为一页进行管理。
在这里插入图片描述

mspan

arenas这个单位还是太细了,因此再抽象出 mspan 这一层来管理arenasmspan 记录了这组连续页面的起止地址、页数量、以及类型规格。

关于 mspan 的类型规格有 67 种,每一种都被定义了一个固定大小,当有对象需要分配内存时,就会挑选合适规格的 mspan 分配给对象。


mcache

mcache 是 Go 中为每个逻辑处理器(P)分配的本地内存缓存。每个 P 拥有一个独立的 mcache

  • alloc: 缓存不同规格的 mspan,用于快速分配内存。
  • tiny: 提供微型分配器,用于分配 ≤16B 的小对象。

作用

  1. 本地内存分配:
    • 优先从 mcache 分配内存,避免线程间的竞争。
  2. 快速分配:
    • mcache 是运行时的最底层内存分配器,分配速度最快。
  3. 减少锁开销:
    • mcache 的分配无需加锁,因为每个 P 独占一个 mcache

mcentral

mcentral 是 Go 中用于管理不同规格(sizeclass)的 mspan 的中间层。它负责为 mcache 提供内存块(mspan)。

作用

  1. 分级管理:
    • mcentral 会按 sizeclassmspan 分为 67 种规格。
  2. 分配内存:
    • 当本地缓存(mcache)的内存不足时,会向 mcentral 请求新的 mspan
  3. 减少竞争:
    • 通过分级管理,避免了对不同大小内存的分配竞争。

mheap

mheap 是 Go 内存管理的全局对象,负责向操作系统申请内存,并管理大块的内存区域。它是整个内存分配系统的顶层。

作用

  1. 大对象分配:
    • 当对象大小超过 32KB 时,直接从 mheap 分配内存。
  2. 内存来源:
    • mcentralmcache 需要新的内存时,会向 mheap 请求。
  3. 内存管理:
    • 管理所有的 arenas 和大块的内存区域。
    • 提供空闲的 span 给下级(mcentralmcache)。

内存分配逻辑

  1. 如果 object size > 32KB,则直接使用 mheap 来分配空间,mheap 中有一个 freelarge 字段管理着超大 span;
  2. 如果 object size < 16Byte,则通过 mcache 的 tiny 分配器来分配(tiny 可看作是一个指针 offset);
  3. 如果 object size 在上述两者之间,首先尝试通过 sizeclass 对应的分配器分配;
  4. 如果 mcache 的分配器没有空闲的 span 分配,则向 mcentral 申请空闲块;
  5. 如果 mcentral 也没空闲块,则向 mheap 申请并进行切分;
  6. 如果 mheap 也没合适的 span,则向系统申请新的内存空间。

内存回收逻辑

  1. 如果 object size > 32KB,直接将 span 返还给 mheap 的自由链;
  2. 如果 object size < 32KB,查找 object 对应 sizeclass,归还到 mcache 自由链;
  3. 如果 mcache 自由链过长或内存过大,将部分 span 归还到 mcentral
  4. 如果某个范围的 mspan 都已经归还到 mcentral,则将这部分 mspan 归还到 mheap 页堆;
  5. mheap 不会定时将内存归还到系统,但会归还虚拟地址到物理内存的映射关系,当系统需要的时候可以回收这部分内存,否则暂时先留着给 Go 使用。

优势

  1. 用户态完成分配:

    • 内存分配大多在用户态完成,不需要频繁进入内核态。
  2. 快速分配,优化碎片管理:

    • 本地缓存 (mcache) 无需加锁,分配效率极高。
    • Go 自己在用户态管理内存,固定大小分类(67 种 sizeclass)减少碎片。
  3. 减少CPU 竞争,支持高并发:

    • 每个逻辑处理器 (P) 独享本地缓存,避免线程间锁竞争。
    • sizeclass 分类分配,降低资源冲突,适合处理大量 goroutine。

内存碎片

系统在内存管理过程中产生的一种现象,表现为无法被有效利用的内存空间。解决内存碎片问题是内存管理优化的重要目标。分为 内部碎片外部碎片

  • 内部碎片: 分配的内存大于实际需求,导致浪费。
  • 外部碎片: 小块空闲内存分散分布,难以利用。

  1. 内部碎片:
    • 分配给进程的内存区域中,有些部分未被使用。
    • 原因:
      1. 字节对齐:
        • 为了满足字节对齐的要求,会额外分配一些内存空间,但这些空间未被实际使用。
      2. 固定大小分配:
        • 申请 28B 内存时,系统可能按 32B 的单位分配,多出的 4B 即为浪费。
    • 特点:
      • 内部碎片主要表现为分配的内存大于实际需求,但超出的部分不能被利用。

  1. 外部碎片:
    • 内存中存在一些小的空闲分区,这些分区没有被分配给任何进程,但由于过小,难以被再次利用。
    • 原因:
      • 内存反复分配和释放,导致小块内存分布在内存的各个位置。
      • 空闲的内存块难以合并成更大的内存区域。
    • 特点:
      • 外部碎片导致内存空间虽然有剩余,但因为分布不连续,无法满足大块内存的分配需求。

Go 的内存逃逸

什么是内存逃逸?

  • 定义: 当一个对象从 中逃逸到 中分配内存时,就称为内存逃逸。
  • 结果:
    • 栈内存对象在函数结束时会被自动回收。
    • 堆内存对象需要垃圾回收器(GC)处理。
  1. 栈 (Stack):

    • 特点:
      • 线性存储,由编译器自动分配和回收。
      • 用于存储函数参数和局部变量。
    • 优点:
      • 内存分配和释放的代价极低,仅需两条指令(PUSHPOP)。
      • 内存生命周期与函数执行周期一致,无需 GC 介入。
    • 缺点:
      • 受限于栈的大小,生命周期有限。
  2. 堆 (Heap):

    • 特点:
      • 用于动态分配,存储生命周期不确定的对象。
      • 内存由垃圾回收器(GC)管理。
    • 优点:
      • 适合存储生命周期超出函数范围的对象。
    • 缺点:
      • 分配和回收的代价较高,依赖 GC,会增加性能开销。

内存逃逸的检测:逃逸分析

  • 逃逸分析:
    编译器在编译阶段对变量生命周期进行分析,决定变量分配在堆还是栈。

    jvm是运行时逃逸分析;
    程序变量会携带有一组校验数据,检查变量的生命周期是否是完全可知的,如果通过检查,则可以在栈上分配。否则,就是所谓的逃逸,必须在堆上进行分配。

  • 分析准则:
    1. 如果变量在函数外部没有引用,则分配在栈中。
    2. 如果变量在函数外部存在引用,则分配在堆中。
    3. 栈上分配更高效,不需要 GC 处理。
    4. 堆上分配适用于生命周期不可知或较长的变量。

典型的内存逃逸

以下是容易导致变量逃逸到堆的情况:

  • 返回局部变量的指针。
  • 指针或带指针的值被发送到 channel
  • 切片中存储指针。
  • 切片的底层数组因扩容而重新分配。
  • 使用 interface 类型或动态分配内存。
  1. 指针返回:

    • 函数内定义的局部变量通过指针返回:
      func escape() *int {
          x := 10
          return &x  // x 逃逸到堆中
      }
      
  2. 指针发送到 channel:

    • 变量的指针或带指针的值被发送到 channel 中:
      go func(ch chan *int) {
          val := 10
          ch <- &val // val 逃逸到堆中
      }(ch)
      
  3. 切片存储指针:

    • 切片中存储指针值导致背后数组的内存逃逸:
      func example() {
          strs := []*string{}
          val := "hello"
          strs = append(strs, &val) // val 逃逸到堆中
      }
      
  4. 切片扩容:

    • 切片的容量动态扩展,导致新的内存分配到堆:
      func example() {
          slice := make([]int, 2)
          slice = append(slice, 1) // 扩容时,背后的数组逃逸到堆中
      }
      
  5. 动态类型调用:

    • 使用 interface 类型时,接口的动态方法调用使得编译器无法确定对象的生命周期。会导致内存逃逸:io.Reader 是接口类型

      func readExample(r io.Reader) {
          buf := make([]byte, 10)
          r.Read(buf) // buf 逃逸到堆中
      }
      

内存逃逸的影响

  1. 优点:

    • 提供灵活性,支持生命周期不确定或超出函数范围的对象。
  2. 缺点:

    • 增加垃圾回收压力,降低程序性能。
    • 堆分配的内存需要更多的系统资源和管理成本。

优化建议

  1. 减少不必要的指针传递:

    • 避免使用指针返回局部变量,改为直接返回值。
  2. 减少动态分配:

    • 使用固定大小的数组或切片,避免频繁扩容。
  3. 优化 interface 使用:

    • 尽量使用具体类型,减少 interface 动态分配。

总结

  • 内存逃逸 是 Go 内存分配过程中,为了确保变量生命周期正确而将其从栈移到堆的现象。
  • 编译器通过 逃逸分析 决定变量是否需要从栈逃逸到堆。
  • 堆分配虽然提供灵活性,但增加了性能开销,应尽量避免不必要的逃逸。
  • 在性能敏感的代码中,通过值传递和优化动态分配可以有效减少逃逸的发生。

Go 的垃圾回收 (GC) 机制

GC(Garbage Collection)垃圾回收是一种自动管理内存的方式,无需手动管理内存,程序能够检测和清除不再被使用的内存块,避免内存泄漏,使开发人员从内存管理上解脱出来。

Go 语言内置了一套现代化、高效的垃圾回收 (Garbage Collection, GC) 机制,用于自动管理内存分配和释放。以下是 Go GC 的详细介绍:


特点

  1. 并发 (Concurrent):

    • Go 的 GC 与程序的其他部分同时运行,不会完全阻塞程序的执行。
    • 标记阶段主要部分是并发的,通过写屏障确保标记的正确性。
    • 清理阶段默认是并发,极少使用串行清理;
  2. 三色标记清除算法 (Tri-color Mark and Sweep):

    • Go 使用三色标记清除算法,是一种高效的垃圾回收技术。
  3. 非分代回收 (Generational):

    • Go 的 GC 会优先回收短生命周期对象,同时针对长期存在的对象优化性能。
  4. 可配置性:

    • 开发者可以通过 GOGC 环境变量调整垃圾回收器的灵敏度。
  5. 写屏障:

    • Go 的GC使用混合写屏障策略,在标记阶段与程序并发运行,同时保持标记的准确性和一致性。

三色标记法工作原理

  1. 初始状态:

    • 所有对象都被标记为白色。
    • 根对象(全局变量、栈上的变量等)被标记为灰色。
  2. 标记过程:

    • 遍历灰色对象,并将其引用的对象标记为灰色,直到没有灰色对象。
    • 将已处理的灰色对象标记为黑色。
  3. 清理过程:

    • 删除仍为白色的对象,因为这些对象不可达。

垃圾回收的触发条件

  1. 手动触发:

    • 使用 runtime.GC() 主动触发垃圾回收。
  2. 自动触发:

    • Go 的 GC 会根据内存使用情况自动触发。
    • GOGC 环境变量控制触发频率:
      • 在申请内存的时候,检查当前已分配的内存是否大于上次GC后的内存的2倍(默认)。
      • 监控线程(sysmon)检测到自上次 GC 已超过一定时间(例如两分钟),它会触发一次 GC。

GC 的影响与优化

  1. 优点:

    • 简化内存管理,减少内存泄漏。
    • 自动化管理内存,开发者不需要手动释放内存。
    • 支持并发和实时性,适合高并发场景。
  2. 缺点:

    • 存在一定的性能开销。
    • 对延迟敏感的程序可能会受到影响。
  3. 优化方法:

    • 优化内存分配: 减少短生命周期对象的创建,避免频繁触发 GC。
    • 减少逃逸: 使用值类型替代指针,尽量避免变量逃逸到堆中。
    • 调整 GOGC: 根据场景调整 GOGC 值以平衡性能和内存使用。

Go GC 流程

GC 的正确流程是:

  1. Stop-the-World: 暂停所有业务逻辑,确保初始状态的一致性。
  2. 标记: 并发标记存活对象,标记过程与应用程序同时运行(并发标记,需要用到写屏障)。
  3. 再次Stop-the-World: 主要写屏障带来的问题,确保标记阶段中所有引用的动态变化都被正确处理。
  4. 清理: 回收未标记的对象,清理可以并发或串行完成。
  5. Start-the-World: 恢复业务逻辑执行。

STW(Stop-the-World)仅用于标记的开始和结束阶段。标记阶段后需要一个非常短暂的 Stop-the-World 来确保标记阶段的一致性和完成状态。确保标记过程中的所有写屏障操作都已处理。
清理阶段(Sweep)通常是并发完成的,不需要 STW。

  1. Stop-the-World (STW):
    • 目的:

      • 确保垃圾回收器(GC)在标记阶段能够准确扫描所有活跃对象。
      • 阻止新的对象分配干扰标记过程。
    • 具体操作:

      • 设置 gcwaiting=1,通知所有 M(系统线程)进入休眠状态。
      • 通过让当前运行中的 G(goroutine)完成或中断任务,确保所有 M 都暂停。
    • 解释:

      • Modern Go (>= 1.5) 使用了并发 GC,尽量缩短 STW 的时间。在并发标记清除的算法下,STW 时间通常仅限于标记过程的开始和结束,而标记本身是在并发阶段完成的。

  1. 标记 (Mark):

    • 目的:

      • 找出程序中仍然存活的对象,并标记它们为活跃。
    • 过程:

      1. 分配任务:
        • 将标记任务分成若干段,分配给 gcproc 个 M(系统线程),其中 gcproc 的默认值为逻辑处理器 P 的数量。
        • 每个 M 在被唤醒后,检查自身的 helpgc 标记是否为 true,如果是,则开始执行标记任务。
      2. 并发标记:
        • 当前运行的 M 和其他被唤醒的 M 会并发执行标记任务。
        • 如果某个 M 的任务完成,会从其他未完成任务的 M 中“偷取”标记任务,直到所有标记任务完成。
      3. 进入休眠:
        • 标记任务完成后,所有参与 GC 的 M 再次进入休眠状态。
    • 解释:

      • Go 的三色标记法(白、灰、黑)用于跟踪对象引用状态。
      • 标记阶段尽量减少对程序逻辑的影响,大部分标记任务是在并发模式下完成的。
  2. 标记阶段结束的 STW

  • 目的:

    • 确保标记阶段中所有引用的动态变化都被正确处理。
    • 处理通过 写屏障 收集的对象引用变更。
  • 过程:

    • 在标记阶段,应用程序可能继续运行,导致对象的引用关系发生变化。
    • 写屏障 记录了这些动态变更。
    • 在标记结束时,STW 确保所有这些变更被应用,保证标记的最终一致性。
  • 特点:

    • 这一 STW 时间非常短,通常只需数十微秒到几毫秒。
    • 这是现代 Go (>= 1.5) 的重要优化点,尽量将 STW 时间压缩到最小。

  1. 清理 (Sweep):
    • 目的:
      • 回收未被标记的对象所占用的内存。
    • 过程:
      • 清理阶段可以选择串行或并发执行:
        • 并发清理: 通过单独的 Goroutine 执行,不需要阻塞业务逻辑(Go >= 1.3 默认行为)。
        • 串行清理: 清理任务与主 GC 线程绑定,可能导致较长的 STW 时间。
    • 解释:
      • 并发清理的引入极大地降低了 GC 对业务逻辑的影响。
      • 清理阶段主要释放未标记的内存,回收至内存池。

  1. Start-the-World (STW 结束):
    • 目的:
      • 恢复业务逻辑执行,唤醒所有线程(最多等于 P 的数量)。
    • 过程:
      • 设置 gcwaiting=0,通知所有的 M 线程业务逻辑恢复正常。
      • 唤醒 P 个 M,继续调度执行业务逻辑中的 G。
    • 解释:
      • Start-the-World 阶段保证垃圾回收完成后程序能够立即恢复。

Go 不是分代回收 (Generational GC),而是采用了 非分代回收 (Non-generational GC) 策略。


非分代回收 (Non-generational GC)

  • Go 的 GC 是非分代的:

    • 不区分对象的生命周期,对所有对象一视同仁地标记和回收。
    • 强调简洁、高效和低延迟的实现。
  • 虽然非分代回收可能在短生命周期对象的处理上效率不如分代回收,但 Go 的整体设计使其在并发、高性能场景中表现优异。

分代回收

分代回收是一种常见的垃圾回收优化技术,它将内存中的对象按照生命周期的长短分成不同的“代”,通常包括:

  • 年轻代: 短生命周期的对象,回收频率高。
  • 老年代: 长生命周期的对象,回收频率低。
  • 持久代: 很少或几乎不会被回收的对象(如类元数据)。

核心思想:

  • 大多数对象会很快变成垃圾(“弱代假设”)。
  • 针对短生命周期和长生命周期的对象,采用不同的回收策略可以提高性能。

分代回收的典型代表是 Java 的 JVM 的垃圾回收器。


为什么 Go 不使用分代回收?
  1. 简化实现:

    • 分代回收需要额外的数据结构和复杂性来管理不同代的内存区域,而 Go 的设计目标是简洁高效。
  2. 内存模型不同:

    • Go 的并发模型(大量 Goroutine 和高频率的内存分配)使得分代回收的复杂性和性能开销可能得不偿失。
  3. 实时性需求:

    • Go 强调低延迟、实时性,非分代回收的设计让 Go 更容易实现垃圾回收与业务逻辑的并发执行。

非分代回收缺点
  1. 短生命周期对象的回收成本较高:
    • 对于短生命周期的对象,仍然需要完整扫描,增加了一定的性能开销。
  2. 内存使用效率可能稍低:
    • 无法像分代回收那样对不同生命周期的对象采用优化策略。

Go 的写屏障 (Write Barrier)

写屏障是现代垃圾回收器中的一项关键技术,用于在垃圾回收的 并发标记阶段 追踪程序运行时对象引用的动态变化。Go 的垃圾回收器使用 混合写屏障 策略,这种设计能够在标记阶段与程序并发运行,在保证 GC 效率和正确性的同时,将 Stop-the-World (STW) 的时间压缩到最短,这是 Go 高效 GC 的重要基础。


写屏障是什么?

写屏障是一种机制,当程序修改对象的引用关系(即写入内存)时,写屏障会执行额外的操作,确保垃圾回收器能够正确地跟踪这些引用的变化。

  • Go 的写屏障特点:
    1. 采用 混合写屏障 策略,结合插入屏障和删除屏障。
    2. 确保三色不变性,防止白色对象被错误回收。
    3. 仅在标记阶段启用,并发运行,与程序逻辑干扰最小。 在清理阶段或非 GC 期间无影响。
为什么需要写屏障?

Go 的垃圾回收 (GC) 使用 并发标记 (Concurrent Marking),在标记阶段,垃圾回收器和应用程序会同时运行。这种并发运行可能导致程序修改对象的引用关系,从而产生潜在问题,如下:

应用程序和垃圾回收器可以同时运行,因此对象确实可以在标记阶段被修改。我们考虑一下,下面的情况:

在这里插入图片描述

我们在进行三色标记中扫描灰色集合中,扫描到了对象 A,并标记了对象 A 的 所有引用,这时候,开始扫描对象 D 的引用,而此时,另一个 goroutine 修改 了 D->E 的引用,变成了如下图所示
在这里插入图片描述
这样会不会导致 E 对象就扫描不到了,而被误认为为白色对象,也就是垃圾。

写屏障就是为了解决这样的问题,引入写屏障后,在上述步骤后,E 会被认为 是存活的,即使后面 E 被 A 对象抛弃,E 会被在下一轮的 GC 中进行回收,这一 轮 GC 中是不会对对象 E 进行回收的。


混合写屏障策略

Go 的写屏障实现采用了 混合写屏障 (Hybrid Write Barrier) 策略。这种设计结合了两种经典的写屏障策略:插入屏障 (Insertion Barrier)删除屏障 (Deletion Barrier)

  • 混合屏障:
    • 插入屏障: 确保新加入的引用不会丢失,目标对象被正确标记。
    • 删除屏障: 记录被移除的旧引用,防止对象错误回收。
  • Go 的混合写屏障在实现时优先基于插入屏障的逻辑进行优化,而删除屏障的逻辑则只在特定情况下被补充实现。
插入屏障 (Insertion Barrier)

插入屏障的作用是在 对象引用发生新增时,对新引用的对象执行某些操作,以确保它们不会被错误回收。

工作原理:

  • 当一个对象的引用被修改为指向另一个对象(新增引用)时,插入屏障会立即将新引用的对象从白色(未标记)变为灰色(待标记)
  • 插入屏障保证了新加入的对象会被正确处理。

优点:

  • 保证新增的引用关系不会丢失。
  • 简化了垃圾回收器的实现逻辑。

缺点:

  • 可能无法处理对象引用被移除的场景。

删除屏障 (Deletion Barrier)

删除屏障的作用是在 对象引用被移除时,对被移除的对象(尤其是白色和灰色对象)执行某些操作,以确保它们在标记阶段被正确处理。

移除引用的对象不一定是需要回收的,它们可能仍然是存活的对象,只是被移除了当前的引用关系。

工作原理:

  • 当对象的引用被移除时,删除屏障会记录下被移除的对象,确保这些对象在当前回收周期中不会被错误地处理或遗漏。
  • 删除屏障强调保护“旧引用”。
  1. 如果被移除引用的对象是白色(未标记):

    • 删除屏障会立即 将该对象从白色变为灰色
    • 目的:确保该对象在标记阶段被进一步处理,而不是被错误回收。
  2. 如果被移除引用的对象是灰色(已访问但其引用未完全处理):

    • 删除屏障保持灰色状态不变。
    • 目的:确保 GC 正确完成对灰色对象的递归标记。
  3. 如果被移除引用的对象是黑色(已完全处理):

    • 删除屏障无需任何额外操作。
    • 目的:黑色对象已经完全处理,不会受引用变化的影响。

优点:

  • 能够处理对象引用被移除的场景。
  • 防止白色对象被错误回收。

缺点:

  • 当引用频繁新增和移除时,性能开销较大。

混合写屏障 (Hybrid Write Barrier)

混合写屏障结合了插入屏障和删除屏障的特点,能够同时处理 引用新增引用移除 的场景。Go 的写屏障实现采用了这种策略。

工作原理:

  1. 新增引用:
    • 当一个对象的引用被修改为指向新对象时,插入屏障逻辑会确保新引用的对象被标记为灰色,避免漏标。
  2. 移除引用:
    • 当一个对象的引用被移除时,删除屏障逻辑会确保被移除的对象不会被错误回收。
混合写屏障的特点

优点:

  1. 完整性:
    • 同时处理新增和移除引用,保证三色标记算法的正确性。
  2. 性能优化:
    • 优化了插入屏障的逻辑,在标记阶段优先处理新增引用。
    • 对删除屏障的逻辑只在必要时执行,避免了不必要的性能开销。
  • 只对三色标记阶段生效:

缺点:

  • 写屏障的逻辑复杂度略高,需要额外处理插入和删除两种场景。
  • 增加了一定的内存写操作开销。

混合写屏障的规则
  1. 目标:确保三色不变性 (Tri-color Invariant):

    • 三色标记法中对象的三种状态:
      • 白色:尚未访问,可能是垃圾。
      • 灰色:已访问但其引用未完全处理。
      • 黑色:已访问且其引用已完全处理。
    • 写屏障的任务是保证标记过程中,白色对象不会因为程序的引用变更被错误遗漏。
  2. 操作逻辑:

    • 当某个对象的引用被修改时:
      1. 如果目标对象是白色,将目标对象标记为灰色(放入灰色队列)。
      2. 确保修改后的引用关系被正确追踪。
写屏障的性能优化

写屏障会引入一定的性能开销,但 Go 的实现通过以下方式优化性能:

  1. 启用条件:

    • 写屏障仅在 GC 的标记阶段启用,在非 GC 阶段没有额外开销。
  2. 批量标记:

    • 写屏障会尽量以批量方式处理引用变更,减少对性能的影响。
  3. 内存屏障优化:

    • 写屏障逻辑使用轻量级的原子操作,最大程度降低对应用程序性能的干扰。
  4. 只处理必要对象:

    • 只有在标记阶段,且目标对象是白色时,写屏障才会执行额外操作,减少了不必要的屏障逻辑。

写屏障的作用
  1. 保证三色不变性:

    • 避免白色对象被程序动态引用后未被标记,确保对象不会被错误回收。
  2. 减少 Stop-the-World 时间:

    • 通过写屏障捕获动态引用,GC 无需在 STW 阶段完全重新扫描内存。
  3. 支持并发标记:

    • 写屏障与并发标记结合,提升了垃圾回收器的效率。

写屏障的局限性
  1. 性能开销:

    • 写屏障逻辑会增加内存写操作的开销,尤其是在高频引用修改的场景下。
  2. 复杂性:

    • 写屏障的实现需要保证低延迟,同时处理动态引用的复杂性。

http://www.kler.cn/a/510148.html

相关文章:

  • 【机器学习实战入门】基于深度学习的乳腺癌分类
  • 无人机(Unmanned Aerial Vehicle, UAV)路径规划介绍
  • LLM - 大模型 ScallingLaws 的迁移学习与混合训练(PLM) 教程(3)
  • 【转】厚植根基,同启新程!一文回顾 2024 OpenHarmony 社区年度工作会议精彩瞬间
  • mysql8.0 重要指标参数介绍
  • Mysql常见问题处理集锦
  • 学生管理系统C++版(简单版)
  • 使用Emgu.CV将tif保存视频,并用AxWindowsMediaPlayer打开
  • ant design vue的级联选择器cascader的悬浮层样式怎么修改
  • Word中如何格式化与网页和 HTML 内容相关的元素
  • 基于python对抖音热门视频的数据分析与实现
  • Linux网络序列化与反序列化
  • LINUX编译LibreOffice
  • React进阶之react.js、jsx模板语法及babel编译
  • 数据结构---并查集
  • Python学习(十三)什么是模块、模块的引入、自定义模块、常见的内置模块(math、random、os、sys、uuid、时间模块、加密模块)
  • 搭建一个基于Spring Boot的数码分享网站
  • [Qt]窗口-QDialog、QMessageBox、QColorDialog、QFileDialog、QFontFialog、QInputDialog对话框
  • 登录校验Cookie、Session、JWT
  • 【unity进阶篇】弧度、角度和三角函数(Mathf),并实现类似蛇的运动
  • Django SimpleUI 自定义功能实战
  • 【漏洞预警】FortiOS 和 FortiProxy 身份认证绕过漏洞(CVE-2024-55591)
  • 网络系统管理Linux环境——AppSrv之SSH
  • 天机学堂5-XxlJobRedis
  • 硬件学习笔记--34 GB/T17215.321相关内容介绍
  • C++实现设计模式---迭代器模式 (Iterator)