算法思想:每一个单元维护一个域,保存其它单元指向它的引用数量(相似有向图的入度)。当引用数量为 0 时,将其回收。引用计数是渐进式的,可以将内存管理的开销分布到整个程序之中。C++ 的 share_ptr 使用的就是引用计算方法。html
引用计数算法实现通常是把全部的单元放在一个单元池里,好比相似 free list。这样全部的单元就被串起来了,就能够进行引用计数了。新分配的单元计数值被设置为 1(注意不是 0,由于申请通常都说 ptr = new object 这种)。每次有一个指针被设为指向该单元时,该单元的计数值加 1;而每次删除某个指向它的指针时,它的计数值减 1。当其引用计数为 0 的时候,该单元会被进行回收。虽然这里说的比较简单,实现的时候仍是有不少细节须要考虑,好比删除某个单元的时候,那么它指向的全部单元都须要对引用计数减 1。java
优势golang
缺点:web
算法思想:把堆栈上的本地变量和任意静态变量叫作根(roots),该算法分红两个阶段,第一个阶段是标记。遍历全部的根变量,而且标记它,而且递归标记它所引用的变量。第二个阶段,遍历全部对象,没有被标记的对象能够回收内存,被标记过的对象去掉标记,方便下次从新标记。 第一阶段伪代码算法
func mark(objectP) {
if !objectP.marked {
objectP.marked = true
for p := range objectP.refedObjs {
mark(p)
}
}
}
复制代码
第二阶段伪代码segmentfault
func sweep() {
for p range in the heap {
if p.marked{
p.marked = false
}else{
head.release(p)
}
}
}
复制代码
由于标记清除式的垃圾回收跟踪了由根(root)访问的全部对象,因此即便是在有循环引用时,它也能够正确的标记并执行垃圾回收工做,这是标记清除最大的优点,而且对对象不会有额外的内存开销和维护。数组
缺点是垃圾回收的时候须要stop the word,影响用户程序的运行。markdown
也叫作复制算法(copying算法)。一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着。当from域内存不够了,开始执行GC操做,这个时候,会把from域存活的对象拷贝到to域,而后直接把from域进行内存清理。并发
jvm将Heap 内存划分为新生代与老年代,又将新生代划分为Eden(伊甸园) 与2块Survivor Space(幸存者区) ,而后在Eden –>Survivor Space 以及From Survivor Space 与To Survivor Space 之间实行Copying 算法。 不过jvm在应用coping算法时,并非把内存按照1:1来划分的,这样太浪费内存空间了。通常的jvm都是8:1。也便是说,Eden区:From区:To区域的比例是:8:1:1
。始终有90%的空间是能够用来建立对象的,而剩下的10%用来存放回收后存活的对象。app
大概步骤:
优势:在存活对象很少的状况下,性能高,能解决内存碎片和标记清除算法的引用更新问题
缺点:会形成一部分的内存浪费。不过能够根据实际状况,将内存块大小比例适当调整; 若是存活对象的数量比较大,coping的性能会变得不好。
基于追踪的垃圾回收算法(标记-清扫、节点复制)一个主要问题是在生命周期较长的对象上浪费时间(长生命周期的对象是不须要频繁扫描的)。同时,内存分配存在这么一个事实 “most object die young”。基于这两点,分代垃圾回收算法将对象按生命周期长短存放到堆上的两个(或者更多)区域,这些区域就是分代(generation)。对于新生代的区域的垃圾回收频率要明显高于老年代区域。
分配对象的时候重新生代里面分配,若是后面发现对象的生命周期较长,则将其移到老年代,这个过程叫作 promote。随着不断 promote,最后新生代的大小在整个堆的占用比例不会特别大。收集的时候集中主要精力在新生代就会相对来讲效率更高,STW 时间也会更短。
优势:性能更优。生命周期长的对象GC频率少,生命周期短的对象GC频率高,大部分对象都是生命周期短的,同时也缩短了STW时间。
缺点:算法实现复杂。
三色标记法是一种改进的标记清除算法,主要是改进了标记部分,使得垃圾回收与用户程序并发执行
算法思想:
Go在进行三色标记的时候并无STW,也就是说,此时的对象仍是能够进行修改,这个时候会有问题,当在进行三色标记中扫描灰色集合中,扫描到了对象A,并标记了对象A的全部引用,以下图 这时候,开始扫描对象D的引用,而此时,另外一个goroutine修改了D->E的引用,变成了以下图所示
这样会不会致使E对象就扫描不到了,而被误认为 为白色对象,也就是垃圾
写屏障就是为了解决这样的问题,引入写屏障后,在上述步骤后,E会被认为是存活的,即便后面E被A对象抛弃,E会被在下一轮的GC中进行回收,这一轮GC中是不会对对象E进行回收的。
写屏障原理:在每一处内存写操做的前面,编译器会生成的一小段代码段,来确保不要打破一些约束条件。即在改变D->E的引用关系到A->E的时候,会有一小段代码,来禁止这个操做。这一小段代码就是一个约束条件,这个约束条件就是:黑色对象不能引用白色对象。
标记阶段:原本标记节点是须要STW,可是为了避免STW,标记阶段与用户程序也作成了并发,并无STW,标记协程是由多个MarkWorker goroutine 共同完成,它们在回收任务完成前绑定到 P,而后进入休眠状态,知道被调度器唤醒,他们与用户协程是并发执行的。
标记阶段与用户程序并发带来两个问题:
1.用户程序新建的对象。对于正在正在进行标记阶段,用户新建的对象可能不会被标记到。所以用户新建的对象直接认为是黑色的,本次标记不会标记到它,这样用户程序新建对象就没有问题了。
2.用户程序修改对象的引用关系。写屏障不容许黑色的对象直接引用白色的对象,固然写屏障并非真的不让黑色对象引用白色对象,而是发⽣⼀个信号,垃圾回收器会捕获到这样的信号后就知道这个对象发⽣改变,而后从新扫描这个对象,看看它的引⽤或者被引⽤是否被改变,这样利⽤状态的重置从⽽实现当对象状态发⽣改变的时候依然能够判断它是活着的仍是死的。
那标记阶段何时会STW呢?
清理阶段:不会STW,由于清理的是白色的对象,白色的对象不可能被用户程序用到,所以清理程序能够与用户程序并发执行,不须要STW。
并发清理本质上是一个死循环,被唤醒后开始执行清理任务。 经过遍历全部span 对象,触发内存回收器的回收操做。任务完成后,再次休眠,等待下次任务
总结一些GO GC的三色标记过程:
三色标记法方案支持并行,即用户代码能够和GC代码同时运行。具体来说,Golang GC分为几个阶段:
总结一下,Golang的GC过程有两次STW:第一次STW会准备根对象的扫描, 启动写屏障(Write Barrier)和辅助GC(mutator assist),为GC Drains阶段作准备.第二次STW会从新扫描部分根对象, 禁用写屏障(Write Barrier)和辅助GC(mutator assist),由于GC Drains阶段已结束.
GO GC分红这么多阶段主要是为了让GC与用户程序并发执行。分红多个阶段,在必须STW的阶段才去STW,这样其余阶段就能够跟用户阶段并发执行了。
每一次STW耗时极小,通常在1ms之内。
(1)触发的时间。在堆上分配大于 32K byte 对象的时候进行检测此时是否知足垃圾回收条件,若是知足则进行垃圾回收。
(2)触发的条件。
// gcShouldStart returns true if the exit condition for the _GCoff
// phase has been met. The exit condition should be tested when
// allocating.
//
// If forceTrigger is true, it ignores the current heap size, but
// checks all other conditions. In general this should be false.
func gcShouldStart(forceTrigger bool) bool {
return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
}
复制代码
forceTrigger 是 forceGC 的标志;后面半句的意思是当前堆上的活跃对象大于咱们初始化时候设置的 GC 触发阈值。在 malloc 以及 free 的时候 heap_live 会一直进行更新。
1.硬性参数
假设进程所占内存为reachable, 则reachable*(1+GOGC/100)=8M 的时候,gc 就会被触发,开始进行相关的 gc 操做。
所以可根据进程占用内存大小来调整GOGC参数,减小GC触发的次数
2.代码层面的tiops
(1)减小对象分配:所谓减小对象的分配,其实是尽可能作到,对象的重用。 例以下面两个函数:
func(r*Reader)Read()([]byte,error)
func(r*Reader)Read(buf[]byte)(int,error)
复制代码
第一个函数没有入参,所以第一个函数里面每次都会分配空间并返回;第二个函数有入参buf,所以在函数内部可利用传入的buf,不须要分配内存。
(2)少作string和[]byte转化。减小gc压力
(3)少使用字符串拼接。使用+进行字符串拼接会生成新的对象
(4)提早预知数组大小。数组初始化的时候必定要用make,及时大小是0,不过最好预估一下大小,这样在append的时候就不会常常去扩容
【1】Go 垃圾回收
【2】Golang 垃圾回收剖析
【3】以标记清除的方式垃圾回收
【4】为何 Go 在 GC 时 STW 的时间很短?
【5】垃圾回收算法(计数、标记、复制),golang垃圾回收
【6】Golang源码探索(三) GC的实现原理
【7】深刻理解Go-垃圾回收机制
【8】java垃圾回收之复制算法