golang基本原理
# Golang基本原理
# 局部变量和全局变量
全局变量存放在静态存储区,位置是固定的。 局部变量在栈空间,栈地址是不固定的
全局存储区(静态存储区):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。
常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改
# 栈,堆,自由存储区
栈: 就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区。里面的变量通常是局部变量、函数参数等。 堆: 就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。 自由存储区: 就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
# go中的make和new
make函数是无可替代的,我们在使用slice、map以及channel的时候,还是要使用make进行初始化,然后才才可以对他们进行操作
# go启动的过程
启动一个 Go 程序时,首先要经过操作系统的加载,通过可执行文件中 Entry point address 记录的地址,找到 go 程序启动入口: _rt0_amd64 -> rt0_go。rt0_go 中 先进行了 go 程序的 runtime 的初始化,其中包括:调度器,栈,堆内存空间初始化,垃圾回收器的初始化,最后最后通过newproc和mstart调度执行runtime.main,完成一系列初始化过程,再然后才是执行用户的主函数
# goroutine内存泄漏
goroutine由于channel的读/写端退出而一直阻塞,导致goroutine一直占用资源,而无法退出 goroutine进入死循环中,导致资源一直无法释放
解决方法:创建goroutine时就要想好该goroutine该如何结束 使用channel时,要考虑到channel阻塞时协程可能的行为 要注意平时一些常见的goroutine leak的场景,包括:master-worker模式,producer-consumer模式等等。
# Go 在什么时候会抢占 P?
如果存在系统调用超时:存在超过 1 个 sysmon tick 周期(至少 20us)的任务,则会从系统调用中抢占 P。
如果没有空闲的 P:所有的 P 都已经与 M 绑定。需要抢占当前正处于系统调用之,而实际上系统调用并不需要的这个 P 的情况,会将其分配给其它 M 去调度其它 G。
如果 P 的运行队列里面有等待运行的 G,为了保证 P 的本地队列中的 G 得到及时调度。而自己本身的 P 又忙于系统调用,无暇管理。此时会寻找另外一个 M 来接管 P,从而实现继续调度 G 的目的。
# goroutine阻塞的原因
通道(Channel)。 垃圾回收(GC)。 休眠(Sleep) 锁等待(Lock)。 抢占(Preempted)。 IO 阻塞(IO Wait) 其他,例如:panic、finalizer、select 等。
# Go sync.map 和原生 map 谁的性能好,为什么?
sync.Map的性能高体现在读操作远多于写操作的时候。 极端情况下,只有读操作时,是普通map的性能的44.3倍。 反过来,如果是全写,没有读,那么sync.Map还不如加普通map+mutex锁呢。只有普通map性能的一半。
建议使用sync.Map时一定要考虑读定比例。当写操作只占总操作的<=1/10的时候,使用sync.Map性能会明显高很多。
# go内存逃逸总结:
- 函数返回指针型数据
- 切片初始化的空间超过限制或者不确定大小
- 使用interface{}
在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出。 发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。 在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。 因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。 在interface类型上调用方法,在Interface调用方法是动态调度的,只有在运行时才知道。
# slice,len,cap,共享,扩容
append函数,因为slice底层数据结构是,由数组、len、cap组成,所以,在使用append扩容时,会查看数组后面有没有连续内存快,有就在后面添加,没有就重新生成一个大的素组
# map如何顺序读取
map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。
# mutex 饥饿模式
Go 采用了饥饿模式。即通过判断队头 Goroutine 在超过一定时间后还是得不到资源时,会在 Unlock 释放锁资源时,直接将锁资源交给队头 Goroutine,并且将当前状态改为饥饿模式。
# mutex 自旋
一定条件后,mutex 会让当前的 Goroutine 去空转 CPU,在空转完后再次调用 CAS 方法去尝试性的占有锁资源,直到不满足自旋条件,则最终会加入到等待队列里
自旋的条件如下: 还没自旋超过 4 次 多核处理器 GOMAXPROCS > 1 p 上本地 Goroutine 队列为空 内存空间主要包括:堆,栈
# Go内存管理过程
三个组件:用户程序(Mutator)、分配器(Allocator)、收集器(Collector) 首次适应:第一个满足的大小要求的内存块 循环首次遍历:从上一次遍历的地方开始首次适应 最优适应:遍历整个链表,选择最优解 隔离适应:分割成多个链表,每个链表的内存块大小不一样,但是同链表上内存块大小一致,申请时先选择链表,在选择链表上的内存块 go defer(for defer),先进后出,后进先出 Go中的map和slice是引用类型,应该通过值传递 MVCC的目的就是多版本的并发控制,在数据库中的实现,就是为了解决读-写冲突的问题,它的实现原理主要是依赖记录中的 3个隐式字段、undo日志、read view 来实现的
# 三色标记法 GC
- 初始状态全部都是白色
- 从root根出发扫描所有根对象,将其标为灰色
- 分析灰色是否引用了其他对象,如果没有则标记为黑色;如果有,将其引用标记为灰色,其自身标记为黑色。
- 重复步骤3,直至灰色对象队列为空。
- 将白色部分的对象视为垃圾,进行回收
# 调优gc
减少对象分配,合理重复利用 避免string与[]byte转化 两者发生转换的时候,底层数据结结构会进行复制,因此导致 gc 效率会变低 少用+连接string Go里面string是最基础的类型,是一个只读类型,针对他的每一个操作都会创建一个新的string。 如果是少量小文本拼接,用 “+” 就好;如果是大量小文本拼接,用 strings.Join;如果是大量大文本拼接,用 bytes.Buffer。