“Go程序员面试笔试宝典”复习便签

一.逃逸分析

1.1逃逸分析是什么?

逃逸分析,主要是Go编译器用来决定变量分配在堆或者栈的手段。

区分于C/C++手动管理内存分配,Go将这些工作交给了编译器。

1.2逃逸分析有什么作用

解放程序员。程序员不需要手动指定指针分配内存。

灵活的内存管理。编译器机制可以高效管理内存。

1.3逃逸分析是怎么完成的

编译器根据变量是否被外部引用,决定是否发生逃逸。

如果不被外部引用,则发生逃逸;反之,则发生逃逸。

1.4如何确定是否发生逃逸

运行/编译时,可以指定参数进行查看

1.5Go与C/C++中的堆和栈是同一个概念吗

不是。

C/C++中的堆栈,就是操作系统中传统的堆栈概念。

Go语言中则不同。Go语言中,操作系统中的栈,都提供给了Go运行时,用来处理调度器、垃圾回收、系统调用等。对于用户态的Go代码,消耗的都是操作系统中的堆内存,只是构造出了逻辑上不同的“堆”和“栈”。

二.延迟语句

2.1延迟语句是什么?

延迟语句(defer)是Go语言中注册延迟调用的机制,主要用于成对操作时,如文打开文件/关闭文件、打开连接/关闭连接、加锁/释放锁。

2.2延迟语句的执行顺序是什么?

defer语句会将调用函数压入栈中,先进后出的执行。

在defer函数定义时,对外部变量的引用有2种形式:

1.函数参数:defer定义时会把值传递给defer,并被cache起来。 2.闭包引用:真正执行时根据上下文确定参数值。

2.3如何拆解延迟语句

return xxx

相当于:

1.返回值=xxx

2.调用defer函数

3.return

2.4如何确定延迟语句的参数

判断依据主要与2.2的规则一样。

2.5闭包是什么?

匿名函数也被称为闭包,一个闭包继承了函数声明时的作用域,Go语言中所有的匿名函数都是闭包。

2.6延迟语句如何配合恢复语句

recover()函数只在defer的函数中直接调用才有效。

2.7 todo

2.8为什么无法从父goroutine恢复子goroutine的panic

设计使然。goroutine被设为为一个独立的代码执行单元,拥有自己的执行栈,不与其他goroutine共享任何数据。这意味着,无法让goroutine拥有返回值、也无法让goroutine拥有自身的ID编号等。

三、数据容器

3.1数组与切片

3.1.1数组和切片有何异同

数组是定长的,长度是类型的一部分,所以表达能力有限,在Go语言中不常见。

切片则非常灵活,可以动态扩容,且切片的类型和长度无关。

*底层数组可以被多个切片同时指向,因此对一个切片的元素进行操作,有可能会影响到其他切片。

3.1.2切片如何被截取

基于已有slice创建新slice对象,被称为reslice。新老slice共用底层数组,它们对底层数组的更改都会影响到彼此。

如果因为append操作引起了新slice或者老slice底层数组扩容,则不会相互影响。

这其中的关键就是:两者是否共用底层数组。

 
  

func main (){ slice := []int{0,1,2,3,4,5,6,7,8,9} s1 := slice[2:5] s2 := s1[2:6:7] s2 = append(s2,100) s2 = append(s2,200) s1[2] = 20 }

 
  

s1:[2 3 20] s2:[4 5 6 7 100 200] slice:[0 1 2 3 4 5 6 7 100 9]

3.1.3 切片的容量是怎样增长的?

下面的说法是不准确的:

1.当原slice容量小于1024的时候,新slice容量变成原来的2倍

2.当原slice容量超过1024,新slice容量变成原来的1.25倍

实际是:

扩容过程对newcap进行了内存对齐,而这个和内存分配策略有关。进行内存对齐后,新s的容量要大于等于老s容量的2倍或者1.25倍。

3.1.4切片作为函数参数会被改变吗?

slice作为函数参数时,就是一个普通的结构体。直接传slice,实参不受影响;传入slice的指针,则会影响。

Go语言中的函数参数传递,只有值传递,没有引用传递。

3.1.5 内建函数make和new的区别是什么

1.make和new都用来分配内存,但适用类型不同。make适用于slice、map、channel等引用类型,new适用于int、数组、结构体等值类型。

