make和new的区别
相同点
都是Go语言中用于内存申请的关键字
底层都是通过mallocgc申请内存
不同点
make返回的是复合结构体本身,new返回的是指向变量内存的指针
make只能为channel,slice,map申请内存空间
func new(Type) *Type,new会根据变量类型返回一个指向该类型的指针
newobject的底层调用mallocgc在堆上按照typ.size的大小申请内存,因此new只会为结构体Student申请一片内存空间,不会为结构体中的指针age申请内存空间
对于结构体指针,工作中一般使用s:=&Stuent{age:=new(int)}的方式赋值
new(T) 为一个T类型新值分配空间并将此空间初始化为T的零值,返回的是新值的地址,也就是T类型的指针*T,该指针指向T的新分配的零值。
make(T) 返回的初始化的T,只能用于slice,map,channel。
golang的内存管理
Go语言的内存分配器采用了跟 tcmalloc 库相同的实现,是一个带内存池的分配器,底层直接调用操作系统的 mmpa 等函数。
作为一个内存池,它的基本部分包括以下几部分:
- 它会向操作系统申请大块内存,自己管理这部分内存
- 它是一个池子,当上层释放内存时它不实际归还给操作系统,而是放回池子重复利用
- 内存管理中必然会考虑的就是内存碎片问题,如果尽量避免内存碎片,提高内存利用率,像操作系统中的首次适应,最佳适应,最差适应,伙伴算法都是一些相关的知识背景。
另外,Go语言是一个支持 goroutine 这种多线程的语言,所以它的内存管理系统必须要考虑在多线程下的稳定性和效率问题。
很自然的做法就是每条线程都有自己的本地的内存,然后有一个全局的分配链,当某个线程中的内存不足后就向全局分配链中申请内存。这样就避免了多线程同时访问共享变量的加锁。
在避免内存碎片方面,大块内存直接按页为单位分配,小块内存会切成各种不同的固定大小的块,申请做任意字节内存时会向上取整到最接近的块,将整块分配给申请者以避免随意切割。
小于32K为小对象,大对象直接从全局控制堆上以页(4k)为单位进行分配,也就是说大对象总是以页对齐的。
调用函数传入结构体时应该传值还是指针
结构体是值类型,如果需要修改结构体对象传指针,否则传值
线程有几种模型
内核线程模型
用户级线程模型
混合型线程模型
goroutine的原理
基于CSP并发模型开发了GMP调度器
G(Goroutine) : 每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数
M(Machine): 对OS内核级线程的封装,数量对应真实的CPU数(真正干活的对象).
P (Processor): 逻辑处理器,即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过 GOMAXPROCS()来设置,默认为核心数。
在单核情况下,所有Goroutine运行在同一个线程(M0)中,每一个线程维护一个上下文(P),任何时刻,一个上下文中只有一个Goroutine,其他Goroutine在runqueue中等待。一个Goroutine运行完自己的时间片后,让出上下文,自己回到runqueue中。当正在运行的G0阻塞的时候(可以需要IO),会再创建一个线程(M1),P转到新的线程中去运行。
当M0返回时,它会尝试从其他线程中“偷”一个上下文过来,如果没有偷到,会把Goroutine放到Global runqueue中去,然后把自己放入线程缓存中。 上下文会定时检查Global runqueue。
goroutine的优势
上下文切换代价小:从GMP调度器可以看出,避免了用户态和内核态线程切换,所以上下文切换代价小
内存占用少:线程栈空间通常是2M,Goroutine 栈空间最小2K;
goroutine什么时候会发生阻塞
channel 在等待网络请求或者数据操作的IO返回的时候会发生阻塞
发生一次系统调用等待返回结果的时候
goroutine进行sleep操作的时候
GPM模型中goroutine有哪几种状态
有9种状态
_Gidle:刚刚被分配并且还没有被初始化
_Grunnable:没有执行代码,没有栈的所有权,存储在运行队列中
_Grunning:可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P
_Gsyscall:正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上
_Gwaiting:由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上
_Gdead:没有被使用,没有执行代码,可能有分配的栈
_Gcopystack:栈正在被拷贝,没有执行代码,不在运行队列上
_Gpreempted:由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒
_Gscan:GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在
去抢占 G 的时候,会有一个自旋和非自旋的状态
每个线程/协程占用多少内存
线程一般是2M,协程一般是2K
如果goroutine一直占用资源怎么办?PMG模型是怎么解决这个问题
如果有一个goroutine一直占用资源的话,GMP模型会从正常模式转为饥饿模式,通过信号协作强制处理在最前的 goroutine 去分配使用
如果若干线程中一个线程OOM会发什么什么?如果是goroutine呢?
如果线程发生OOM,也就是内存溢出,发生OOM的线程会被kill掉,其它线程不受影响。
go中的内存泄漏一般都是goroutine泄露,就是goroutine没有被关闭,或者没有添加超时控制,让goroutine一只处于阻塞状态,不能被GC。
暂时性内存泄露
获取长字符串中的一段导致长字符串未释放
获取长slice中的一段导致长slice未释放
在长slice新建slice导致泄漏
永久性内存泄露
goroutine永久阻塞而导致泄漏
time.Ticker未关闭导致泄漏
不正确使用Finalizer导致泄漏
如果若干个goroutine其中一个panic会发生什么
如果协程A发生了panic,协程B会因为协程A的panic而挂掉
defer可以捕获到goroutine的子goroutine的panic吗
无法在父协程中捕获子协程的panic
反射实例
1 | package main |
golang的锁机制,mutex的锁有哪几种模式
正常模式
在正常模式下,锁的等待者会按照先进先出的顺序获取锁。但是刚被唤起的 Goroutine 与新创建的 Goroutine 竞争时,大概率会获取不到锁,为了减少这种情况的出现,一旦 Goroutine 超过 1ms 没有获取到锁,它就会将当前互斥锁切换饥饿模式,防止部分 Goroutine被”饿死”。饥饿模式
在饥饿模式中,互斥锁会直接交给等待队列最前面的 Goroutine。新的 Goroutine 在该状态下不能获取锁、也不会进入自旋状态,它们只会在队列的末尾等待。如果一个 Goroutine 获得了互斥锁并且它在队列的末尾或者它等待的时间少于 1ms,那么当前的互斥锁就会切换回正常模式。
与饥饿模式相比,正常模式下的互斥锁能够提供更好地性能,饥饿模式的能避免 Goroutine 由于陷入等待无法获取锁而造成的高尾延时。
程序的垃圾是怎么产生的吗?
程序在内存上被分为堆区、栈区、全局数据区、代码段、数据区五个部分。对于C++等早期编程语言栈上的内存由编译器管理回收,堆上的内存空间需要编程人员负责申请与释放。在Go中栈上内存仍由编译器负责管理回收,而堆上的内存由编译器和垃圾收集器负责管理回收,给编程人员带来了极大的便利性。
垃圾是指程序向堆栈申请的内存空间,随着程序的运行已经不再使用这些内存空间,这时如果不释放他们就会造成垃圾也就是内存泄漏。
GC的实现
Go1.3使用的是标记清除法,分下面四步进行
进行STW(stop the worl即暂停程序业务逻辑),然后从main函数开始找到不可达的内存占用和可达的内存占用
开始标记,程序找出可达内存占用并做标记
标记结束清除未标记的内存占用
结束STW停止暂停,让程序继续运行,循环该过程直到main生命周期结束
Go1.5三色标记法
三色标记算法将程序中的对象分成白色、黑色和灰色三类。白色对象表示暂无对象引用的潜在垃圾,其内存可能会被垃圾收集器回收;灰色对象表示活跃的对象,黑色到白色的中间状态,因为存在指向白色对象的外部指针,垃圾收集器会扫描这些对象的子对象;黑色对象表示活跃的对象,包括不存在引用外部指针的对象以及从根对象可达的对象。
三色标记法分五步进行
将所有对象标记为白色
从根节点集合出发,将第一次遍历到的节点标记为灰色放入集合列表中
遍历灰色集合,将灰色节点遍历到的白色节点标记为灰色,并把灰色节点标记为黑色
循环这个过程
直到灰色节点集合为空,回收所有的白色节点
Go1.8 三色标记+混合写屏障
基于插入写屏障和删除写屏障在结束时需要STW来重新扫描栈,所带来的性能瓶颈,Go在1.8引入了混合写屏障的方式实现了弱三色不变式的设计方式,混合写屏障分下面四步
GC开始时将栈上可达对象全部标记为黑色(不需要二次扫描,无需STW)
GC期间,任何栈上创建的新对象均为黑色
被删除引用的对象标记为灰色
被添加引用的对象标记为灰色
GC触发的条件
触发GC有俩个条件,一是堆内存的分配达到控制器计算的触发堆大小,初始大小环境变量GOGC,之后堆内存达到上一次垃圾收集的2倍时才会触发GC。二是如果一定时间内没有触发,就会触发新的循环,该触发条件由runtime.forcegcperiod变量控制,默认为2分钟。
什么是内存逃逸
在程序中,每个函数块都会有自己的内存区域用来存自己的局部变量(内存占用少)、返回地址、返回值之类的数据,这一块内存区域有特定的结构和寻址方式,寻址起来十分迅速,开销很少。这一块内存地址称为栈。栈是线程级别的,大小在创建的时候已经确定,当变量太大的时候,会”逃逸”到堆上,这种现象称为内存逃逸。简单来说,局部变量通过堆分配和回收,就叫内存逃逸。
内存逃逸的危害
堆是一块没有特定结构,也没有固定大小的内存区域,可以根据需要进行调整。全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。变量在堆上的分配和回收都比在栈上开销大的多。对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片。
如何分析程序是否发生内存逃逸
build时添加-gcflags=-m 选项可分析内存逃逸情况,比如输出./main.go:3:6: moved to heap: x 表示局部变量x逃逸到了堆上。
内存逃逸发生时机
- 在方法内把局部变量指针返回 局部变量原本应该在栈中分配,在栈中回收。但是由于返回时被外部引用,因此其生命周期大于栈,则溢出。
- 发送指针或带有指针的值到 channel 中。 在编译时,是没有办法知道哪个 goroutine 会在 channel 上接收数据。所以编译器没法知道变量什么时候才会被释放。
- 在一个切片上存储指针或带指针的值。 一个典型的例子就是 []*string 。这会导致切片的内容逃逸。尽管其后面的数组可能是在栈上分配的,但其引用的值一定是在堆上。
- slice 的背后数组被重新分配了,因为 append 时可能会超出其容量( cap )。 slice 初始化的地方在编译时是可以知道的,它最开始会在栈上分配。如果切片背后的存储要基于运行时的数据进行扩充,就会在堆上分配。
- 在 interface 类型上调用方法。 在 interface 类型上调用方法都是动态调度的 —— 方法的真正实现只能在运行时知道。想像一个 io.Reader 类型的变量 r , 调用 r.Read(b) 会使得 r 的值和切片b 的背后存储都逃逸掉,所以会在堆上分配。
避免内存逃逸的办法
对于小型的数据,使用传值而不是传指针,避免内存逃逸。
避免使用长度不固定的slice切片,在编译期无法确定切片长度,只能将切片使用堆分配。
interface调用方法会发生内存逃逸,在热点代码片段,谨慎使用。
for循环select时,如果通道已经关闭会怎么样?如果select中的case只有一个,又会怎么样?
for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case。
如果select里边只有一个case,而这个case被关闭了,则会出现死循环。
select中如果任意某个通道有值可读时,它就会被执行,其他被忽略。
如果没有default字句,select将有可能阻塞,直到某个通道有值可以运行,所以select里最好有一个default,否则将有一直阻塞的风险。
字符串转成byte数组,会发生内存拷贝吗?
字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝
对未初始化的的chan进行读写,会怎么样?为什么?
读写未初始化的 chan 都会阻塞。未初始化的 chan 此时是等于nil,当它不能阻塞的情况下,直接返回 false,表示读 chan 失败
说说uintptr和unsafe.Pointer的区别
unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
unsafe.Pointer 可以和 普通指针 进行相互转换;
unsafe.Pointer 可以和 uintptr 进行相互转换。
grpc遵循什么协议?
grpc是由Google主导开发的RPC框架,使用HTTP/2协议并用ProtoBuf作为序列化工具。gRPC是动态代理的模式实现的,客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法。和传统的REST不同的是gRPC使用了静态路径,从而提高性能,另外开发者不用了解各种底层网络协议,不用去拼REST风格的动态URL,用一些格式化的错误码代替了HTTP的状态码,不用管各种的HTTP状态码,开发者开发效率比较高。客户端可以充分利用高级流和链接功能,从而有助于节省带宽、降低的TCP链接次数、节省CPU使用.
grpc内部原理是什么?
gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON).
创建Message,简单的实体表示形式,称为message
定义 gRPC 服务,以“service”关键字开头的代码段应被视为gRPC服务。带有“rpc”关键字的方法表示它是一个远程过程调用
1 | service RepositoryService { |