Go内存管理

发布于 2019-04-25 · 本文总共 3316 字 · 阅读大约需要 10 分钟

Go垃圾回收机制

v1.1 STW
v1.3 Mark STW, Sweep 并行
v1.5 三色标记法
v1.8 hybrid write barrier(混合写屏障:优化STW)

GC算法

引用计数(Reference Counting)、标记-清扫(Mark & Sweep)、节点复制(Copying Garbage Collection)、 分代收集(Generational Garbage Collection)等

引用计数

Python-GC相关内容

标记清扫—-Mark & Sweep

内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。 这个时候系统会挂起用户程序,也就是STW,转而执行垃圾回收程序

标记-清扫算法的优点也就是基于追踪的垃圾回收算法;

具有的优点:

避免了引用计数算法的缺点(不能处理循环引用,需要维护指针)。

缺点:

需要 STW

三色标记算法

三色标记算法是对标记阶段的改进,原理如下:

1.起初所有对象都是白色。

2.从根出发扫描所有可达对象,标记为灰色,放入待处理队列。

3.从队列取出灰色对象,将其引用对象标记为灰色放入队列,自身标记为黑色。

4.重复 3,直到灰色对象队列为空。此时白色对象即为垃圾,进行回收。

Golang GC算法

三色标记法

写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。 就是在gc跑的过程中,可以监控对象的内存修改,并对对象进行重新标记。 (实际上也是超短暂的STW,然后对对象进行标记)

go内部内存结构

栈存储区,每个Goroutine有一个栈,存储了静态数据,包括函数栈帧、静态结构、原生类型值和指向动态结构的指针。

Go内存使用—-栈与堆

栈由OS自动管理,go不用管

  • 堆不是由OS管理
  • 具有最大内存空间
  • 保存动态数据
    可能会成倍增长,导致程序会随着时间耗尽内存

随着时间流逝,内存空间变得支离破碎—->GC

Go内存管理

1。需要内存时自动分配
2。不需要时GC
runtime完成,开发人员不必处理

1。内存分配

thread local cache:
使用线程本地缓存来加速小对象分配

维护scan/noscan的span来加速GC

根据对象大小决定对象的分配过程:

将内存分成不同的级别分别管理,引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存

Tiny-微小对象,size<16B

mcache微小分配器分配小于16B的对象

小对象, 16B<size<32KB

分配在G运行所在的P的mcache的对应的mspan size class上

大对象,>32KB

直接分配在mheap上,如果mheap没有足够的空间则从OS分配一组新的页

2。GC

  • 释放孤儿对象(不再被栈直接或间接引用)使用的内存
  • Go使用非分代的、并发的、基于三色标记和清除的垃圾回收器(1.12以后)

GC触发条件

自动垃圾回收的触发条件有两个:

  • 超过内存大小阈值
  • 达到定时时间
    阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。比如一次回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高。 如果一直达不到内存大小的阈值呢?这个时候GC就会被定时时间触发,比如一直达不到10M,那就定时(默认2min触发一次)触发一次GC保证资源的回收。

实践

打开GC日志

只要在程序执⾏之前加上环境变量GODEBUG=gctrace=1
如:GODEBUG=gctrace=1 go test -bench=.
GODEBUG=gctrace=1 go run main.go

日志详细信息参考: https://godoc.org/runtime

gc次数 时间 gc时间占的百分比 堆大小–gc开始-gc结束–现存 所有堆大小 用到的processor

gc 2925 @1.879s 3%: 0.002+0.12+0.011 ms clock, 0.038+0.054/0.075/0.12+0.18 ms cpu, 4->4->0 MB, 5 MB goal, 16 P scvg: 0 MB released
scvg: inuse: 2, idle: 61, sys: 63, released: 58, consumed: 5 (MB)

go tool trace

普通程序输出 trace 信息:

package main
import (
"os"
"runtime/trace"
)
func main() {
f, err := os.Create("trace.out") if err != nil {
panic(err) }
defer f.Close()
err = trace.Start(f) if err != nil {
panic(err) }
defer trace.Stop()
     // Your program here
}

测试程序输出 trace 信息 :

go test -trace trace.out

可视化 trace 信息:

go tool trace trace.out

逃逸分析

终端运行命令查看逃逸分析日志:

go build -gcflags=-m

逃逸分析的作用是什么呢?

逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。

逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(逃逸的局部变量会在堆上分配 ,而没有发生逃逸的则有编译器在栈上分配)。

同步消除,如果你定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

逃逸总结:

栈上分配内存比在堆中分配内存有更高的效率

栈上分配的内存不需要GC处理

堆上分配的内存使用完毕会交给GC处理

逃逸分析目的是决定内分配地址是栈还是堆

逃逸分析在编译阶段完成

  • 提问:函数传递指针真的比传值效率高吗?

我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小, 由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame.

However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.



本博客所有文章采用的授权方式为 自由转载-非商用-非衍生-保持署名 ,转载请务必注明出处,谢谢。
声明:
本博客欢迎转发,但请保留原作者信息!
博客地址:邱文奇(qiuwenqi)的博客;
内容系本人学习、研究和总结,如有雷同,实属荣幸!
阅读次数:

文章评论

comments powered by Disqus


章节列表