垃圾回收GC

2021年12月13日 · 2408 字 · 5 分钟 · Java

什么是GC

GC指一种自动的存储器管理机制,当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。

-from wiki

垃圾回收算法有哪些 分别的优缺点

================

引用计数法


对每个对象设置引用计数,当对象被引用+1,失去引用/销毁-1,当计数为0的时回收对象内存

优点:简单直接,回收速度快

缺点:需要额外空间维护引用计数,无法解决对象的循环引用问题

标记清除法


从根对象开始遍历所有引用对象,引用的对象打上标记tag,遍历完成,将没有标记的进行回收

优点:解决引用计数法的缺点

缺点:会产生大量不连续的内存碎片,导致无法给大对象分配内存

标记整理法


让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

优点:不会产生内存碎片

缺点:需要移动大量对象,处理效率比较低。

#复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理

优点:不会产生内存碎片,每次清除针对的都是整块内存

缺点:只使用了内存的一半、移动对象需要耗费时间,效率低于标记清除法

分代收集法


按照对象的生命周期长短划分代空间,生命周期长的放在老年代,生命周期短的放在新生代

优点:回收性能好

缺点:算法复杂

go的垃圾回收采用是哪个GC方法

================

go采用的是标记清除法,核心就是标记出哪些是内存还在使用(被引用的),哪些内存不再使用(未被引用),把未被引用的内存回收掉,供后续内存分配使用。

暂时无法在飞书文档外展示此内容

特殊case:如果内存块存放的是指针,那还需要递归的进行标记,全部标记完后,只保留标记的内存,未被标记的内存全部进行回收

为什么Go采用标记清除法,而不是其他的方法?

======================

引用计数无法解决循环引用,排除

标记整理好处在于解决内存碎片化的问题,但是Go运行时的分配算法基于tcmalloc,基本上没有碎片问题,对于gc并没有提升

复制只能用一半的内存,还需要大量移动,效率低

分代收集的话也不适用,因为go的gc主要目标是新创建的对象上,即存活时间短更利回收,而不是频繁的检查所有对象

逃逸分析:编译器决定内存分配的位置,不需要程序员指定。函数中申请一个新的对象

如果分配到栈,则函数执行结束就可自动将内存回收

如果分配到堆,则函数执行结束可交给GC(垃圾回收)处理

go编译器的逃逸分析,将大部分新生对象存储在栈里面,直接被回收,生命周期短的对象直接回收并不需要gc处理,长期存在的/比较大的对象会分配到堆中,才被gc回收,所以分代回收并没有实质上提升

什么是三色标记法?mark-sweep

===================

人为的用三种颜色好描述go的gc过程,内存中的对象并无颜色区分

三色对应了垃圾回收中的三种状态:

灰色:对象放入“标记队列”中等待(待处理的对象)

黑色:对象已被标记为使用

白色:对象未被标记

步骤:

  1. 开始gc初,所有对象放入白色队列

  2. 从根对象开始遍历,将所有可达的对象,标记为灰色,放入灰色队列(待处理队列)

  3. 从灰色队列中取出灰色对象,将它引用的对象标记灰色放入灰色队列,它自己标黑色,放入黑色队列

  4. 重复步骤3,直到灰色队列为空,这时候白色对象是不可达对象,回收白色对象

什么是根对象,根对象有哪些?

==============

  1. 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量

  2. 执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上包含栈上的变量及指向分配的堆内存区块的指针

  3. 寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块

怎么的条件会触发Go的GC

=============

  • GOGC threshold

每次内存分配时检查当前内存分配量是否达到阀值,达到则会触发gc

阀值 = 上次gc内存分配量 * 内存增长率

内存增长率是由环境变量GCGO控制,默认是100,即当内存扩大一倍的时候启动gc

  • runtime.GC()

类似Java的system.gc api手动代码触发gc

  • runtime.forcegcperiod(2min)

强制定期gc,默认2min触发一次gc,在runtime/proc.go:forcegcperiod

go的GC有哪些优化

==========

标记-清理需要stw,需要暂停所有的goruntine,做gc然后再恢复。

减少stw时间,可以提升go的gc性能

写屏障(Write Barrier) 

本质就是每次内存写操作时候,额外执行一小段代码

写屏障就是让goroutine与GC同时运行的手段,虽然写屏障不能完全消除stw,但是可以大大减少stw时间,类似开关,gc的特定时候开启,开启后指针传递时,把指针标记,即本轮不回收,下次gc再确定

辅助GC(Mutator Assist) 

为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么这个goroutine会参与一部分GC的 工作,即帮助GC做一部分工作,这个机制叫作Mutator Assist

代码GC编程

多制造inline的机会,将新对象尽可能都分配到栈而不是堆,因为go实现了退栈即释放,不影响gc

代码减少逃逸分析:

  1. 尽量使用局部变量(编译器会根据变量是否被外部引用来决定是否逃逸)

  2. 参数、返回数值传递值(传指针还是数值,需要修改原值或者内存比较大结构体传指针,而对于只读的占内存较少的结构体,传值获取较好性能)

代码简单直白,制造inline机会

减少分配次数

a = make([]int01234)
b = make(map[string]int2048)

缓存对象

什么是inline

内联扩展内联是一种手动或编译器优化,它用被调用函数的主体替换函数调用站点

from wiki

通过参数-gflags="-m"查看

func add (xy int) {
    return x + y
}

func main() {
    x := 1
    y := 2
    a := add(x,y)
    fmt.println(a)
}
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-

func main() {
    x := 1
    y := 2
    //inline function add replace by the body of the function
    a := x + y
    fmt.println(a)
}

不用使用内联的case:闭包调用、select、for、defer、go关键词创建的协程

总结:采用越简单的实现,对于傻瓜式语言性能越好

逃逸分析

通过命令go build -gcflags ‘-m’命令查看

var refs = make([]*int32)

func fc()  {
   refs[0] = new(int)
}
func main() {
   fc()
}