GC 不回收什么?
为了解释垃圾回收是什么,我们先来说说 GC 不回收什么。
在我们程序中会使用到两种内存,分别为堆(Heap)和栈(Stack),而 GC 不负责回收栈中的内存。
那么这是为什么呢?
主要原因是栈是一块专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈。
除此以外,栈中的数据都有一个特点——简单。比如局部变量就不能被函数外访问,所以这块内存用完就可以直接释放。正是因为这个特点,栈中的数据可以通过简单的编译器指令自动清理,也就不需要通过 GC 来回收了。
什么是垃圾回收
“垃圾”的定义
首先,我们要给个“垃圾”的定义,才能进行回收吧。书中给出的定义:
把分配到堆中那些不能通过程序引用的对象称为非活动对象,也就是死掉的对象,我们称为“垃圾”
GC的定义
垃圾回收(Garbage Collection,GC),顾名思义就是释放垃圾占用的空间,防止内存泄露。
有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
因为我们期望让内存管理变得自动(只管用内存,不管内存的回收),我们就必须做两件事情:
- 找到内存空间里的垃圾
- 回收垃圾,让程序员能再次利用这部分空间 [1] 只要满足这两项功能的程序,就是GC,不论它是在JVM中,还是在Ruby的VM中。
但这只是两个需求,并没有说明GC应该何时找垃圾,何时回收垃圾等等更具体的问题,各类GC算法就是在这些更具体问题的处理方式上施展手脚。
什么是根对象(roots)
根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:
- 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
- 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针。
- 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。
常见的垃圾回收方法
目前比较常见的垃圾回收算法有三种
引用计数
引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0是回收该对象。
c++ 需要用户手动释放内存,容易导致内存泄露等问题,后来引入了智能指针使用的就是引用计数进行垃圾回收。对每一个对象都引入一个计数器,每引用一次计数器+1,解除引用-1,当计数器为0时即表示该对象已经不需要访问了可以回收。
- 优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
- 缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
- 代表语言:Python、PHP、Swift、JavaScript(V8引擎)
标记-清扫
从根变量开始遍历所有引用的对象,引用的对象标记为”被引用”,没有被标记的进行回收。
- 触发 GC:主动,定时,被动,阈值等
- STW,从根对象开始扫描,标记活跃对象
- 清除非活跃对象
- 取消 STW
- 优点:解决了引用计数的缺点。
- 缺点:需要STW,即要暂时停掉程序运行。
-
分代收集
JVM垃圾回收时常用的GC算法。
按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。 优点:回收性能好
- 缺点:算法复杂
- 代表语言: JAVA
这些GC算法共同解决的问题
GC的定义只给出了需求,三种算法都为实现这个需求,那么它们总会遇到共同要解决的问题吧? 我尝试总结出:
- 如何分辨出什么是垃圾?
- 如何、何时搜索垃圾?
-
如何评价GC算法?
如果没有评价标准,我们当然无法评估这些GC算法的性能。作者给出了4个标准:
吞吐量: 单位时间内的处理能力
- 最大暂停时间:GC执行过程中,应用暂停的时长。 较大的吞吐量和较短的最大暂停时间不可兼得
- 堆的使用效率:就是堆空间的利用率。 可用的堆越大,GC运行越快;相反,越想有效地利用有限的堆,GC花费的时间就越长。
- 访问的局部性:把具有引用关系的对象安排在堆中较近的位置,就能提高在缓存中读取到想利用的数据的概率。
Go GC
Golang中的垃圾回收主要应用三色标记法,GC过程和其他用户goroutine可并发运行,但需要一定时间的STW(stop the world),STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,Golang进行了多次的迭代优化来解决这个问题。
我们先看看历代Golang版本的垃圾回收机制吧
- G0 V1.3 之前的标记-清除(mark and sweep)算法,缺点是STW时间过长
- Go V1.5 三色并发标记法 (有且仅有栈空间清扫时需要STW来重新标记,但堆空间清扫不需要STW)
- Go V1.8 引入混合写屏障机制(几乎不需要STW)
Golang的GC有分栈空间清扫与堆空间清扫。为什么GC还需要针对栈,栈难道不是OS直接去管理的吗?
每个调度器的结构体有两个G:
- 一个是crug,代表结构体M当前绑定的结构体G。
一个是g0(G零),是带有调度栈的goroutine,这是一个比较特殊的goroutine。
注意: 普通的goroutine的栈是在堆上分配的可增长的栈。 而g0的栈是M对应的线程的栈。 所有调度相关的代码,会先切换到该goroutine栈中再执行。也就是说线程的栈也是用的g实现,而不是使用os的。
简单来说:g0所使用的栈就是Go程序进程所创建的M线程的线程栈,与相关的task_struck相关联而未与M绑定(不是running状态的goroutine)的goroutine的协程栈也是存放在进程的堆空间中的!
因此这里就会有很好玩的现象,goroutine的栈是可以扩容与缩容的。
且栈收缩不是在函数调用时发生的,是由Golang垃圾回收器在垃圾回收时主动触发的。
标记-清扫GC算法
首先会从一些固定的root节点开始,对于Go语言来说就是全局指针和 goroutine 栈上的指针,根据这些root节点进行递归标记。当标记完成后,所有被标记的对象就都是存活的,其余的对象即是可以清扫的。
Golang三色标记+混合写屏障GC模式全分析
参考: