go基础

目录

基础资料

小林coding

1.golang基础

  • 数组和切片

    • 都是非线程安全
    • slice 扩容,1.18前cap<1024,扩大2倍,后面扩大1.25倍,1.18后<256扩大2倍,而后(oldcap+3*256)/4。
    • slice底层是数组,slice 是对数组的封装,数组需要使用字面量声明
    • 数组是定长的,slice是可以append动态扩容,数组就是一片连续的内存,不能使用append, slice 实际上是一个结构体
    type slice struct {
      array unsafe.Pointer // 元素指针
      len   int // 长度 
      cap   int // 容量
    }
    
    • 字符串和切片,字符串s[i]不可写,0拷贝成byte后,也不可写,底层指针没变。
    type StringHeader struct {
      Data uintptr
      Len  int
    }
    type SliceHeader struct {
      Data uintptr
      Len  int
      Cap  int
    }
    func s2b(s string) []byte {
      return *(*[]byte)(unsafe.Pointer(&s))
    }
    func b2s(b []byte) string{
      return *(*string)(unsafe.Pointer(&b))
    }
    
  • channel

    • 有缓冲通道
      • 发送操作会将元素放入缓冲区,只有当缓冲区满时发送操作才会阻塞。
      • 接收操作会从缓冲区中取出元素,只有当缓冲区为空时接收操作才会阻塞。
      • 当缓冲区不满时,发送操作和接收操作都是非阻塞的,它们之间的元素传递是异步的。
      • 当关闭通道时,不能在向里面写数据panic,读数据读不到。
    • 无缓冲通道
      • 无缓冲通道的容量为 0,意味着发送操作和接收操作是同步的。
      • 发送操作和接收操作都是原子的,即发送操作和接收操作之间的元素传递是保证原子性的。
      • 当关闭通道时,不能在向里面写数据panic,还有数据的话,可以读数据。
  • 闭包函数

    • 闭包函数里引用的外部变量,是在堆还是栈内存申请的,取决于,你这个闭包函数在函数 Return 后是否还会在其他地方使用,若会, 就会在堆上申请,若不会,就在栈上申请。
    • 闭包函数里,引用的外部变量,存储的并不是对值的拷贝,存的是值的指针。defer使用变量快照失效。
    • 函数的返回值里若写了变量名,则该变量是在上级的栈内存里申请的,return 的值,会直接赋值给该变量。
  • sync.Map、sync.Mutex、sync.WaitGroup、sync.Cond

  • go协程和线程

    • 内存占用,go协程内存占用小,会自动扩容
    • 创建销毁,go协程runtime管理,线程是内核级别开销大
    • 线程切换开销大

2.GMP

什么是GMP?

  • G:Goroutine,也就是 go 里的协程,是用户态的轻量级线程,具体可以创建多个 goroutine ,取决你的内存有多大,一个 goroutine 大概需要 4k 内存,只要你不是在 32 位的机器上,那么创建个几百万个的 goroutine 应该没有问题。
  • M:Thread,也就是操作系统线程,go runtime 最多允许创建 10000 个操作系统线程,超过了就会抛出异常
  • P:Processor,处理器,数量默认等于开机器的cpu核心数,若想调小,可以通过 GOMAXPROCS 这个环境变量设置。
  • M:N模型: runtime启动时会创建M个线程,之后N个协程依附在M上执行。

GMP工作原理

  • 初始化:当程序启动时,Go运行时会初始化一定数量的M和P,并开始执行程序的主函数。
  • 创建goroutines:当程序中有新的goroutine被创建时,它们会被加入到某个P的本地队列(localrun ueue)中。如果本地队列已满,goroutines会被放到全局队列(globalrunqueue)中。
  • 调度执行:每个P都有一个调度器(scheduler),它会负责从本地队列或全局队列中选择goroutines来执行。调度器会根据一定的策略选择goroutines,并将它们分配给关联的M来执行。
  • 执行goroutines:M会执行与其关联的goroutines。当一个goroutine阻塞时(如等待I/O操作完成),与之关联的M会释放处理器,去执行其他goroutines,以充分利用系统资源。
  • 退出和回收:当goroutine执行完成或被取消时,它会退出,并释放相关的资源。当程序退出时,所有的M和P也会被销毁,释放系统资源。

