高并发下的内存模型与垃圾回收

高并发下的内存模型与垃圾回收

Go内存模型

在这里插入图片描述

为什么说Go的栈在堆上

在这里插入图片描述

Go协程栈的位置

  • Go协程栈位于Go堆内存上。
  • Go堆内存位于操作系统虚拟内存上。
    在这里插入图片描述

Go使用参数拷贝传递(值传递)

  • 传递结构体时:会拷贝结构体中的全部内容。
  • 传递结构体指针时:会拷贝结构体指针。

总结

  • 协程栈记录了协程的执行现场。
  • 协程栈还负责记录局部变量,传递参数和返回值。
  • Go使用参数拷贝传递。

协程栈不够用了怎么办

局部变量太大导致协程栈不够用

逃逸分析

  • 不是所有的变量都能放在协程栈上。
  • 栈帧回收后,需要继续使用的变量。
  • 太大的变量。

指针逃逸

  • 函数返回了对象的指针。

空接口逃逸

  • 如果函数参数为interface{}。
  • 函数的实参很可能会逃逸。
  • 因为interface{}类型的函数往往会使用反射。

大变量逃逸

  • 过大的变量会导致栈空间不足
  • 64位机器中,一般超过64KB的变量会逃逸。

栈扩容

  • Go栈的初始空间为2KB。
  • 在函数调用前判断栈空间(morestack)
  • 必要时对栈进行扩容。

连续栈方式扩容(1.14版本开始)

  • 优点:空间一直连续
  • 缺点:伸缩时的开销大。
  • 当空间不足时扩容,变为原来的2倍。
  • 当空间使用率不足1/4时缩容,变为原谅的1/2.

Go的堆内存结构是怎样的?

heapArena

  • Go每次申请的虚拟内存单元为64MB。
  • 最多有4194304个虚拟内存单元(2的20次方)
  • 内存单元也叫heapArena
  • 所有的heapArena组成了mheap(Go堆内存)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • 线性分配或者链表分配容易出现空间碎片。

Go分配heapArena的方式——分级分配

在这里插入图片描述

内存管理单元mspan

  • 根据隔离适应策略,使用内存时的最小单位为mspan。
  • 每个mspan为N个相同大小的“格子”
  • Go中一共有67种mspan。
    在这里插入图片描述

每个heapArena中的span都不确定,那如何快速找到所需的mspan级别?

中心索引 mcentral

  • 136个mcentral结构体,其中
  • 68个组需要GC扫描的mspan
  • 68个组不需要GC扫描的mspan

mcentral的性能问题

  • mcentral实际是中心索引,使用互斥锁保护。
  • 在高并发场景下,锁冲突问题严重。
  • 参考GMP模型,增加线程本地缓存。

线程缓存 mcache

  • 每个P拥有一个mcache。
  • 一个mcache拥有136个mspan,其中
  • 68个需要GC扫描的mspan
  • 68个不需要GC扫描的mspan

总结

  • Go模仿TCmalloc,建立了自己的堆内存架构。
  • 使用heapArena向操作系统申请内存。
  • 使用heapArena时,以mspan为单位,防止碎片化。
  • mcentral是mspan们的中心索引(存在锁竞争问题)。
  • 所以引入了mcache,mcache记录了分配给各个P的本地mspan。

Go是如何分配堆内存的

在这里插入图片描述

  • 微小对象分配至普通mspan。
  • 大对象量身定做mspan。

微对象分配

  • 从mcache拿到2级mspan
  • 将多个微对象合并成一个16Byte存入

mcache的分配

  • mcache中,每个级别的mspan只有一个。
  • 当mspan满了之后,会从mcentral中换一个新的。

mcentral的扩容

  • mcentral中,只有有限数量的mspan。
  • 当mspan缺少时,会从heapArena开辟新的mspan。

大对象分配

  • 直接从heapArena开辟0级的mspan。
  • 0级的mspan为大对象定制。

heapArena的扩充

  • 当heapArena空间不足时。
  • 向操作系统申请新的heapArena。

总结

  • Go将对象按照大小分为3种。
  • tiny对象和small对象使用mcache。
  • mcache中的mspan填满后,与mcentral交换新的。
  • mcentral不足时,在heapArena开辟新的mspan。
  • 大对象之间在heapArena开辟新的mspan。

什么样的对象需要垃圾回收

垃圾回收(Garbage Collection)思路

  • Go因为堆内存结构的独特优势,选择最简单的标记-清除算法。
  • 找到有引用的对象,剩下的就是没有引用的。
    那么就引出了下一个问题:

从哪开始找引用的对象,即GC的起点?

  • 被栈上的指针引用
  • 被全局变量指针引用
  • 被寄存器中的指针引用
  • 上述变量被称为Root Set(GC Root)
    在这里插入图片描述
  • 这种方法也叫可达性分析标记法

并发垃圾回收

  • 并发的难点在于标记阶段

三色标记法

  • 黑色:黑色对象本身是有用的,且该对象指向的对象已经被分析扫描
  • 灰色:灰色对象本身是有用的,但是该对象指向的对象还未被分析扫描。
  • 白色:还未扫描到的对象或者被扫描过确认是无用的对象。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

