这段代码输出?

  1. package main
  2. import "fmt"
  3. func main() {
  4. slice := []int{0, 1, 2, 3}
  5. m := make(map[int]*int)
  6. for key, val := range slice {
  7. m[key] = &val
  8. }
  9. for k, v := range m {
  10. fmt.Println(k, "->", *v)
  11. }
  12. }
  1. map遍历是无序的
  2. //这里的val是源数据的一份拷贝,
  3. //在循环过程中地址不变,因此&val是不变的,循环结束是val=3
  4. // 因此所有的*v==3
  5. 3 -> 3
  6. 0 -> 3
  7. 1 -> 3
  8. 2 -> 3

线程和协程内存大小?

线程一般是2M,协程一般是2K,线程栈空间大小默认是固定的(和操作系统有关),协程启动时是2-4k,栈空间按照需要动态伸缩。

堆栈的缓存方式

栈使用的一级缓存,通常是被调用时处于存储空间中,调用完立即释放。
堆则是存放在二级缓存中,生命周期有虚拟机的垃圾回收算法来决定。

make和new区别?

go数据类型分为值类型和引用类型,其中
值类型:int,float,string,bool,struct,array,直接存储值,分配在栈上,调用完释放
引用类型:slice,map,chan和值类型的指针,他们的存储是一个地址,指针指向内存中真正存储数据的首地址,内存通常在堆分配,通过GC回收。
区别

  • new 参数要求传入一个类型,而不是一个值,它会申请该类型的内存空间,并初始化为对应的零值,返回该指向类型空间的一个指针。
  • make也用于内存分配,但它只用于引用对象slice,map,channel的内存创建,返回是类型本身。

值传递和指针传递的区别?

值传递:会创建一个新的副本并将其传递给所调用的函数或方法
指针传递:会创建相同内存地址的新副本。
需要改变参数本身的时候用指针传递,否则用值传递。
另外,如果函数内部返回指针,则会发生内存逃逸。

聊聊内存逃逸分析?

所谓内存逃逸指由编译器来决定内存分配的位置,不需要程序员来指定。
导致内存逃逸的情况比较多,通常来讲就是变量作用域不会扩大并且行为或者大小能够在其编译时确定,一般情况下都分配在栈上,否则就可能发生内存逃逸到堆上。
内存逃逸典型情况:在函数内部将局部变量指针返回

指针逃逸

  1. package main
  2. type Student struct {
  3. Name string
  4. Age int
  5. }
  6. func StudentRegister(name string, age int) *Student {
  7. s := new(Student) //局部变量s逃逸到堆
  8. s.Name = name
  9. s.Age = age
  10. return s
  11. }
  12. func main() {
  13. StudentRegister("Jim", 18)
  14. }

虽然 在函数 StudentRegister() 内部 s 为局部变量,其值通过函数返回值返回,s 本身为一指针,其指向的内存地址不会是栈而是堆,这就是典型的逃逸案例。
终端运行命令查看逃逸分析日志:go build -gcflags=-m main.go

  1. ./main.go:16:6: can inline StudentRegister
  2. ./main.go:25:6: can inline main
  3. ./main.go:26:17: inlining call to StudentRegister
  4. ./main.go:16:22: leaking param: name
  5. ./main.go:17:10: new(Student) escapes to heap
  6. ./main.go:26:17: new(Student) does not escape

可见在StudentRegister()函数中,也即代码第10行显示”escapes to heap”,代表该行内存分配发生了逃逸现象。

栈空间不足逃逸(空间开辟过大)

  1. package main
  2. func Slice() {
  3. s := make([]int, 1000, 1000)
  4. for index, _ := range s {
  5. s[index] = index
  6. }
  7. }
  8. func main() {
  9. Slice()
  10. }

上面代码Slice()函数中分配了一个1000个长度的切片,是否逃逸取决于栈空间是否足够大。
实际上当栈空间不足以存放当前对象时或无法判断当前切片长度时会将对象分配到堆中。