2.make返回一个值,new返回一个指针。

3.make返回初始化之后的类型的引用,new会为类型的新值分配已置零的内存空间,并返回指针。

3.2散列表map

  3.2.1map是什么

map最主要的数据结构有两种:哈希查找表(Hash table)、搜索树(search tree)

哈希查找表用一个哈希函数将key分配到不同的bucket桶,开销主要是哈希函数的计算以及数组的常数访问时间。处理碰撞的方法一般有:链表法和开放地址法。

搜索树一般采用平衡搜索树,包括AVL树、红黑树等。

  3.2.2map的底层原理是什么?

  Go语言使用的是哈希查找表,并且使用链表法解决哈希冲突。

  1.map内存模型

  map的结构体是hmap。关键字段有:

  B:buckets数组的长度的对数,即buckets数组的长度为2^B,bucket里面存储了key和value,bucket是一个指针,指向的是一个结构体:bmap。

  bmap就是人们常说的“桶”,桶里最多装8个

  这些key之所以会落入同一个桶,是因为它们的hash结果是“一类”的,并不是完全相等。hash值的高8位,决定落入桶内的槽位。

每个bucket设计成最多只能放8个key-value对,如果有第9个key-value落入当前的bucket,则需要再构建一个bucket,并通过overflow指针连接起来。这就是所谓的“链表法”。

  2.创建map

创建map,就是调用makemap函数,初始化hmap的各个字段。

slice和map分别作为函数参数数时有什么区别?

在函数内部对map的操作会影响map结构体;而对slice操作则不会。

主要原因:前者是指针(*hmap),后者是结构体(slice)

  3.哈希函数

  Go会检测cpu是否支持aes,如果支持使用aes hash,否则使用memhash。

  4.key定位过程

哈希值共64个bit位(针对64位机),计算元素落入到哪个bucket,只会用到最后B个bit位。

当两个不同的key落入同一个桶中,使用链表法解决哈希冲突,即链表法:从前往后查找第一个空位。

查找时:先找到对应的桶,再去遍历桶中的所有key。如果bucket中没有找到,并且overflow不为空,则会继续在overflow bucket中寻找。

寻找某个key的底层函数是mapacess系列函数。

  5.map的赋值过程是怎样的

  向map插入或者修改key,调用的是mapassign函数。赋值操作的核心仍然是一个双层循环:外层遍历bucket和overflow bucket,内层遍历单个bucket的所有槽位。有比较重要的几点:

  1.写标志flags=1时,说明有其他协程在执行“写”操作,程序会panic,说明map不是协程安全的。

  2.map的扩容是渐进式的,定位元素到某个bucket后,需要确保这个bucket对应的老bucket已经完成了迁移过程(老bucket中的key会被分散到2个新bucket)

  3.定位元素放置的位置时,准备两个指针: inserti和insertk分别指向第一个空的tophash、第一个空闲的cell(槽)

  4.如果触发扩容,则查找定位key的过程会重新执行一次。

  5.最后会更新元素的值,如果是插入的话,map的count字段值加1,hasWriting清零。

  6.map的删除过程是怎样的

  底层执行mapdelete函数,主要逻辑:

  1.检测并发写操作

  2.计算元素的hash值,找到落入的bucket

  3.设置写标志位

  4.检测此map是否在扩容中,如果是,则触发一次搬迁。

  5.两层循环,核心是找到key的具体位置。

  6.找到对应位置后,完成清零操作。

  7.将map的count字段减1,对应位置的tophash改为emptyone。

  8.联动判断是否处理同bucket的其他槽位,emptyOne改成emptyRest的过程。

  7.map的扩容过程是怎样的(todo)
  8.map的遍历过程是怎样的(todo)

  3.2.3map中的key为什么是无序的?

  map在扩容时会触发搬迁。一个bucket中的元素会分散到2个。这个过程不能保证元素的顺序。

  3.2.4map是线程安全的吗?

  不是。如果检测到写标志flags=1则直接panic了。

  3.2.5float类型可以作为map的key吗?

  可以,但是会出现精度丢失问题。float64作为key时,会转成uint64类型,再插入key中。

  3.2.6map如何实现两种get操作

  带comma和不带comma。

  3.2.7如何比较两个map是否相等

  1.都为nil

  2.非空、长度相等,指向同一个map实体对象

  3.相同的key指向value“深度”相等。

  3.2.8可以对map的元素取地址吗?

  不能。

  3.2.9可以边遍历边删除吗?

  同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以的。

  如果存在多个读写同时进行的情况,推荐使用线程安全的sync.map。

四.通道

4.1CSP是什么

不用通过共享内存来通信,而要通过通信来实现共享内存。

大多数编程语言的并发编程模型是基于线程和内存同步访问控制。Go的并发编程模型则用goroutine和channel来替代。goroutine和线程类似,channel则和mutex类似。

4.2通道有哪些应用

1.停止信号

2.定时任务(结合time包,一般有2种做法,实现超时控制、定时任务)

3.解耦生产方和消费方

4.控制并发数(使用缓存通道)

4.3通道的底结构#todo

  4.3.1数据结构

  4.3.2创建过程

  4.3.3接收过程

  4.3.4发送过程

  4.3.5收发数据的本质

4.4通道的关闭过程发生了什么#todo

4.5从一个关闭的通道里仍然能读出数据吗

从一个有缓冲的channel里读数据,当channel被关闭,已然能读出有效值,只有当返回的ok值为false时,读出的数据才是无效的。

如果是无缓冲的呢?

4.6如何优雅的关闭通道

所谓的优雅关闭channel,就是不关闭channel,让GC代劳。

4.7关于通道的happens-before有哪些?

定义:假设事件a和事件b存在happened-before关系,那么a/b完成后的结果也一定要体现这种关系。

由于现代编译器、CPU会做各种优化,包括编译器重排、内存重排等,在并发代码里,happened-before限制就非常重要了。

4.8通道在什么情况下会引发资源泄露

goroutine操作channel后,处于发送或者接收阻塞状态,而channel处于满或空的状态,一直得不到改变,垃圾回收器并不会回收此类资源。

如果一个channel,没有任何goroutine引用,GC会对其进行回收操作,不会引起内存泄露。

4.9通道的操作情况总结

操作

nil chan

closed chan

not nil & not closed

close

panic

panic

正常关闭

读<-ch

阻塞

对应类型的0值

1.正常读取。

2.缓冲型channel为空阻塞

无缓存型channel无发送者时阻塞

写ch<-

阻塞

panic

1.正常写入

2.缓冲型channel满时阻塞

非缓冲型channel无接收者时阻塞

五.接口

5.1Go接口与C++接口有何异同

Go采用的是“非侵入式”不需要显式声明,只需要定义接口定义的函数,编译器就会自动识别。

5.2Go语言与“鸭子类型”的关系

Go不要求类型显式的声明实现了某个接口,只要实现了相关地方法即可。

5.3iface和eface的区别是什么

iface和eface都是Go中描述接口的底层结构体,区别在于iface描述的接口包含方法,而eface则是不包含任何方法的空接口。

 
  

