这是我参与「第三届青训营 -后端场」笔记创作活动的的第4篇笔记
优化
- 性能优化:提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力
- 为什么要做性能优化
- 用户体验:带来用户体验的提升
- 资源高效利用:降低成本,提升效率
- 优化 TODO:
- 测试用例要覆盖尽可能多的场景,方便回归
- 使用文档向用户说明优化做了什么,没做什么,能达到什么样的效果
- 通过选项控制是否开启优化保证隔离性
- 使用必要的日志输出保证优化的可观测性
- 数据驱动:
- 一定要依托于数据而非猜测
- 使用自动化性能分析工具
- 首先优化最大瓶颈
- 性能优化的层面:
- 业务代码
- 针对特定场景,做到具体问题具体分析
- 容易获得较大性能收益
- SDK(Commands + APIs + New APIs)
- 解决更通用的性能问题
- 考虑更多场景
- 使用 Tradeoffs
- 在保证接口稳定的前提下改进具体实现
- 基础库
- 语言运行时
- OS
- 业务代码
自动内存管理
概念
-
管理的是动态内存而不是静态内存
-
由程序语言的运行时系统管理动态内存,保证内存使用的正确性和安全性
- 例如避免了 C 语言中的 double free 和 use after free 问题
-
任务:
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
-
两种线程:
- Mutator:业务线程,分配新对象,修改对象指向关系
- Collector:GC 线程,找到存活对象并回收死亡对象的内存空间
- 必须能够感知对象指向关系的改变,避免部分对象标记错误
-
三种算法(区别在于停止的时候 GC 线程的个数以及是否可以同时执行):
- Serial GC:只有一个 Collector
- Parallel GC:支持多个 collectors 同时回收的 GC 算法
- Concurrent GC:mutator 和 collector 可以同时执行
-
评价 GC 算法
- 安全性(基本要求):不能回收存活的对象
- 吞吐率:,也即花在 GC 上的时间
- 暂停时间:业务是否感知到暂停了
- 内存开销:GC 元数据开销,额外开辟的空间大小
追踪垃圾回收
- 被回收的条件是指针指向了关系不可达的对象
- 步骤:
- 标记根对象
- 静态变量 + 全局变量 + 常量 + 线程栈
- 找到可达对象并标记
- 求指针指向关系的传递闭包
- 找到所有不可达对象并清理
- 标记根对象
- 三种清理方法:
- 将存活对象复制到另外的内存空间(copying GC)
- 将死亡对象的内存标记为可分配:下次分配空间的时候直接使用被标记为可分配的空间(mark-sweep GC)
- 移动并整理存活对象:将所有存活对象挪动到最前端(Mark-compact GC)
- 根据对象的生命周期,使用不同的标记和清理策略
分代 GC
-
most object die young
-
对于不同对象,经历过 GC 的次数就是她们的年龄,而对于年轻和老年的对象,通过制定不同的 GC 策略,降低了整体内存管理的开销
- 年轻代:
- 常规的对象分配
- 使用 copying collection
- GC 吞吐率较高
- 老年代:
- 对象趋向于一直活着,反复复制开销较大
- 采用 mark-sweep collection
- 年轻代:
-
不同年龄的对象处于 heap 的不同区域
引用计数
- 每个对象都有一个与之关联的引用数目
- 对象的存活条件是当且仅当引用数目大于 0
- 优点:
- 内存管理的操作被平摊到了程序执行的过程中
- 内存管理不需要了解 runtime 的实现细节
- 缺点:
- 由于需要通过原子操作保证对引用计数操作的原子性和可见性,导致了维护引用计数的开销较大
- 无法回收环形数据结构
- 每个对象都引入了额外的内存空间来存储引用数目,因此会造成一定的内存开销
- 尽管平摊到了程序执行过程,但是如果结点很多,回收内存的时候依然可能引发暂停
Go 内存管理及优化
- 目标:为对象在 heap 上分配内存
- 分块:
- 调用系统调用 mmap 向 OS 申请一大块内存
- 先将内存划分成大块,称为 mspan
- noscan mspan:分配不包含指针的对象(GC 不需要扫描)
- scan mspan:分配包含指针的对象(GC 需要扫描)
- 再将大块继续划分为特定大小的小块,用于对象分配
- 对象分配:根据对象的大小,选择最合适的块返回