三色标记法是为了解决并发标记中存在的问题

并发标记问题一:删除

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

yuasa删除屏障

  • 并发标记时
  • 对指针释放的白色对象置灰
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

并发标记问题二:插入

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Dijkstra插入屏障

  • 并发标记时
  • 对指针新指向的白色对象置灰。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

Go语言使用的是混合屏障

  • 被释放引用的堆对象标记为灰色
  • 被添加引用的堆对象标记为灰色
    在这里插入图片描述

如何优化GC效率

GC触发的时机

  • 系统定时触发
  • 用户显式触发
  • 申请内存时触发

系统定时触发

  • sysmon定时检查
  • 如果2分钟内没有过GC,触发。
  • 谨慎调整

用户显式触发

  • 用户调用runtime.GC 方法
  • 并不推荐调用

申请内存时触发

  • 给对象申请堆空间时,可能导致GC。

GC优化原则

  • 尽量少在堆上产生垃圾
  • 内存池化
  • 减少逃逸
  • 使用空结构体

内存池化

  • 有缓存性质的对象
  • 频繁创建和删除
  • 使用内存池,不GC。

减少逃逸

  • 逃逸会使原本在栈上的对象进入堆中。
  • 尽量少用fmt包,使用log包等
  • 返回了指针而不是拷贝,留心指针逃逸问题。

使用空结构体

  • 空结构体指向一个固定地址
  • 不占用堆空间
  • 比如Channel传递空结构体

GC分析工具

  • go tool pprof
  • go tool race
  • go build -gcflags=" -m"
  • GODEBUG=" gctrace=1"
/*
使用 $env:GODEBUG="gctrace=1" 查看gc情况
*/

func main() {
	wg := sync.WaitGroup{}
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func(wg *sync.WaitGroup) {
			var count int
			for i := 0; i < 1e10; i++ {
				count++
			}
			wg.Done()
		}(&wg)
	}
}
$env:GODEBUG="gctrace=1"
gc 1 @0.115s 0%: 0+1.6+0 ms clock, 0+0.55/2.9/0+0 ms cpu, 3->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
practice on main ≡  ~1 |  +1
➜ go run .\9.9.go
gc 1 @0.089s 0%: 0+1.0+0 ms clock, 0+0/0/1.0+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 2 @0.108s 0%: 0+0.63+0 ms clock, 0+0.63/0.63/0.63+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 3 @0.140s 0%: 0+0.54+0 ms clock, 0+0/1.0/0.54+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 4 @0.188s 0%: 0+0.52+0 ms clock, 0+0.52/0.52/0+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 5 @0.264s 0%: 0+0.55+0.53 ms clock, 0+0/1.6/1.1+8.5 ms cpu, 4->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 6 @0.273s 0%: 0+0.63+0 ms clock, 0+0/0.32/1.0+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 7 @0.396s 0%: 0.15+2.1+0 ms clock, 2.5+1.1/0/1.6+0 ms cpu, 4->4->0 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
# command-line-arguments
gc 1 @0.049s 2%: 1.0+4.0+0 ms clock, 16+1.0/4.0/6.0+0 ms cpu, 4->5->3 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
# command-line-arguments
gc 1 @0.012s 6%: 0.52+2.9+0 ms clock, 8.4+2.9/5.9/2.1+0 ms cpu, 4->6->5 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 2 @0.024s 3%: 0+11+0 ms clock, 0+0/2.1/12+0 ms cpu, 14->14->13 MB, 14 MB goal, 0 MB stacks, 0 MB globals, 16 P
gc 1 @0.116s 0%: 0+1.1+0 ms clock, 0+0.54/2.7/0+0 ms cpu, 3->4->1 MB, 4 MB goal, 0 MB stacks, 0 MB globals, 16 P

协程栈

  • 协程栈记录了协程的执行现场
  • Go协程栈位于Go堆内存上
  • Go使用参数拷贝传递函数的参数
  • 3种特殊情况下,变量可能会逃逸到堆上
    在这里插入图片描述
  • Go模仿TCmalloc,建立了自己的堆内存架构
  • 使用heapArena向操作系统申请内存
  • 使用heapArena时,以mspan为单位,防止碎片化。
  • mcentral是mspan们的中心索引
  • mcache记录了分配给各个P的本地mspan。

堆内存分配

  • Go将对象按照大小分为3种
  • 微对象和小对象使用mcache
  • mcache中的mspan填满后,与mcentral交换新的。
  • mcentral不足时,在heapArena开辟新的mspan
  • 大对象直接在heapArena开辟新的mspan。
    在这里插入图片描述

并发GC

  • 并发垃圾回收的关键在于标记安全
  • Go语言采用混合屏障机制兼顾了安全与效率

GC优化

  • GC主要由系统定时触发或者申请内存时触发,也可由程序员手动触发但不推荐这么做。
  • GC优化的原则是减少在堆上产生垃圾。
  • 使用GC分析工具可以帮助分析GC问题。
  • 18
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值