动态类型逃逸(不确定长度大小)

很多函数参数类型为interface类型,比如fmt.Println(a…interface{})在编译期间很难确定其具体类型,也能发生逃逸。如

  1. package main
  2. import "fmt"
  3. func main() {
  4. s := "Escape"
  5. fmt.Println(s)
  6. }

或者

闭包引用对象逃逸

斐波那契数列:

  1. package main
  2. import "fmt"
  3. func Fibonacci() func() int {
  4. a, b := 0, 1
  5. return func() int {
  6. a, b = b, a+b
  7. return a
  8. }
  9. }
  10. func main() {
  11. f := Fibonacci()
  12. for i := 0; i < 10; i++ {
  13. fmt.Printf("Fibonacci: %d\n", f())
  14. }
  15. }

Fibonacci函数中原本属于局部变量的a和b由于闭包的引用,不得不将两者放在堆上,以致产生逃逸。

逃逸分析的作用?

(逃逸分析在编译阶段完成)

  • 逃逸分析的好处是为了减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。
  • 逃逸分析完后可以确定哪些变量分配在栈上,哪些在堆上。
  • 同步消除,如果定义的对象方法上有同步锁,但在运行时,只有一个线程在访问,此时逃逸分析后的机器码会去掉同步锁运行。

    函数传递指针真的比传值效率高吗?

    传递指针相比传值减少了底层拷贝,可以提高效率,但是拷贝的数据量较小,由于指针传递会产生逃逸,可能使用堆,也可能增加gc负担。

    golang 内存管理

    内存池概述

    Go语言内存分配器采用了和tcmalloc相同的实现,是一个带内存池的分配器,底层直接调用操作系统的mmpa等函数。
    作为一个内存池,他的基本组成部分为:

  • 首先,向操作系统申请一大块内存,自己管理这部分内存。

  • 然后,它是一个池子,当上层释放内存的时候并不实际归还给操作希滕,而是放回池子重复使用
  • 接着,内存管理必然要考虑内存碎片问题,如果尽量避免内存碎片,提高内存利用率,会涉及到操作系统中的首次适应,最佳适应,最差适应,伙伴算法等相关知识。
  • 另外,Go语言的内存管理必须要考虑在多线程下的稳定性和效率问题。

    多线程方面

    Go语言中每个线程都有自己的本地内存,然后有一个全局分配链,当某个线程中的内存不足后就向全局分配链中申请内存。这样就避免了多线程同时访问共享变量的加锁。
    在避免内存碎片方面,大块内存直接按页为单位分配,小块内存会切成各种不同的固定大小的块,申请做任意字节内存时会向上取整到最接近的块,将整块分配给申请者以避免随意切割。

线程有几种模型?

线程模型有:

  • 用户级线程模型:多对一(M:1)
  • 内核级线程模型: 一对一(1:1)
  • 混合线程模型 : 多对多(M:N)

GO垃圾回收算法

GoLang GC算法变化:

  • v1.1 STW
  • v1.3 标记清除法,Mark STW, Sweep并行,
  • v1.5 并发三色标记法,将时间延迟从几百ms降低到10ms以下
  • v1.8 hybrid write barrier (混合写屏障),将时间缩短至0.5ms以内

Go的垃圾回收基于标记-清扫算法,并作了一定改进,减少了STW时间。
标记清扫算法

    1. 暂停应用程序,找出不可达对象,然后做上标记。
    1. 回收标记好的对象。

注意: 在执行gc的时候程序需要暂停,即stop the world,就是runtime将所有线程全部冻结掉,所有对象都不会被修改了
标记清扫出现的问题

  • SWT,程序卡顿
  • 标记需要扫描整个heap
  • 清除数据会产生heap碎片

三色并发标记法:
将程序中对象分类三类,白色(潜在垃圾), 黑色 (活跃对象), 灰色(活跃对象,因存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象).

    1. 首先将程序创建的对象都标记为白色
    1. gc开始扫描,遍历根节点集合里的所有对象,将根对象将可达对象都标记为灰色。
    1. 再从灰色对象中找到其引用的对象,将其标记为灰色,将自身标记为黑色

再重复2,3步骤直至没有灰色对象

    1. 对所有白色对象进行清除。

      GC和用户逻辑如何并行?

      标记清除算法包含两部分逻辑,标记和清除,在三色标记法中最后只剩下黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不触碰黑色对象,只清除白色对象,就不会影响程序逻辑,所以清除操作和用户逻辑可以并发。

      进程生成新对象的时候GC如何操作?

      Golang为解决这个问题,引入了写屏障这个机制。
      写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其他组件感知。
      通俗来说就是gc在跑的时候,可以监控对象的内存修改,并对对象重新进行标记(实际上也是超短暂的stw,然后对对象标记).新生成的对象一律标为灰色
      在1.7版本之前,使用的是Dijkstra插入写屏障来保证强三色不变性,但是运行时并没有在所有垃圾收集跟对象上开启插入写屏障,因为Go程序可能包含成百上千个goroutine,gc的跟对象一般包括全局对象和栈对象,带来的开销巨大,因此Go团队在实现上选择了暂停程序,将所有栈对象标记为灰色并重新扫苗重新扫描的过程占用10~100ms时间。
      在1.8版本中组合Dijkstra插入写屏障和Yuasa删除写名章构成了混合写屏障,该写屏障会将被覆盖的对象标记成灰色并在当前栈没有扫描时将新对象也标记为灰色

      那么灰色或者黑色对象的引用改为白色对象的时候,Golang如何操作?

      比如,一个黑色对象引用了曾经标记为白色的对象,这是写屏障机制触发,向GC发送信号,GC重新扫描对象并标记为灰色,因此gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

三色不变性

想要在并发或者增量的标记算法中保证正确性,需要达成两种三色不变性中的任意一种:

  • 强三色不变性:黑色对象不会指向白色对象,只会指向灰色对象或者黑色对象
  • 若三色不变性:黑色对象指向的白色对象必须包含一条从灰色对象经由多个白色对象的可达路径。

    调优经验

  1. 减少对象分配,实际上尽量做到对象的重用,比如两个函数的定义:

第一个函数没有形参,每次调用返回一个[]byte,第二个函数每次调用形参是一个buf []byte的对象,之后返回读取的数目,第一个每次调用都会分配一段空间,给gc造成额外压力,而第二个每次调用都会重用形参声明。

  1. string和[]byte转化,在string和byte转化会造成gc压力,两者发生转换时,底层数据结构会复制,解决策略是在网络传输是一直用[]byte,另一种是使用更为底层的操作,来避免复制发生。
  2. 少使用+连接string,使用+连接string会生成新的对象,降低gc效率,较好的方式是使用append函数
  3. append操作,如果提前知道数组长度,最好最初分配空间就做好空间规划,降低gc压力,提升代码效率。

gc 简述

垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障
标记清除法
分为两个阶段:标记和清除
标记阶段:从根对象出发寻找并标记所有存活的对象。
清除阶段:遍历堆中的对象,回收未标记的对象,并加入空闲链表。
缺点是需要暂停程序STW。
三色标记法
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此Go采用了**写屏障技术**,当对象新增或者更新会将其着色为灰色
一次完整的GC分为四个阶段:

  1. 准备标记(需要STW),开启写屏障。
  2. 开始标记
  3. 标记结束(STW),关闭写屏障
  4. 清理(并发)

基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:

  1. GC开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需STW);
  2. GC期间,任何栈上创建的新对象均为黑色
  3. 被删除引用的对象标记为灰色
  4. 被添加引用的对象标记为灰色

总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得GC时间从 2s降低到2us。