go调度时机

  • goroutine:go 创建一个新的 goroutine,Go scheduler 会考虑调度
  • GC:由于进行 GC 的 goroutine 也需要在 M 上运行,因此肯定会发生调度。当然,Go scheduler 还会做很多其他的调度,例如调度不涉及堆访问的 goroutine 来运行。GC 不管栈上的内存,只会回收堆上的内存
  • 系统调用SysCall,cgo调用:当 goroutine 进行系统调用时,会阻塞 M,所以它会被调度走,同时一个新的 goroutine 会被调度上来
  • 同步访问,阻塞:atomic,mutex,channel 操作等会使 goroutine 阻塞,因此会被调度走。等条件满足后(例如其他 goroutine 解锁了)还会被调度上来继续运行

什么情况阻塞

  • 系统调用和cgo调用
  • 通道操作,锁,网络IO,时间等待

Go调度器

将P绑定到一个合适的M 为P选中一个G来执行

GO调度策略

  • 复用线程
    • 窃取机制,从其他M绑定的P偷G,不让P空闲
    • 交接,G阻塞时,M将P解绑,把P转移给其他空闲的M执行
  • GOMAXPROCS
    • 设置P的数量
  • 抢占调度
    • 基于信号抢占:当一个goroutine处于运行状态时,超过20ms,直接让出cpu运行权
    • 协作式抢占:sysmon监控线程发现有G阻塞时,打上标记,让出CPU,切换到主协程里。

GMP为什么要有P

  • M需要从全局队列里获取G,高并发下,锁的性能瓶颈,P有自己的本地队列,减少锁的竞争。
  • 当M中的一个G阻塞时,P会重新选择或创建一个M执行G,提高效率。
  • GMP模型中复用线程,如果本地P空闲会从其他M绑定的P窃取G,提高了资源利用率。

3.内存管理

对象大小

类别 大小
微对象 (0, 16B)
小对象 [16B, 32KB]
大对象 (32KB, +∞)

内存管理组件

