【Go】Go语言的介绍

1. 开发速度

使用了更加智能的编译器,简化了解决依赖的算法,最终提供了更快的编译速度。编译Go程序时,编译器只会关注那些直接被引用的库,而不是像Java、C和C++那样,要遍历依赖链中所有依赖库。因此Go程序可以在1秒内编译完。

2. 并发

  • goroutine: 可以与其他goroutine并行执行的函数,同时也会与主程序并行执行。
  • 通道:保证同一时刻只会有一个goroutine修改数据。

3. 类型系统

  • 类型简单
    有类似 int 和string 这样的内置类型,还支持用户定义的类型。传统的语言在自定义类型可以使用继承来扩展数据结构。Go语言则倾向于构建小类型,进而组合成更大的类型
  • Go接口对一组行为建模
type Reader interface{
	Read(p []byte)(n int, err error)  //输入是p, 返回int类型和可能的错误
}

4. 内存管理

4.1 栈内存

  • Go 栈内存的作用:局部变量记录在协程栈里,参数传递也是通过栈内存传递,返回值也是通过栈内存传递。
  • 携程栈位置:栈内存是从堆内存申请的,初始空间为2KB,Go携程栈位于Go堆内存上,而Go堆内存位于操作系统的虚拟内存上。
  • 协程栈的结构
package main
 
func sum(a, b int) int {
    sum := 0
    sum = a + b
    return sum
}
 
func main() {
    a := 3
    b := 5
    fmt.Println(sum(a, b))
}

在这里插入图片描述
main()调用sum(a,b int)时需要传递参数,在栈帧里面参数的传递顺序是反的,传递参数时在自己的栈帧里开辟空间记录下要传递的参数,因为Go采用的是值传递。然后会记录sum(a,b int)返回后的指令,也就是上述代码中fmt.Println()。

运行sum(a,b int)函数时会首先在函数的栈帧中记录调用者的栈基址,意思就是当函数返回后需要返回到哪一个栈帧。当代码运行到sum = a + b,sum()函数会到main()函数的栈帧中寻找a、b的值,sum()函数返回时,会将返回值写回它的调用者的栈内存中预留的返回值空间,也就是上图中的sum函数返回值。

  • 协程栈填满的原因
    协程栈记录了函数的执行路径(函数调用信息)和局部变量信息,所以协程栈被填满的原因就是:函数调用太深或者局部变量太大。
    局部变量太大导致协程栈空间不足,局部变量会逃逸到堆上。
    函数调用太深导致协程栈空间不足,会进行栈扩容
    协程在函数调用前会调用morestack判断栈空间是否足够,在调用函数之前要给下一个函数开辟新的栈空间,必要时对栈进行扩容,栈扩容的策略有分段栈和连续栈。

  • 栈扩容策略
    1) 分段栈:Go1.13之前栈扩容使用分段栈策略,如果栈空间不足调用newstack创建一个新的栈空间,但是新创建的栈空间和原来的栈空间不连续,协程多个栈空间之间用双向链表的形式串联,通过指针找到这些栈空间。
    优点:没有空间浪费,能够按需分配内存,及时减少内存占用
    缺点:栈空间不连续,栈指针会在不连续的空间跳转,协程的栈空间处于填满状态时,新的函数调用会触发栈扩容,会给新的函数开辟一个不连续的栈空间,当这个函数返回后新开辟的栈空间使用完毕就会触发栈收缩,此时函数调用特别频繁,那么会导致频繁的扩容和栈收缩,增加gc压力。2
    2) 连续栈:Go1.13之后栈扩容为连续栈,解决了分段栈开辟的栈空间不连续问题。
    连续栈策略在协程的栈空间不足时,调用newstack创建一块为原来大小两倍的栈空间,然后调用copystack将原来空间中所有内容复制到新开辟的栈空间中,将指向旧栈对应变量的指针重新指向新栈(想同变量在栈扩容前后地址发生变化),最后调用stackfree销毁并回收原来的栈空间。因此连续栈的缺点就是栈扩容时开销大,优点就是栈空间连续。
    连续栈收缩情况:当栈的使用率不足1/4的时候,会收缩为原来的1/2

4.2 堆内存

  • 基本概念
  • 64位机器上Go程序启动时,首先会向操作系统申请一块大小为64M的虚拟内存单元,称为heapArena,最多可以有2的22次方哥内存单元heapArena,所有的heapArena组成了Go的堆内存。假设物理内存为64G,操作系统会给每个进程分配256T的虚拟内存。
  • 分级分配
  • mspan
    mspan被划分为67种,代表着67种内置不同大小的内存块的mspan,每种mspan为N个相同大小的内存块class,可以满足各种对象的内存分配,而且每种mspan的大小也可能不相同。其中四种mspan示意图如下:
    在这里插入图片描述
    在这里插入图片描述
    class:代表每种mspan编号。
    bytes/obj:每种mspan中内存块的大小,可以理解为上图中每个格子的大小。
    byte/span:每种mspan的大小,占用堆的字节数。
    objects:每种mspan中包含多少个小内存块,可以理解为上图中每个mspan的格子数量。

mspan数据结构如下:

type mspan struct {
	next *mspan                 // next span in list, or nil if none
	prev *mspan                 // previous span in list, or nil if none
	startAddr uintptr           // 起始地址
	npages    uintptr           // span中包含的页数
	nelems uintptr              // 上图span中的class数量,代表可分配的内存块数量
    allocCount  uint16          // 已分配的内存块(格子)个数
    spanclass   spanClass       // span中内存块的规格
    elemsize    uintptr         // span中每一个内存块大小
	allocBits  *gcBits          // 分配位图,代表mspan中每一个内存块的分配情况
	gcmarkBits *gcBits          // mspan中每一个内存块的标记情况
    ......
}
  • mcentral
    为了管理这么多的mspan,于是有了mcentral,每个mcentral用于管理一种特定规格的mspan mcentral一共有136种。按理说mcentral是管理mspan的,mspan有67种,为什么mcentral却有136种呢,其实mspan又分为需要gc扫描和不需要gc扫描的,mspan还有一个class0,用于大对象分配。
