这是我参与「第五届青训营」伴学笔记创作活动的第 4 天
零、序言
本文记录和整理了本人在字节青训营中学习的一些所得所想,用于本人回顾和梳理相关知识点,也欢迎大家参考,一同学习。如果发现有问题或者错误,可以在下方留言或者私信我(^-^)
一、自动内存管理
基本概念
自动内存管理:由程序语言的运行时系统管理动态内存,避免手动内存管理,专注于实现业务逻辑。需要保证内存使用的正确性和安全性(double-free problem, use-after-free problem)。
GC 的三个任务
- 为新对象分配空间
- 找到存活对象
- 回收死亡对象的内存空间
GC 相关的线程
Mutator: 业务线程,分配新对象,修改对象指向关系
Collector: GC 线程,找到存活对象,回收死亡对象的内存空间
GC 模型
Serial GC: 只有一个 collector
Parallel GC: 并行 GC,支持多个 collectors 同时回收的 GC 算法
Concurrent GC: 并发 GC,支持 mutator(s) 和 collector(s) 同时执行的 GC 算法。
并发GC实现时存在难点,Collectors 必须感知对象指向关系的改变!
如下图,在GC过程中,Collector 会标记当前存活的对象,目前o、a都已经被标记了。但此时Mutator依然在执行,它新分配了一个对象b,那么Collector也应该标记对象b。如果没有标记上,那对象b就视为死亡对象了,明显不符合GC算法的基本要求——不回收存活的对象。
GC 算法评价指标
- 安全性(Safety):不能回收存活的对象 基本要求
- 吞吐率(Througput):
花在GC上的时间
- 暂停时间(Pause time):Stop the World (STW)serial GC和parrelGC都存在暂停时间,业务是否感知
- 内存开销(Space overhead):GC会开辟一些内存空间来协作完成GC操作,GC元数据开销
相关技术
常见的两种垃圾回收相关技术
- 追踪垃圾回收(Tracing garbage collection)
- 引用计数(Refence counting)
追踪垃圾回收
Tracing garbage collection: 追踪垃圾回收,用于回收不可达对象。
追踪垃圾回收过程如下:
- 标记根对象(GC roots):静态变量、全局变量、常量、线程栈等
- 标记:找到所有可达对象,求指针指向关系的传递闭包:从根对象出发,找到所有可达对象
- 清理:回收所有不可达对象占据的内存空间
垃圾回收策略:根据对象的生命周期,使用不同的标记和清理策略。
GC算法
Copying GC(复制): 将存活对象从一块内存空间复制到另外一块内存空间,原先的空间可以直接进行对象分配
Mark-sweep GC(标记-清除): 将死亡对象所在内存块标记为可分配,使用 free list 管理可分配的空间
Mark-compact GC(标记-整理): 将存活对象复制到同一块内存区域的开头,原地整理对象
Generational GC(分代):分代 GC 假定每个对象都有年龄,即经历过GC的次数。不同年龄的对象处于heap的不同区域。针对年轻和老年的对象,制定不同的GC策略,降低整体内存管理的开销。分代假说,most objects die young,很多对象在分配出来后就不在使用了。
Young generation: 年轻代
- 常规的对象分配
- 由于存活对象很少,可以采用 copy collection
- 好处:GC 吞吐率很高
Old generation: 老年代
- 对象趋向于一直活着,反复复制开销较大
- 可以采用 mark-sweep collection
引用计数
每个对象都有一个与之关联的引用数目,对象存活的条件:当且仅当引用数大于 0。
优点:
- 内存管理的操作被平摊到程序运行中:指针传递的过程中进行引用计数的增减。
- 不需要了解 runtime 的细节:因为不需要标记 GC roots,因此不需要知道哪里是全局变量、线程栈等。
缺点
- 开销大,因为对象可能会被多线程访问,对引用计数的修改需要原子操作保证原子性和可见性。
- 无法回收环形数据结构——解决方法:weak reference。
- 内存开销:每个对象都引入额外存储空间存储引用计数。
- 虽然引用计数的操作被平摊到程序运行过程中,但是回收大的数据结构依然可能引发暂停。
引用参考
推荐书籍《The GARBAGE CONLLECTION HANDBOOK》