type iface struct { tab *itab //接口的类型以及赋值给这个接口的实体类型《动态类型》 data unsafe.Pointer // 指向接口具体的值《动态值》 } type itab struct { inter *interfacetype // 接口类型 _type *_type // 赋值给这个接口的实体类型 ... } type interfacetype struct { typ _type pkgpath name // 接口的包名 mhdr []imethod // 接口定义的函数列表 }

 
  

type eface struct { _type *_type // 空接口承载的实体类型 data unsafe.Pointer // 具体的值 }

Go语言中各种数据类型都是在_type字段的基础上,增加一些额外的字段来进行管理。

主要包括:类型大小、类型的hash值、内存对齐相关、类型的编号以及GC相关的字段。

5.4值接收者和指针接收者的区别

函数添加一个接收者,它就变成了方法。接收者可以是值接收者,也可以是指针接收者。

实现了接收者是值类型的方法,相当于是自动实现了接收者是指针类型方法。

实现了接收者是指针类型的方法,不会自动生成接收者是值类型的方法。

使用指针作为方法的接收者的理由如下:

1.方法能够修改接收者指向的值

2.避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

5.5如何用interface实现多态

多态,可以让一种类型具有多种类型的能力。

5.6接口的动态类型和动态值是什么

iface包含两个字段:tab是接口表指针,指向类型信息;data是数据指针。分别被称为动态类型和动态值。

 
  

var c coder var g *Gopher c = g fmt.Println(c == nil) // false

5.7接口转换的原理是什么

当判定一种类型是否满足某个接口时,Go将类型的方法集和接口所需的方法集进行匹配。

如果类型的方法集,完全包含接口的方法集,则可认为该类型实现了该接口。

5.8类型转换和断言的区别是什么

类型转换、类型断言本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。

对应类型转换,转换前后的两个类型要相互兼容才行。

因为空接口interface{}没有定义任何函数,因此Go中所有类型都实现了空接口。

当一个函数的形参是interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。

Go语言中的switch仅执行第一个匹配成功的分支,不需要break语句;另外case不需要是常量,也不必是整数。

fallthrough关键字,表示需要执行下一个分支。

5.9如何让编译器自动检测类型是否实现了接口

var _ io.Writer = (*myWriter)(nil)

六.unsafe

  6.1如何利用unsafe包修改私有成员

对于一个结构体,通过offset函数可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的。

结构体被分配一块连续的内存,结构体的地址,也代表了第一个成员的地址。

  6.2如何利用unsafe获取slice和map的长度

  主要通过unsafe.Pointer和uintptr进行转换。

  6.3如何实现字符串和byte切片的零复制转换

 
  

type StringHeader stuct { Data uintptr Len int } type SliceHeader stuct { Data uintptr Len int Cap int }

只需要共享底层Data和Len就可以实现zero-copy。

七.context

7.1context是什么

context是goroutine的上下文,在goroutine中传递上下文信息,包含:取消信号、超时时间、截止时间、k-v等

7.2context有什么作用

使用context的几点建议:

1.不要讲context塞到结构体里,而是作为第一参数,一般命名为ctx 2.不用向函数传入一个含nil属性的context,可以使用todo代替

3.不用把业务参数塞到context中

4.context是并发安全的

7.3如何使用context

1.传递共享的数据

2.定时取消

3.防止goroutine泄露

7.4context底层原理是什么

主要包含Context和Canceler两个接口。

1.context定义了4个方法,都是冪等的

2.Deadline()返回context的截止时间,决定是否进行后续操作

3.Done()返回一个channel,表示context被取消的信号

4.Err()返回一个错误,表示channel被关闭的原因。例如被取消还是超时

5.Value()获取之前设置的key对应的value

源码中有2个类型实现了canceler接口:*cancelCtx、*timerCtx,注意是指针类型。

设计原因主要是:

  1.“取消”操作应该是建议性,而非强制性

  2.“取消”操作应该可传递

  八、错误

  8.1接口error是什么

  Go使用error类型表示错误,是一个接口类型。

  最简单的是errors.New(),如果需要具体的上下文信息,可以使用fmt.Errorf()

  8.2接口error有什么问题

  Go代码里error满天飞,显得非常冗长拖沓。

  8.3如何理解关于error的三句谚语

  1.视错误为值

  处理error的方式分为三种。

  Sentinel errors:哨兵。处理流程停止。最大的问题在于定义error和使用error的包之间建立了依赖关系,容易引起循环调用。(不推荐)

  Error Types:自定义Error类型,在error基础上,附带其他字段,外层调用者需要使用类型断言来判断错误。也存在循环调用的问题。(不推荐)

  Opaque errors:黑盒error。能知道错误发生了,但是无法看到它内部到底是什么,不知道它的具体类型。

  一旦出错,直接返回错误;否则继续后面的流程。

  如果调用者需要判断返回的错误类型,可以判断错误是否具有某种行为,或者说实现了某个接口。

  这样做的好处是:不需要import引用定义错误的包,并且不需要知道error的具体类型,只需要判断它的行为。即:面向接口编程。

  2.检查并优雅的处理错误

  Go1.13之前使用github.com/pkg/errors,使用Wrap可以将一个错误,加上一个字符串,“包装”成一个新的错误。Cause则是反向操作,将里层的错误还原。

  3.只处理一次错误

  避免函数内和函数外的调用者都处理错误。

  8.4错误处理的改进

  Go1.13支持了error包裹(wrapping),fmt.Errorf增加了%w的格式,并且在error包增加了三个函数:errors.Unwrap、errors.Is、errors.As。

  fmt.Errorf使用%w来生成一个嵌套的error

  Unwrap将嵌套的error解析出来

  Is判断err和target是同一类型,或者error嵌套的error有没有和target同一类型。

  As从错误链中找到第一个和target相等的值,并且设置target指向的变量为err。

九、计时器

  9.1Timer底层数据结构为什么用四叉堆而非二叉堆

  四叉堆和二叉堆本质上没有区别,它使得整体上层数更低,且时间复杂度从O(log2N)降到O(log4N)

  9.2Timer曾做过哪些重大的改进

  9.3定时器的使用场景有哪些

  1.固定时间间隔触发

  2.固定时间间隔重复触发

  3.在某个具体时刻触发

  9.4Timer/Ticker的计时功能有多准确

  影响时间准确性的元素

  1.对系统时间的依赖程度:只能依靠操作系统或者时间提供方(通常认为精度在毫秒级)

  2.对运行时的依赖程度:由于运行时组件的存在,这个时间管理的准确性也将或多或少受到一定程度的影响,例如调度器的调度延迟、垃圾回收器的干扰、操作系统对应用程序进行中断产生的延迟等。(当系统出现可感知的延迟时,可以着重调试运行时本身对延迟的影响,如:调度器任务的数量、Timer/Ticker的密度和垃圾回收器的压力)

  9.5定时器的实现还有哪些方式

  定时器可以使用链表、堆、红黑树等数据结构,也可以使用时间轮实现。流行的高效定时器有三种:Go使用的堆结构、nginx使用的红黑树、linux kernel使用的时间轮。

  十.反射

  10.1反射是什么

  Go语言提供了一个机制,在允许时更新变量和检查它们的值、调用它们的方法,但是在编译时并不知道这些变量的类型,这就是反射机制。

  10.2什么情况下需要使用反射机制

  使用反射的常用场景有以下两种:

  1.不能明确接口调用哪个函数,需要根据传入的参数在运行时决定。

  2.不能明确传入函数的参数类型,需要在运行时处理任意对象。

  不推荐使用反射的原因:

  1.代码经常难以阅读

  2.编译器能提前发现一些类型错误,但对反射代码无能为力。

  3.性能影响较大

  10.3Go语言如何实现反射

  反射是通过接口的类型信息实现的,建立在类型的基础上。

  反射主要与interface{}有关。

  接口变量,可以存储任何实现了接口定义的所有方法的变量。

  GO语言reflect包里定义了一个接口和一个结构体,即reflect.Type和reflect.Value,它们提供很多函数来获取存储在接口里的类型信息。

  reflect.Type提供关于类型相关的信息,和_type关联比较紧密。

  reflect.Value则结合_type和data两者,因此可以获取并改变类型的值。

  reflect包提供了两个基础的关于反射的函数,来获取上述的接口和结构体:TypeOf、ValueOf

  TypeOf函数用来提取一个接口中值的类型信息。

  ValueOf函数返回一个结构体变量,包含类型信息以及实际值。

  反射三大定律

  1.接口类型变量,可以转化成反射类型对象(反射类型对象,指reflect.Type、reflect.Value)

  2.反射类型对象,可以转化成接口类型变量

  3.如果想要操作原变量,反射变量Value必须要持有原变量的地址才行。

  10.4如何比较两个对象是否完全相同

  Go语言中提供了DeepEqual()函数进行比较。参数是两个interface。

  如果是不同的类型,即使是底层类型相同,相应的值也相同,那么两者也不是深度相等。

  10.5如何利用反射实现深度拷贝

  浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象中指针类型的字段还是共享同一块内存。深拷贝:创造一个内容完全相同的对象,新对象与原对象不共享内存,修改新对象不影响原对象。

  实现深度拷贝可以存在多种形式,最简单、最安全也是最容易的方式是使用json.Marshal/Unmarshal。但是涉及到序列化和反序列化,性能较差。通过反射可以实现更高效的深度复制。

  十一.同步模式

    11.1等待组sync.Waitgroup的原理是什么

      sync.Waitgroup可以达到并发goroutine的执行屏障的效果。

      当需要对一个并行执行的代码块引入等待条件时,便可以使用Add操作来产生同步记录;而当不再需要等待条件时,则可在并发代码块中使用Done操作来促成同步屏障条件的达成。

      Waitgroup的内部结构非常简单,内部由3个uint32来对并发的goroutine进行不同目的的计数,分别是运行计数、等待计数和信号计数。并通过state()函数来消除在高层实现上的差异,返回状态(运行计数和等待计数)和信号(信号计数)。

    11.2缓存池sync.pool

    大量重复的创建很多对象,会引起GC的工作量飚升,这时可以使用sync.Pool来缓存对象,减轻对GC的消耗。

    11.3并发安全散列表sync.Map

    Sync.Map是线程安全的,读取、插入、删除也都保持着常数级的时间复杂度。

    Sync.Map的零值是有效的,并且零值是一个空的map,它在第一次使用后,不允许被复制。

    Sync.Map使用非常简单,和普通map相比,仅遍历的方式略有不同。

      sync.Map数据结构:

      mu Mutext 保护read和dirty字段

      read是atomic.Value类型,可以并发地读。

      dirty是一个非线程安全的原始map。

  十二、调度机制

    12.1goroutine和线程有什么区别

  1.内存消耗:goroutine的栈内存消耗为2kb,创建线程需要1MB栈内存

  2.创建和销毁:线程的创建和销毁消耗巨大,是内核级的,通常使用线程池提高复用;goroutine由go runtime负责管理,创建和消费的消耗非常小,是用户级的。

  3.切换:线程切换需要保存各种寄存器,以便恢复;goroutine切换只需保存三个寄存器:PC、Stack Pointer和BP。

    12.2 Go sheduler是什么

    Go程序的执行有两个层面,Go program和Runtime。

  Go schedule是Go运行时最重要的部分。Runtime维护所有的goroutine,并通过schedule进行调度。

  对操作系统而言,只有线程的概念,并不感知goroutine。

  有3个基础的结构体来实现goroutine的调度:G、P、M。

  G:代表goroutine,表示goroutine栈的一些字段等。

  M:代表内核线程,包含正在运行的goroutine等字段

  P:代表一个虚拟的Processor,维护一个处于Runnable状态的goroutine队列。M需要获得P才能运行G。

  还有一个核心结构体:sched,总览全局,负责整个调度器的运行。

  Runtime起始时,会启动一些G:垃圾回收的G,执行调度的G,运行用户代码的G;并且创建一个M用来开始G的运行。

  Go schedule会启动一个后台线程sysmon,来检测长时间(超过10ms)运行的goroutine,将其“停靠”到global runqueues。

  初始化时,Go程序会有一个G,G在M上得到执行,内核线程是在CPU核心上调度,G则在M上进行调度。

  此外,还有两个比较重要的组件:全局可运行队列(GRQ)和本地可运行队列(LRQ)。

  和线程类似,goroutine的状态也有3种:Waiting(等待状态)、Runnable(就绪状态)、Executing(运行状态)。

  12.3goroutine的调度时机有哪些

  4种情形下,goroutine可能会发生调度,但也并不是一定发生。分别是:使用go关键字、GC、系统调用、内存同步访问。

  12.4 M:N模型是什么

  Go runtime会在程序启动后,“按需”创建N个线程,之后创建M个goroutine会依附在N个线程上执行。

  12.5 工作窃取是什么

  Go schedule的职责就是将所有处于runnable的goroutine均匀调度到在P上运行的M。

  当一个P发现自己的LRQ已经没有G时,会从其他P“偷”一些G来运行,这被称为“工作窃取”。

  Go schedule每一轮调度要做的工作,就是找到处于runnable的goroutine,并执行它。顺序如下:

  1.从LRQ中找

  2.从GRQ中找

  3.从netpoll里找

  4.从其他P偷取

  12.6 GPM底层数据结构是怎样的

  G:取的是goroutine的首字母,主要保持goroutine的一些状态信息,以及CPU的一些寄存器的值。G关联了两个比较重要的结构体,stack表示goroutine运行时的栈,gobuf保存PC、SP等寄存器的值。

  M:取的是machine的首字母,代表一个工作线程,或者说系统线程。G需要调度到M上才能运行,M是真正工作的实体。m结构体保存了M自身需要使用的栈信息、正在M上执行的G信息、与之绑定的P信息等。

  P:取processor的首字母,为M的执行提供“上下文”,保存M执行G时的一些资源,例如本地可运行G队列,memeory cache等。一个M只有绑定P才能执行goroutine,当M被阻塞时,整个P会被传递给其他M。

  12.7schedule的初始化过程是怎样的

  Go scheduler在源码中的结构体为schedt,保存调度器的状态信息、全局的可运行G队列等。

  12.9g0栈和用户栈如何被切换

  g0栈用于执行调度器的代码,它选择一个可运行的goroutine,之后跳转到执行用户代码的地方。如何跳转,这中间涉及栈和寄存器的切换。函数调用和返回主要靠的也是CPU寄存器的切换,goroutine的切换和此类似。

  12.13M如何找工作

  1.从本地队列找、2.定期从全局队列找、最后从别的P偷取。

  12.14系统监控sysmon后台监控线程做了什么

  1.抢占处于系统调用的P,让其他M接管它,以运行其他的goroutine

  2.将运行时间过长的goroutine调度出去,给其他goroutine运行的机会

  十三、内存分配机制

    13.1管理内存的动机是什么,通常涉及哪些组件

    内存管理的动机:性能要求、解放程序员

    内存管理运行时的组件:

    1.页分配器:从操作系统申请内存

    2.对象分配器:为用户程序分配内存

    3.垃圾回收期:回收用户程序所分配的内存

    4.拾荒器:向操作系统归还申请的内存

    从运行时对内存的管理角度来看,内存有4种状态:空状态(None)预留态(Reserved)准备态(Prepared)以及就绪态(Ready)

    13.2Go语言中的堆和栈概念与传统意义上的堆和栈有什么区别

    运行时中的mheap结构存储了整个go堆的管理状态,涉及页分配器和对象分配器。从两个分配器视角可以将内存考虑为两种不同的粒度单位。

    页分配器:堆是按照连续的页进行管理。

    对象分配器:以跨度(span)进行管理。一个跨度以mspan结构进行存储,每个跨度可以存储多个分配的对象,并由多个连续的页组成。

    13.3对象分配器是如何实现的

    分配的基本策略:顺序分配和自由表分配两大策略。

    顺序分配:直接从一段连续空间的一端开始,按需逐次将内存分配给用户程序。(Go堆内存使用)

    自由表分配:使用链表结构来维护未分配的内存,进行串联管理。(运行时对象所在的非托管内存使用)

    对象分配的缓存分为两个:

    1.本地跨度缓存:不需要分配新的跨度 2.中枢跨度缓存:需要分配新的跨度

    _type是Go类型的实现,通过size属性可以获得该类型对应的大小。对象分配的流程分为三种基本情况:

    1.微对象分配:针对小于16B的对象分配请求

    2.小对象分配:针对大小介于16B和32KB之间的分配请求

    3.大对象分配:针对大于32KB的对象分配请求

十四、垃圾回收机制

14.1垃圾回收的认识

垃圾回收,是一种自动内存管理的机制。

当程序向操作系统申请的内存不再需要时,垃圾回收主动将其回收,并供其他代码申请内存复用,或者将其归还给操作系统,这个过程称为垃圾回收。

垃圾回收器的执行过程被划分为两个半独立的组件:

1.赋值器:代指用户态的代码,对垃圾回收器而言,用户态的代码只修改对象之间的引用关系。

2.回收器:负责执行垃圾回收的代码。

垃圾回收器在标记过程中,最先检查的对象包括:全局变量、执行栈、寄存器。

GC算法的存在形式,可以归结为:追踪和引用计数这两种形式的混合运用。

追踪式GC:从根对象出发,根据引用关系逐步扫描,确定保留的对象,从而回收所有可回收的对象。

引用计数式GC:每个对象自身包含一个被引用的计数器,计数器归零时自动回收。

三色标记法是什么?

关键是理解三色抽象以及波面推进两个概念。

三色抽象规定了三种不同类型的对象,并以不同颜色相称。

1.白色对象:未被回收器访问,初始颜色,回收结束后,均不可达。

2.灰色对象:已被回收器访问,可能指向白色对象。

3.黑色对象:已被回收器访问,所有字段已被扫描,黑色对象中任何一个指针都不可能指向白色对象。

STW是什么?

stop the world,也可以是start the world。从stop the world到start the world的这段时间间隔。

垃圾回收过程,为了保证实现的正确性,防止无止境的内存增长。

你可能感兴趣的:(go,golang,面试,java)