这是我参与「第三届青训营 -后端场」笔记创作活动的第1篇笔记
1.自动内存管理/垃圾回收
三个任务
- 为新对象分配内存
- 找到存活的对象
- 回收死亡对象的内存空间 相关概念(GC也就是Collector,垃圾回收器)
- 业务线程,也是用户线程
- GC线程,负责垃圾回收的线程
- Serial GC:只有一个GC线程,GC线程和用户线程串行化执行
- Parallel GC:多个GC线程同时回收垃圾
- Concurrent GC:用户线程和GC线程可以同时执行。这要求GC线程能够感知对象指向关系的变化。
GC算法的评价指标
- 安全性:不能够回收存活的对象
- 吞吐率:1-GC时间/程序执行总时间
- 暂停时间:STW,Stop The World
- 内存开销:GC元数据开销
追踪垃圾回收
- 对象被回收的条件:指针指向关系不可达的对象
- 标记根对象。包括静态变量,全局变量,常量,线程栈
- 找到可达对象。求指针传递关系的闭包,从根对象开始,找到所有可达的对象
- 清除不可达的对象。
- 将存活的对象复制到另外的内存空间。Copying GC,复制到另外的内存
- 将死亡对象的内存标记为可用。Mark-sweep GC,标记清除
- 移动并整理存活对象。Mark-compact GC,标记后,原地整理
分代GC
- 分代假说,大多数的对象都die young。很多对象刚分配就不使用了
- 每个对象的年龄:经历GC的次数
- 目的:对年轻和老年的对象,采用不同的GC策略,降低内存管理整体开销
- 不同年龄的对象放在不同heap区域
- 年轻代:常规的对象分配,存活对象较少,可以采用copying GC,GC吞吐率高
- 老年代:对象趋于一致存活,反复复制的话开销比较大,可以采用mark-sweep GC 引用计数
- 每个对象都有一个与之相关联的计数,记录了引用该对象的指针或者引用数目
- 对象存活条件:引用计数大于0
- 优点:内存管理操作被平摊在程序执行过程中,不需要了解runtime细节,比如c++的智能指针。
- 缺点:维护引用计数开销大,要通过原子操作保证对引用计数的原子性和可见性。循环引用问题,但是可以通过弱引用解决。内存开销,需要引入额外的空间存储引用数目。回收内存依然可能引发暂停。
2. 内存分配
分块
- 调用系统条用mmap()从os获取一块内存,例如4MB
- 先将内存分成大块,mspan,8kb
- mspan一共有67类,每一类mspan能够存储的对象大小不同。
- noscan mspan,GC不需要扫描的mspan,分配的对象不包含指针
- scan mspan,GC需要扫描的mspan,分配的对象包含指针
缓存
- 每个P包含一个mcache用于为P上绑定的G快速分配内存
- mcache管理67*2个mspan链表,每个类型的mspan有两个链表,scan和noscan
- 当mcache中内存不够用时,向mcentral申请mspan。
- mcentral管理一类mspan。也就是说一共67*2个mcentral,归mheap管理。
go中内存分配优化
- 内存分配比较耗时,分配路径长
- 小对象占比高
- 对象分配是非常高频的操作,每秒分配GB级别
3. 编译
词法分析,语法分析,语义分析,中间表示,代码优化,代码生成
4. 内联函数
将被调用函数在调用位置展开,同时重写代码以反应参数的绑定 优点
- 消除函数调用开销,比如参数传递,保存寄存器
- 将过程间分析转换为过程内分析,帮助其他优化,比如逃逸分析。 缺点
- 函数体变大,指令缓存不友好
- 编译生成的go镜像变大
5. 逃逸分析
- 分析代码中指针的动态作用域:指针在何处被访问
- 思路
- 从对象分配处出发,沿着控制流,观察对象的数据流
- 若返现指针p在当前作用于s:
-
作为函数传递给其他变量 -
传递给全局变量 -
传递给其他携程 -
3. 则指针p逃逸出s传递个已逃逸的指针指向的对象时
- 优化。未逃逸的指针在栈上分配,而栈上分配和回收非常快,移动栈顶指针即可,还减少了GC的负担。可以使用内联函数优化。