多级缓存,Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件,几种最重要组件对应的数据结构 runtime.mspan、runtime.mcache、runtime.mcentral 和 runtime.mheap. mheap,mcentral加锁,mcache独立的p拥有,不需要加锁。 结构图

  • mheap:全局的内存起源,访问要加全局锁
  • mcentral:每种对象大小规格(全局共划分为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内
  • mcache:每个 P(正是 GMP 中的 P)持有一份的内存缓存,访问时无锁

mspan内存管理单元

(1)page:最小的存储单元.

Golang 借鉴操作系统分页管理的思想,每个最小的存储单元也称之为页 page,但大小为 8 KB

(2)mspan:最小的管理单元.

mspan 大小为 page 的整数倍,且从 8B 到 80 KB 被划分为 67 种不同的规格,分配对象时,会根据大小映射到不同规格的 mspan,从中获取空间. alt

type mspan struct {
    // 标识前后节点的指针 
    next *mspan     
    prev *mspan    
    // ...
    // 起始地址
    startAddr uintptr 
    // 包含几页,页是连续的
    npages    uintptr 


    // 标识此前的位置都已被占用 
    freeindex uintptr
    // 最多可以存放多少个 object
    nelems uintptr // number of object in the span.


    // bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
    allocCache uint64
    // ...
    // 标识 mspan 等级,包含 class 和 noscan 两部分信息,不同等级分配不同大小内存
    spanclass             spanClass    
    // ...
}

• mspan 是 Golang 内存管理的最小单元

• mspan 大小是 page 的整数倍(Go 中的 page 大小为 8KB),且内部的页是连续的(至少在虚拟内存的视角中是这样)

• 每个 mspan 根据空间大小以及面向分配对象的大小,会被划分为不同的等级

• 同等级的 mspan 会从属同一个 mcentral,最终会被组织成链表,因此带有前后指针(prev、next)

• 由于同等级的 mspan 内聚于同一个 mcentral,所以会基于同一把互斥锁管理

• mspan 会基于 bitMap 辅助快速找到空闲内存块(块大小为对应等级下的 object 大小),此时需要使用到 Ctz64 算法.

内存分配

  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;如果都没有,直接向操作系统分配内存

GO GC

GC原理

主流GC算法

  • 引用计数
    • 为每个对象维护一个计数,当引用对象销毁时计数-1,为0的时候回收
    • 优点:对象回收快
    • 缺点:不好处理循环引用
  • 分代收集
    • 按照对象生命周期长短划分不同空间
    • 优点:性能好
    • 缺点:算法复杂
  • 标记清除
    • 从根变量开始遍历所有引用对象,没有被标记的清楚
    • 优点:解决引用计数问题
    • 缺点:需要STW,耗时慢

GO-GC标记清除

  • 标记阶段(Marking Phase):
    • 初始标记(Initial Mark):GC会暂停整个程序(STW,Stop The World),标记从根对象(根对象包括全局变量、栈上的局部变量等)可以直接访问到的所有对象。这部分通常会快速完成。
    • 并发标记(Concurrent Mark):在初始标记之后,GC和程序同时运行。GC会继续追踪并标记在初始标记之后新创建或被修改的对象。
    • 重新标记(Re-mark):在并发标记阶段结束时,GC会再次暂停程序,确保没有遗漏任何存活对象。这是为了确保并发标记阶段新创建或被修改的对象也能被正确标记。
  • 清除阶段(Sweeping Phase):
    • 并发清除(Concurrent Sweep):在标记阶段标记完所有存活对象之后,GC会并发清理所有未被标记的对象,回收它们所占用的内存。这个阶段通常也是并发进行的,不会对程序的运行造成明显的暂停。
标记算法-三色标记法

在标记过程中,GC使用三种颜色(白色、灰色和黑色)来跟踪对象状态。

  • 白色表示未标记对象,
  • 灰色表示已标记但其子对象尚未完全标记的对象,
  • 黑色表示已标记且其子对象也已完全标记的对象。

遍历过程:

  • 1.创建白色、灰色、黑色集合
  • 2.将所有对象放入白色集合中
  • 3.遍历所有root对象,将遍历的到对象从白色集合转移到灰色集合中。
  • 4.遍历灰色对象,将灰色对象引用的对象从白色集合放到黑色集合,自身标记成黑色
  • 5.重复步骤4,知道灰色中无任何对象,其中用到的2个机制:
    • 写屏障:让程序和GC并发运行,减少STW次数,在每次对象被引用时记录变化,减少重新标记时工作量
    • 辅助GC:防止内存分配过快。
  • 6.回收白色对象

什么时候触发STW

  • 初始标记(Initial Mark):
    • 时机:在标记阶段的开始,GC需要暂停所有goroutine,以标记从根对象(例如全局变量、栈上的局部变量等)可以直接访问到的所有对象。
    • 目的:确保从根对象开始的标记过程的准确性。这一阶段通常会非常短暂,因为只是标记直接可达的对象。
  • 重新标记(Re-mark):
    • 时机:在并发标记阶段结束之后,GC会再次触发STW暂停,以确保在并发标记阶段期间新创建或被修改的对象也能被正确标记。
    • 目的:确保所有存活对象都被正确标记,包括在并发标记阶段期间发生变动的对象。这是为了保证最终标记结果的完整性和准确性。
  • 抢占式GC暂停:
    • 时机:在GC运行过程中,如果发现某些goroutine长时间占用资源,导致GC无法及时完成必要的操作,GC可能会强制暂停这些goroutine。
    • 目的:确保垃圾回收过程能够顺利进行,避免因为某些长时间运行的goroutine导致回收过程被延迟。
  • 调节内存分配压力:
    • 时机:在内存分配压力较大时,GC可能会增加STW暂停的频率,以便更频繁地进行垃圾回收,防止内存耗尽。
    • 目的:保证程序在高内存压力下仍然能够正常运行,避免因为内存不足导致程序崩溃。

GC调优

  • 控制内存分配速度,限制goroutine数量
  • 少量使用+连接string
  • slice提前申请内存,防止频繁扩容
  • 避免map key对象过多,增加扫描时间
  • 变量复用,避免重复分配
  • 降低GC频率
全部评论

相关推荐

10-28 14:42
门头沟学院 Java
watermelon1124:因为嵌入式炸了
点赞 评论 收藏
分享
点赞 2 评论
分享
牛客网
牛客企业服务