前言——性能优化
什么是性能优化?
提升软件系统处理能力,减少不必要的消耗,充分发掘计算机算力。
为什么要性能优化?
提升用户体验
资源高效利用:降低成本,提高效率——很小的优化乘以海量机器会显著提升性能和成本节约
性能优化的层面:业务层优化和语言运行时层面优化 业务代码 SDK 基础库 语言运行时:GC、调度器等 OS
自动内存管理
1.概念
中英文对照:
Auto memory management: 自动内存管理
Grabage collction: 垃圾回收
Mutator: 业务线程
Collector: GC 线程
Concurrent GC: 并发 GC
Parallel GC: 并行 GC
Tracing garbage collection: 追踪垃圾回收
Copying GC: 复制对象 GC
Mark-sweep GC: 标记-清理 GC
Mark-compact GC: 标记-压缩 GC
Reference counting: 引用计数
Generational GC: 分代 GC
Young generation: 年轻代
Old generation: 老年代
2.内存泄露(Memory leak)
内存泄露是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存泄露会导致内存溢出,无法继续申请内存空间,内存已经占满了。
3.自动内存管理(Auto memory management)
动态内存:程序在运行时根据需求动态分配的内存
自动内存管理(垃圾回收):由程序语言的运行时系统管理动态内存
可有效避免手动内存管理,使我们在编写代码时专注于实现业务逻辑
保证内存使用的正确性和安全性:duble-free problem,use-after-free problem
三个任务:为新对象分配空间
找到存活对象
回收死亡对象的内存空间
4.Go中自动内存管理
Mutator:业务线程,分配新对象,修改对象指向关系
Collector:GC 线程,找到存活对象,回收死亡对象的内存空间
Serial GC:只有一个collector
Parallel GC:支持多个collectors同时回收的GC算法
Concurrent GC: mutator(s)和collector(s)可以同时执行
5.两种常用的GC算法
追踪垃圾回收(Tracing garbage collection)
引用计数(Reference counting)
2.追踪垃圾回收(Tracing garbage collection)
2.1算法思想
对象被回收的条件:指针指向关系不可达的对象
判断一个对象是否可达,因为一旦这个对象不可达就可以立刻被 GC 回收了。那么我们怎么判断一个对象是否可达呢?
第一步,标记根对象,即找出所有的全局变量和当前函数栈里的变量,常量等,标记为可达;
第二步,从已经标记的数据开始,进一步标记它们可访问的变量,以此类推,专业术语叫传递闭包。
清理所有不可达对象:三种方式,根据对象的生命周期,使用不同的标记和清理策略
1.标记-复制——将存活对象复制到另外的内存空间(Copying GC)
2.标记-清除——将死亡对象的内存标记为可分配(Mark-sweep GC):使用free-list管理空闲内存
3.标记-整理——移动并整理存活对象(Mark-compact GC):原地整理对象
2.2分代GC(Generational GC)
追踪式垃圾收集算法在少量垃圾回收的时候效率非常高效,特别是复制回收算法。但是长寿对象的存在会影响到回收回收的效率,这个时候就通过分区,使长寿的数据都堆积在一边,这样对年轻的数据使用复制回收算法就可以大大提升效率。
分代假说(Generational hypothesis):most objects die young
弱分代假说:大多数对象都是朝生夕灭的。
强分代假说:越长寿的对象越不容易死亡。
设计原则:收集器将堆划分出不同区域,然后将回收对象依据年龄分配到不同的区域之中。对象年龄指的是其经历过GC的次数。
不同年龄的对象应有不同的GC策略:
年轻代(Young generation):常规的对象分配;由于存活对象很少,可以采用标记-复制策略;GC吞吐率很高
老年代(Old generation):对象趋向于一直活着,反复复制开销较大;可以采用标记-清除策略
2.3三色标记法(tricolor mark-and-sweep algorithm)
Golang使用的垃圾回收算法是三色标记法,是传统标记-清除的一个改进,是一个并发的GC算法。
原理如下:
第一步,创建白、灰、黑三个集合;
第二步,将所有对象放入白色集合中;
第三步,从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合(备注:这里放入灰色集合的都是根节点的对象);
第四步,遍历灰色集合,将灰色对象引用的对象(备注:这里指的是灰色对象引用到的所有对象,包括灰色节点间接引用的那些对象)从白色集合放入灰色集合,然后将分析过的灰色对象放入黑色集合,直到灰色中无任何对象;
第五步,通过写屏障(write-barrier)检测对象有变化,重复以上操作(备注:因为 mark 和用户程序是并行的,所以在上一步执行的时候可能会有新的对象分配,写屏障是为了解决这个问题引入的);
第六步,收集所有白色对象(垃圾)。
缺点:可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。
3.引用计数(Reference counting)
每个对象都有一个与之关联的引用数目。
对象的存活条件:当且仅当引用数大于0
优点:内存管理的操作被平摊到程序执行过程中
内存管理不需要了解runtime的实现细节
缺点:维护引用计数的开销较大:通过原子操作保证对引用计数操作的原子性和可见性
无法回收环形数据结构——weak reference
内存开销:每个对象都引入的额外内存空间存储引用数目
回收内存时依然可能引发暂停
4.Go内存管理
4.1分块
目标:为对象在heap上分配内存
提前将内存分块:调用系统调用mmap(),向OS申请一大块内存;将其划分成大块,称作mspan,再将大块分成特定大小的小块,用于对象分配,根据对象的大小,选择最合适的块返回。小块有两种类型,
noscan mspan:分配不含指针的对象,GC不需要扫描
scan mspan:分配含指针的对象,GC需要扫描
4.2缓存
GMP模型中,每个p包含一个mcache用于快速分配,用于为绑定于p上的G分配对象;mcache管理一组mspan
4.3优化方案(字节):Balanced GC
每个G都绑定一大块内存(1KB),称作 goroutine allocation buffer(GAB)
GAB用于noscan类型的小对象分配:<128 B
GAB对于Go内存管理来说是一个对象。
本质:将多个小对象的分配合并成一次大对象的分配
4.4逃逸分析
分析代码中指针的动态作用域:指针在何处可以被访问
大致思路:从对象分配处出发,沿着控制流,观察对象的数据流,若发现指针 p 在当前作用域 s 出现一下情况:
作为参数传递给其他函数
传递给全局变量
传递给其他的goroutine
传递给已逃逸的指针指向的对象
则指针p指向的对象逃逸出 s ,反之则没有逃逸出 s 。
未逃逸的对象在栈上分配。对象在栈上的分配和回收很快:移动sp;可以减少在heap上的分配,降低GC负担。
原文:https://juejin.cn/post/7097609603461939231