常见的垃圾回收机制有两大种:
一 引用计数
每个对象维护一个引用计数器,当引用该对象的对象被销毁或者更新的时候,被引用对象的引用计数器自动减 1,当被应用的对象被创建,或者赋值给其他对象时,引用 +1,引用为 0 的时候回收
优点:实现简单;渐进式回收;回收及时;
缺点:循环引用;维护计数单元较为麻烦
二 跟踪回收
跟踪回收根据特点又可以分为三大类:
2.1 标记-清理
内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。垃圾回收程序对所有的存活单元进行一次全局遍历确定哪些单元可以回收。
算法分两个部分:标记(mark)和清扫(sweep)。标记阶段表明所有的存活单元,清扫阶段将垃圾单元回收。
(1)标记阶段:
在此阶段,垃圾回收器会从应用程序的根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识,于是这个对象就被标识为可到达对象。
(2)清理阶段:
在此阶段中,垃圾回收器,会对堆内存从头到尾进行线性遍历,如果发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作
如图所示,在标记阶段,从跟对象1可以访问到B,从B又可以访问到E,那么B和E都是可到达对象,同样的道理,F、G、J和K都是可到达对象。在回收阶段,所有未标记为可到达的对象都会被垃圾回收器回收。
优点:无循环引用问题;不用维护计数开销
缺点:需要STW,应用程序的执行会暂停;非渐进式需遍历整个堆空间,开销大
2.2 三色标记法(标记-清理法的改进-并发版本)
也是当前GO GC所使用的算法。
三色标记法(tricolor mark-and-sweep algorithm)是传统 Mark-Sweep 的一个改进,它是一个并发的 GC 算法,在Golang中被用作垃圾回收的算法,但是也会有一个缺陷,可能程序中的垃圾产生的速度会大于垃圾收集的速度,这样会导致程序中的垃圾越来越多无法被收集掉。原理如下所示:
step 1: 创建:白、灰、黑 三个集合。
step 2: 将所有对象放入白色集合中。
step 3: 从根节点开始遍历所有对象,把遍历到的对象从白色集合放入灰色集合(备注:这里放入灰色集合的都是根节点的对象)。
step 4: 遍历灰色集合,将灰色对象引用的对象(备注:这里指的是灰色对象引用到的所有对象,包括灰色节点间接引用的那些对象)从白色集合放入灰色集合,然后将分析过的灰色对象放入黑色集合。
step 5: 直到灰色中无任何对象。
step 6: 通过写屏障(write-barrier)检测对象有变化,重复以上操作(备注:因为 mark 和用户程序是并行的,所以在上一步执行的时候可能会有新的对象分配,写屏障是为了解决这个问题引入的)。
step 7: 收集所有白色对象(垃圾)
具体步骤:
1. 初始阶段,假设当前的对象调用情况如下所示,root ->A->B/A->C/A<->D;root->F; E; G->H;
根据算法,会将所有的对象都放到白色集合当中,对应于step 1和step 2。
2. GC开始扫描,这里会从根节点开始,遍历发现只有A和F是根节点,于是将A、F从白色集合移动到灰色集合当中,在白色结合中之后剩下B、C、D、E、G、H这些节点,对应于step 3
3. GC继续扫描灰色集合,会将灰色集合中的节点中引用的节点移动到灰色集合当中,本例中A节点引用的节点B、C、D会被移动到灰色集合中,紧接着A发现自己引用的所有子节点都已经在灰色集合了,便会被移动到黑色集合中,同时F节点没有自节点,也会被移动到黑色集合当中,对应于step 4
4. GC会循环遍历灰色集合,直到灰色集合之中没有节点为止,在本例中,发现B、C、D都没有子节点在白色集合中,便将B、C、D都移动到黑色集合中,对应于step 5
5. 此时只剩下E、G、H在白色集合中,剩下的对象都在黑色集合中,GC便清除白色集合中的对象,也就是进行回收这些对象,对应于step 7
6. 上面的垃圾回收结束之后,GC会在进行一步操作,也就是将黑色集合变色成白色集合,供下一次垃圾回收使用
go 语言在 1.3 以前,使用的是传统的 Mark-Sweep 算法。
1.3 版本进行了一下改进,把 Sweep 改为了并行操作。
1.5 版本进行了较大改进,使用了三色标记算法。go 1.5 在源码中的解释是“非分代的、非移动的、并发的、三色的标记清除垃圾收集器”
runtime.gcAssistAllocsync.Pool
在GO种具体GC流程:
(1)标记准备
打开WriteBarrier,监控新对象修改。需要短暂STW记录当前状态。三色标记法是一种可以并发执行的算法。所以在运行过程中程序的函数栈内可能会有新分配的对象,那么这些对象该怎么通知到 GC,怎么给他们着色呢?这个时候就需要 Write Barrier 出马了。Write Barrier 主要做这样一件事情,修改原先的写逻辑,然后在对象新增的同时给它着色,并且着色为”灰色“。因此打开了 Write Barrier 可以保证了三色标记法在并发下安全正确地运行。
(2)标记
三色标记法。可并行。这里根指的是全局变量和函数栈
(3)清除白色的点。
需要STW,可并行
GC的触发条件
毕竟GC还是需要STW的,虽然可能停止时间很短,但是对于程序来说,整个程序停止1秒那对于用户来说就是致命打击。所以GC肯定需要一个触发的条件。
GC百分比这是一个触发的条件,默认GC百分比设置的是100,意思是,如果这次回收之后总共占用2M的内存,那么下次触发的条件时当超过4M的时候;同理,当这次回收之后总共占用4M,那么下次触发条件就是8M。
2分钟这个简单,当一定时间(2分钟)没有执行过GC就触发GC
手动使用命令runtime.GC()手动触发GC
优点:无循环引用等问题;可并行处理,GC STW时间短
缺点:writeBarrier存在开销;STW仍然存在
2.3 复制
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另外一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾回收。
原始:
复制(将From中的存活对象复制到To中):
交换角色:
优点:所有存活的数据结构都缩并地排列在 Tospace 的底部,这样就不会存在内存碎片的问题。
获取新内存可以简单地通过递增***空间指针来实现
缺点:内存得不到充分利用,总有一半浪费
2.4 分代回收
分代垃圾回收机制,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不用生命周期的对象可以
采取不同的回收算法,以便提高回收效率。我们将对象分为三种状态:年轻态、年老态、持久代。JVM将堆内存
划分为Eden、Survivor和Tenured/Old空间。
1、年轻代
所有新生成的对象首先都是放在Eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象,对应
的是Minor GC,每次Minor GC会清理年轻代的内存,算法采用效率较高的复制算法,频繁的操作,但是会浪费
内存空间。当“年轻代”区域存放满对象后,就将对象存放到年老代区域。
2、年老代
在年轻代中经历了N(默认15)次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中
存放的都是一些生命周期较长的对象。年老代对象越来越多,我们就需要启动Major GC和Full GC(全量回收),
来一次大扫除,全面清理年轻代区域和年老代区域。
3、持久代
用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响。