前言
本文通过阅读学习《GO 专家编程》《GO 语言学习笔记》内存管理相关章节,进行总结梳理
提示:以下是本篇文章正文内容,下面案例可供参考
一、内存分配
为了方便自主管理内存, 一般做法是先向系统申请一块内存, 然后将内存切割成小块, 通过一定的内存分配算法管理内存。预申请的内存划分为 spans, bitmap, arena 三部分。 其中arena 是堆区,应用中需要的内存从这里分配, spans 和 bitmap 是为了管理 arena 区而存在的
arena:arena 的大小为 512 GB, 为了方便管理, 把arena区域划分成一个个的page(页),每个页大小为8KB, 一共 512GB / 8KB 个页
spans:存放span的指针,每个指针对应一个或多个page(1个或多个连续页), span 区域大小为 (512GB/8KB)* 指针大小(8byte) = 512 MB
span: 为了满足小对象分配, 会将 span 中的一页划分为更小的粒度。span之间以链表的方式链接起来。
class: 面向对象分配, 将span按特定大小分成多个小块, 每个小块可存储一个对象(一个span 有多个对象存储器)。
bitmap:位图, 区域大小通过arena计算得出, 主要用于 保存指针, GC标记等信息
管理span
数据结构 mcentral 用于管理span, 各线程需要内存时从 mcentral 管理的 span 中申请内存, 为了避免多线程申请内存时不断加锁, GO 为每个线程分配span的缓存(cache) , 用于无锁分配。
假如cache1获取一个span后,仅使用了一部分object,那么剩余空间就可能会被浪费。而回收操作将该span交还给central后,该span完全可以被cache2、cacheN获取使用。此时,cache1已不再持有该span,完全不会造成问题 (多个cache 间 提高object 利用率)。
heap 管理闲置 span,需要时向操作系统申请新内存。span大小并非固定不变。在获取闲置span时,如果没找到大小合适的,那就返回页数更多的,此时会引发裁剪操作,多余部分将构成新的span被放回管理数组。分配器还会尝试将地址相邻的空闲span合并,以构建更大的内存块,减少碎片,提供更灵活的分配策略
内存分配流程
计算待分配对象对应的规格(size class)。
从cache.alloc数组找到规格相同的span。
从span.freelist链表提取可用object(存储对象用, 有不同规格,需要找到合适的规格)。如span.freelist为空,从central(工作线程 cache)获取新span。
如central.nonempty为空,从heap.free/freelarge获取,并切分成object链表。
如heap没有大小合适的闲置span,向操作系统申请新内存块。
二、内存回收
之所以说回收而非释放,是因为整个内存分配器的核心是内存复用,不再使用的内存会被放回合适位置,等下次分配时再次使用。只有当空闲内存资源过多时,才会考虑释放。
垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到), 哪些内存不再使用了(即未被引用),把未被引用的内存回收,以供后续内存分配时使用
三色标记的三色对应了垃圾回收过程中对象的三种状态
灰色: 对象还在标记队列中等待
黑色: 对象已被标记, 说明对象有被引用到,不会在本次GC中被处理
白色: 对象未被标记, 初始时为白色, 经过一轮检测后还是白色的话,说明没被引用到, 会在本次GC中被清理
内存释放流程
将标记(三色标记, 三种状态, 未标记, 已标记, 待标记)为可回收的object交还给所属span.freelist。
该span被放回central,可供任意cache重新获取使用。
如span已收回全部object,则将其交还给heap,以便重新切分复用。
定期扫描heap里长时间闲置的span,释放其占用的内存。
(大对象直接从 heap 分配 和 回收)
STW
GO中的 STW(Stop The World)就是停止所有的 goroutine, 专心做垃圾回收, 待垃圾回收结束后再恢复 goroutine(STW 时间的长短直接影响了应用的执行)
垃圾回收优化
1 写屏障
优化STW机制, 让goroutine 与 GC 同时运行。写屏障类似一种开关, 在GC的特定时机开启, 开启后指针传递时会标记指针,即本轮不回收, 下次GC时再确定
2 辅助GC
为了防止内存分配过快, 在GC执行过程中, 如果goroutine 需要分配内存, 那么该 goroutine 会参与一部分GC的工作,即帮助GC做一部分工作。
垃圾回收的触发时机
(1)内存分配量达到阀值时触发GC
阀值 = 上次GC 内存分配量 * 内存增长率(GOGC 控制)
(2) 定期触发GC( src/runtime/proc.go : forcegcperiod 变量中声明)
(3) 手动触发(runtime.GC())
逃逸分析
逃逸分析 是由编译器决定内存分配的位置,在函数中申请一个新的对象:
如果分配在栈中, 则函数执行结束后可自动将内存回收
如果分配在堆中, 则函数执行结束后可交给GC(垃圾回收)处理
1. 逃逸策略
在函数中申请新的对象时, 编译器会根据该对象是否被函数外部引用来决定是否逃逸
如果函数外部没有引用, 则优先放到栈中
如果函数外部存在引用, 则必定放到栈中
2. 逃逸场景
(1) 指针逃逸
当GO函数返回局部变量指针, 局部变量指向其指向的内存地址不会是栈而是堆
(2) 栈空间不足逃逸
当栈空间不足以存放当前对象或无法判断当前切片长度时会将对象分配到堆中(比如创建一个长度为10000 的切片)
(3) 动态类型逃逸
很多函数的参数为 interface 类型, 比如 fmt.Println( a ...interface{}),编译期间很难确定其参数的具体类型, 也会产生逃逸
(4) 闭包引用对象逃逸
总结
GC 性能优化思路之一就是减少对象分配的个数, 比如对象复用或使用大对象组合多个小对象
栈上分配内存比在堆中分配内存有更高的效率
栈上分配的内存不需要GC处理
堆上分配的内存使用完毕会交给GC处理
逃逸分析的目的是决定分配地址是栈还是堆
逃逸分析在编译阶段完成
————————————————
版权声明:本文为CSDN博主「麦满分无敌」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37181864/article/details/134211461