编程语言的内存分配器一般包含两种分配方法,一种是线性分配器(Sequential Allocator,Bump Allocator),另一种是空闲链表分配器(Free-List Allocator)
线性分配器
只需要在内存中维护一个指向内存特定位置的指针,指针前面的部分表示已经分配的内存区域,指针后面部分是未分配区域,每次申请内存,只要移动位置指针即可。
缺点:无法在内存释放时重用内存,它们可以通过拷贝的方式整理存活对象的碎片,将空闲内存定期合并,这样就能利用线性分配器的效率提升内存分配器的性能了。
空闲链表分配器
将内存分割成由 4、8、16、32 字节的内存块组成的链表,申请不同的内存大小时,到适合的链表上申请内存即可。
Go的内存分配
TCMalloc是Thread Cache Malloc的简称,是Go内存管理的起源,Go的内存管理是借鉴了TCMalloc。
同一进程的所有线程共享相同的内存空间,他们申请内存时需要加锁,如果不加锁就存在同一块内存被2个线程同时访问的问题。
TCMalloc的做法是什么呢?为每个线程预分配一块缓存,线程申请小内存时,可以从缓存分配内存,这样有2个好处:
1、为线程预分配缓存需要进行1次系统调用,后续线程申请小内存时,从缓存分配,都是在用户态执行,没有系统调用,缩短了内存总体的分配和释放时间,这是快速分配内存的第二个层次。
2、多个线程同时申请小内存时,从各自的缓存分配,访问的是不同的地址空间,无需加锁,把内存并发访问的粒度进一步降低了,这是快速分配内存的第三个层次。
Thread Cache: 每个线程各自的Cache,一个Cache包含多个空闲内存块链表,不同的链表内存块大小也不同,程序根据需要到对应的链表申请内存。
垃圾回收
所有的 GC 算法其存在形式可以归结为追踪(Tracing)和引用计数(Reference Counting)这两种形式的混合运用。
- 追踪式 GC
- 从根对象出发,根据对象之间的引用信息,一步步推进直到扫描完毕整个堆并确定需要保留的对象,从而回收所有可回收的对象。Go、 Java、V8 对 JavaScript 的实现等均为追踪式 GC。
- 引用计数式 GC
- 每个对象自身包含一个被引用的计数器,当计数器归零时自动得到回收。因为此方法缺陷较多,在追求高性能时通常不被应用。Python、Objective-C 等均为引用计数式 GC。
根对象到底是什么?
根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
- 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
追踪式GC的过程就是mark+sweep
mark+sweep->work….->make+sweep…
标记mark:从根对象开始通过DFS查询到所有引用的对象,标记为有颜色(说明这些对象都是有用的)
清除sweep:回收未被标记的垃圾对象并将回收的内存加入空闲链表;
在mark+sweep过程中,用户进程不能执行,称为stop the word.造成程序卡顿。为了缩短stw的时间,把mark拆分出来
mark->work->mark->work->mark+sweep->work..
mark一小部分,再切换到用户进程work,一点点的mark.一直到灰色对象都处理完成。mark阶段结束,执行sweep阶段。
拆分的mark: 根对象开始查找所有的引用对象,标记为黑色,把遍历到的最外层的对象标记为灰色(表示下次mark从那里开始),其他的对象就是白色对象。
从垃圾回收器的视角来看,三色抽象规定了三种不同类型的对象,并用不同的颜色相称:
- 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。
- 灰色对象(黑色与白色中间):已被回收器访问到的对象,但是因为mark中断,还未完成
- 黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。
当三色的标记清除的标记阶段结束之后,应用程序的堆中就不存在任何的灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾.
由于加入了第三种颜色来记录mark过程,所以mark阶段可以拆分并且可以和用户进程并发的执行。大大缩短了STW的时间。