Go 内存管理 | 青训营笔记
这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天,主要记录相关的知识点。
本堂课重点内容
- 自动内存管理
- Go内存管理
- Go编译器优化
性能优化
实际的性能优化,整体上可以从如下五个方向来考虑:
- 业务代码
- 基础库
- 语言运行时
对于实际的业务优化,我们可以针对业务场景,具体尝尽具体分析。
而语言运行时的优化,则需要考虑更多的场景,解决更通用的性能问题。
应该以 真实可靠 的数据来驱动优化代码,而不是猜测,可以使用辅助工具 ,优先解决最大瓶颈。
自动内存管理
程序在运行时常常会根据需求动态分配内存,比如 。而这些动态分配的内存如果不能够被很好的释放,那么就可能会导致严重的内存泄漏的问题。
而自动内存管理(垃圾回收,又称 )这一机制,就是一种通过程序语言的运行时系统来自动管理动态内存的机制,避免了程序员手动管理内存,使程序员只要关注业务逻辑,保证了程序的 安全性和正确性 。
的主要任务有三个:
- 为新对象分配内存空间
- 找到存活对象
- 回收死亡对象的内存空间
从 的角度看,可以将 中的线程分为两类:
- ,业务线程,分配新对象,修改对象的指向关系
- , 线程,找到存活对象,释放死亡对象的内存空间。
对于这两种线程的管理有三种算法:
- , 这种只有一个 ,当 工作时,会暂停 的工作
- ,可以支持多个 进行 , 工作时,会暂停 的工作
- , 可以支持多个 进行 , 可以支持 和 同时工作
三种算法的对比图:
对于 来说,其必须能够监控到对象的变化,也即:
对于一个 , 我们可以从四个角度来评价 :
- 安全性 , 不能回收存活对象,这是最基本的要求
- 吞吐量 , 程序运行 所需要的时间,
- 暂停时间 , 能否被业务所感知
- 内存开销 , 自身的元数据开销
下面是 最常用的两种技术
追踪垃圾回收(Tracing garbage collection)
这种技术会对指向了关系不可达的对象的指针或内存进行清理,一般可以分为三个步骤。
- 标记根对象,比如静态变量、全局变量、常量、线程栈等
- 标记,找到所有可达对象,即求出所有指针可达的传递闭包
- 清理,回收所有不可达对象的内存空间
对于清理,常用的策略有三种:
-
Copying GC: 将存活对象从一块内存空间复制到另外一块内存空间,原先的空间可以直接进行对象分配
-
-
Mark-sweep GC: 将死亡对象所在内存块标记为可分配,使用 free list 管理可分配的空间
-
-
Mark-compact GC: 将存活对象复制到同一块内存区域的开头
-
虽然后很多种策略,但是实际运行时环境是复杂的,需要灵活根据对象的不同生命周期使用不同的标记请清理策略,一个经典的例子是 , 其根据对象经历 的次数来给对象划分年龄,对于年轻的对象多使用 , 而对于老年对象多实用 。
引用计数(Reference counting)
引用计数方法就是对于每个对象都维护一个高度关联的引用数目,当引用计数为 时,进行内存释放。这也是 中新增的 所推荐的管理内存的方式。
优点:
- 这种方式使得程序员可以更多关注业务代码,而不需要了解实际运行时的更多细节
- 内存管理的操作被分摊在了程序实际运行中
缺点:
- 引用计数的维护是原子操作,开销较大
- 无法处理环形的结构
- 需要维护引用计数,带来额外的内存开销
- 回收内存时仍可能触发暂停
Go内存管理及其优化
Go内存分配
的内存分配,是通过程序运行时的预分配实现的。
会在程序运行前,提前将内存分块,具体步骤如下:
- 调用系统调用 向 申请一大块内存,例如
- 先将内存划分成大块,例如 ,称作
- 再将大块继续划分成特定大小的小块,用于对象分配
- : 分配不包含指针的对象 —— 不需要扫描
- : 分配包含指针的对象 —— 需要扫描
实际对象分配的过程,就是在这一堆小块里面查找一个大小合适的块并返回。
对这些内存会做多级缓存机制,对于从 分配得的内存,其被内存管理回收后,立刻归还给 ,而是在 内部先缓存起来,从而避免频繁向 申请内存。内存分配的路线图如下。
Go编译器优化
的编译器很多时候,会为了编译的效率,而放弃很多的优化,比如:函数内联等。
函数内联
函数内联会使得被调用的函数的副本复制到调用方,从而减少函数的调用次数,避免了函数调用、参数传递、栈的开销,还可以帮助分析其他的优化,比如逃逸分析。
但是函数内联也可能导致函数体变得很大,导致最终生成的 镜像过大的问题。
逃逸分析
逃逸分析的基本定义为:代码中指针的动态作用域,即指针在何处可以被访问。
分析逃逸分析的基本思路:
-
从对象分配处出发,沿着控制流,观察数据流。若发现指针 在当前作用域:
- 作为参数传递给其他函数
- 传递给全局变量
- 传递给其他的
- 传递给已逃逸的指针指向的对象
-
则指针 逃逸,反之则没有逃逸.
优化:未逃逸出当前函数的指针指向的对象可以在栈上分配
- 对象在栈上分配和回收很快:移动 即可完成内存的分配和回收;
- 减少在堆上分配对象,降低 负担。
个人总结
本次课程主要学习了:
- 自动内存管理
- Go内存管理
- Go编译器优化