这是我参与「第五届青训营」笔记创作活动的第3天
来自前段时间读Go语言专家编程的读书笔记
内存分配管理
申请的内存会被划分成三个部分
arena:即堆区,应用需要的内存也是从这里分配出去的,这个区域会划分成 page页,每个页8KBspans:存放span指针的区域,每个指针又对应一个或多个pagebitmap:和GC标志有关
span:用来管理arena中page页的,包含一个或多个连续页,页又会根据需要划分出更小的粒度class,每个span管理特定的大小的class对象`type mspan struct { next *mspan // 链表前向指针 prev *mspan // 链表后向指针 list *mSpanList // For debugging. TODO: Remove. startAddr uintptr // 管理的页开始地址 npages uintptr // 页数 manualFreeList gclinkptr // list of free objects in mSpanManual spans nelems uintptr // 块个数 allocBits *gcBits // 每一位代表一个块是否被分配 gcmarkBits *gcBits // 每块的gc标记情况 allocCache uint64 sweepgen uint32 allocCount uint16 // 已被分配的块数 spanclass spanClass // size class and noscan (uint8) state mSpanStateBox // needzero uint8 // needs to be zeroed before allocation allocCountBeforeCache uint16 // elemsize uintptr // 块大小 limit uintptr // speciallock mutex // guards specials list specials *special // }
cache:为了避免多线程申请内存时频繁加锁,为每个线程分配了span的缓存数组中每个元素代表一种class类型的span列表,列表又有两种,一种包含了指针,一种不包含指针,目的是为了方便GC扫描,没指针对象的就没必要去扫描他
type mcache struct { alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass stackcache [_NumStackOrders]stackfreelist }
central:用来管理span,这么说吧,cache是为了单个线程提供服务缓存,central则是为了多线程服务。
当某个线程内存不足时向central申请,当线程释放又回收到central,cache初始时是没有span的,过程中动态的从central获取并缓存下来
申请span过程:加锁、从空闲表中取span并从链表中删除加入到非空闲链表中,span返回给线程,解锁、线程将span缓存到cache释放span过程:加锁、从非空闲链表删除加到空闲链表,解锁type mcentral struct { spanclass spanClass partial [2]spanSet // 还有空闲块的span列表 full [2]spanSet // 没有空闲块的span列表 }
heap:知道了central管理的某一种大小的class类型的span,这些central又存放在 heap中,Go也就是通过一个heap管理内存type mheap struct { lock mutex allspans []*mspan // all spans out there curArena struct { // 当前arena区域的起始和已使用的地址 base, end uintptr } central [numSpanClasses]struct { // 每种类型class对应的central mcentral mcentral pad [cpu.CacheLinePadSize -unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte } }
总结就是下面这张图
垃圾回收
常见的垃圾回收算法:
引用计数:每个对象维护一个计数,引用该对象的对象被销毁则减1,直到0就回收对象
- 好处就是能够很快的发现并回收,不会等到内存耗尽才回收,缺点是不能很好处理循环引用,实时维护计数也要代价
分代收集:按照对象生命周期长短划分不同的代空间,也就是老年代、新生代,不同代有不同的回收算法和频率
- 好处就是回收性能好,但是算法复杂
标记清除:从根变量开始遍历引用的对象,引用的标记为“被引用”,没有的就会被回收
- 好处是避免了引用计数的循环引用问题,但是需要STW(停止所有协程,专心做垃圾回收)
三色标记:
- 白色:对象未标记(gcmarkBits 对应位 0)
- 灰色:对象在标记队列中等待
- 黑色:对象被标记(gcmarkBits 对应位 1)
使用混合写屏障优化减弱STW
逃逸分析
- 如果分配在栈中,函数执行结束自动内存回收
- 如果分配在堆中,函数执行结束可交给GC处理
编译器根据对象是否被函数外部引用来决定是否逃逸:
- 没被引用,优先放入栈中
- 被引用,一定是放到堆中
还有当内存过大超过栈存储能力时,也会放到堆中
- 动态类型逃逸
func main(){ s := "test" fmt.Println(s) // Println(a...interface{}),编译期间很难确定参数具体类型,也会产生逃逸 }
- 闭包引用
func fibonaci() func() int { a,b := 0,0 return func() int { a,b = b,a+b return a } // 闭包引用了局部变量a、b,所以被放入到堆中 }
Go编译器优化
函数内联
定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
优点 - 消除调用开销 - 将过程间分析的问题转换为过程内分析,帮助其他分析
缺点 - 函数体变大 - 编译生成的 Go 镜像文件变大
采取一定的策略决定是否内联 - 调用和被调用函数的规模
Go 内联的限制 - 语言特性:interface, defer 等等,限制了内联优化 - 内联策略非常保守
开销 - Go 镜像大小略有增加 - 编译时间增加 - 运行时栈扩展开销增加