type mcentral struct {
	spanclass spanClass
	partial [2]spanSet // list of spans with a free object,空闲块span链表
	full    [2]spanSet // list of spans with no free objects,没有空闲块的span链表
}

mcentral数据结构中并没有锁,协程申请内存时需要向mcentral申请mpsan,此时会调用cacheSpan() mspan,返回值是mspan,申请到的mspan会加入到另一个结构中供协程使用,而调用cacheSpan() *mspan的过程中是需要加锁的,在高并发的场景下,多个协程并发的申请锁,锁的频繁加锁解锁的开销非常大,因此就需要刚才说的另一个结构来缓冲这种开销压力。

  • mcache
    相当于一个协程的本地队列,里面存储着从mcentral申请的mspan,协程向mcentral申请mspan需要加锁,为了避免多个协程申请内存不断加锁,于是引入了mcache,这个思想就是参考了GMP模型的本地写成队列。
    mcentral是全局资源,为多个协程服务,当协程内存不足时,会向mcentral申请。

数据结构如下:

type mcache struct {
    ...
	alloc [numSpanClasses]*mspan // 保存着申请到的mpsan, numSpanClasses = 68 << 1
    ...
}

mcache在初始化时是没有任何mspan的,在使用过程中会动态的从mcentral中申请兵保存至alloc 中,这样协程需要申请内存时,就直接从自己的本地缓存中获取mspan,避免了加锁。
在这里插入图片描述

  • 内存分配过程
    1 分配逻辑
    Go分配内存前,会按照对象的大小进行不同的分配,分配逻辑如下:
  1. 0-16字节不包含指针的对象:Tiny微小对象分配,从mcache中拿到一个2级的mspan,将多个微对象合并成一个16B对象存入2级mspan。
  2. 0-16字节包含指针的对象和16B-32KB的对象:正常对象分配至mspan。
  3. 32KB以上的大对象:使用0级mspan分配,0级mspan没有固定大小,专为大对象分配。

2申请内存过程

  1. 获取当前协程的私有缓存mcache。
  2. 根据申请内存的大小计算出合适的mspan编号。
  3. 从mcache的成员alloc中查询可用的mspan。
  4. 如果mcache中没有可用的mspan,则从mcentral中申请一个新的mspan加入mcache中。
  5. 如果mcentral中也没有可用的mspan,则从mheap中获取一个新的mspan加入mcentral。
  6. 如果mheap也没有可用内存,那么就会向操作系统再次申请新的内存块heapArena供Go程序使用。

5. 垃圾回收GC

5.1垃圾回收算法

  • 引用计数
  • 标记清除
  • 分代收集

5.2 根对象

根对象是在垃圾回收的过程中最先被检查的对象,包括:

  1. 全局变量:全局变量存在于程序整个生命周期。
  2. 协程栈中的对象,或者从栈上逃逸到堆上的对象。
  3. 被寄存器中的指针引用的对象。

5.3 三色标记法

三色只是为了方便描述而抽象出来的一种说法,实际上并没有颜色,所说的三种颜色指的是对象的三种状态。

  • 白色:对象未被标记,在mspan中的gcmarkBits成员对应的位为0。
  • 灰色:等待被表记的对象
  • 黑色:对象被标记,在mspan中的gcmarkBits成员对应的位为1。

5.4 STW

STW全称为stop the world,意思是暂停程序的运行,在垃圾回收的过程中,如果不暂停程序的运行,指针传递会引起内存引用关系变化,如果错误的回收了还在使用的内存,带来的结果可能是灾难性的。例如下图中,A、B、C、D、E、F都被标记为黑色,而G、H为白色,那么G、H将会被回收,如果没有STW,此时程序继续运行,黑色对象突然又引用了G、H,而扫描过程已经结束了,那么就会错误的回收G、H这两个还在使用的对象。

在这里插入图片描述

5.5 混合屏障

由于STW对程序的执行影响较大,对于一些应用是不可接受的,特别是WEB应用,所以 Go也不断地在优化GC,提出了混合屏障,使得程序和GC同时进行。

  • 插入屏障:插入屏障就是在GC扫描的过程中,对于新增引用的对象(上图中的C对象),会立即置为灰色,保证其不会被错误清除。
  • 删除屏障:删除屏障就是在GC扫描的过程中,对于引用被移除的对象(上图中的C对象),会立即置为灰色,保证其不会被错误清除。

6. 内存逃逸

在函数中申请的新对象:

  • 如果函数外部没有引用,优先放入栈中;
  • 如果函数外部存在引用,则放入堆中;
  • 如果分配在协程栈上,那么函数执行结束后会自动回收内存。
  • 如果是分配在堆上,那么这个对象由GC进行回收。

由于栈上分配内存比在堆中分配内存的效率更高,因为栈上分配的内存不需要GC处理,所以逃逸分析的目的就是把那些不需要分配到堆上的变量直接分配到栈上,提高程序效率。

  1. 指针逃逸
  2. 空接口逃逸
  3. 大对象逃逸:当协程栈空间不足以存放当前对象或者无法判断当前切片的长度时,会将对象分配到堆中,在64位机器上超过64KB的对象会发生逃逸。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值