做电影字幕的网站,鄂尔多斯市东胜区城市建设局网站,微信公众号平台入口官网,免费空间访客一.逃逸分析
1.1逃逸分析是什么#xff1f;
逃逸分析#xff0c;主要是Go编译器用来决定变量分配在堆或者栈的手段。
区分于C/C手动管理内存分配#xff0c;Go将这些工作交给了编译器。
1.2逃逸分析有什么作用
解放程序员。程序员不需要手动指定指针分配内存。
灵活的…一.逃逸分析
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。关键字段有 Bbuckets数组的长度的对数即buckets数组的长度为2^Bbucket里面存储了key和valuebucket是一个指针指向的是一个结构体bmap。 bmap就是人们常说的“桶”桶里最多装8个key,value。 这些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.写标志flags1时说明有其他协程在执行“写”操作程序会panic说明map不是协程安全的。 2.map的扩容是渐进式的定位元素到某个bucket后需要确保这个bucket对应的老bucket已经完成了迁移过程老bucket中的key会被分散到2个新bucket 3.定位元素放置的位置时准备两个指针 inserti和insertk分别指向第一个空的tophash、第一个空闲的cell槽 4.如果触发扩容则查找定位key的过程会重新执行一次。 5.最后会更新元素的值如果是插入的话map的count字段值加1hasWriting清零。 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是线程安全的吗 不是。如果检测到写标志flags1则直接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程序会有一个GG在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的这段时间间隔。
垃圾回收过程为了保证实现的正确性防止无止境的内存增长。