go垃圾回收
1 go 垃圾回收变更
Go 语言的垃圾回收器(GC)自其诞生以来一直在不断演进和优化,以提高性能、减少暂停时间和对程序执行的影响。以下是一些关键的改进和变更点:
-
并发标记周期:
- Go 语言从一开始就采用了并发标记(Concurrent Marking)的垃圾回收策略,在不影响应用程序主要逻辑的同时进行垃圾回收。
-
写屏障:
- 在早期版本中,为了支持并发标记,Go 引入了写屏障技术来追踪在标记过程中可能发生的指针变动。
- 后续对写屏障进行了优化,例如引入了混合写屏障(Hybrid Write Barrier),减少了写屏障带来的开销。
-
并发三色标记:
- Go 1.5 引入了并发三色标记算法(Concurrent Tri-color Marking),进一步缩短了 GC 暂停时间。
-
并发清扫与辅助清扫:
- Go 1.8 开始尝试并发清扫阶段,并在后续版本中逐步完善这一机制,使得整个清理过程也可以与应用逻辑并发执行。
- 辅助清扫(Assisted Sweep)是在特定条件下让工作线程协助完成清扫任务,从而加快清扫速度。
-
并行标记:
- Go 1.5 之后还增加了并行标记(Parallel Marking)的支持,通过多核CPU并行化处理标记阶段的工作。
-
栈收缩与栈拷贝:
- Go 的垃圾回收还包括对 goroutine 栈的管理,如栈收缩功能可以回收未使用的栈空间。
- 在某些版本中,对于增长过大的栈会采用栈拷贝的方式来动态调整栈大小。
-
抢占式标记:
- Go 1.14 引入了抢占式标记(Preemptive Marking),目的是将长时间运行的 goroutine 对垃圾回收暂停时间的影响降到最低。
每个新版本的 Go 都致力于提升垃圾回收器的效率和对用户代码透明性,确保高并发、高性能的应用场景下能够更好地运行。随着Go社区和开发团队的努力,垃圾回收器的性能表现得到了持续改善。
golang垃圾回收图解 - 知乎
2 如何避免缓存穿透
缓存穿透是指在高并发场景下,查询请求针对的是数据库中不存在的数据(或已失效的缓存数据),由于缓存系统本身的设计是当缓存未命中时从后端存储(如数据库)加载数据并回填到缓存中,但如果数据库也查不到该数据,则不会将空值写入缓存。这样,每次对同一不存在数据发起查询时,都会直接绕过缓存去查询数据库,导致大量无效请求直接打到数据库上,增加了数据库的压力,同时也浪费了网络资源和计算资源。
这种现象称为“缓存穿透”,它会严重影响系统的性能,并可能导致数据库崩溃。解决缓存穿透的主要策略有:
-
缓存空对象:
- 对于查询结果为null的Key,仍然将其设置为缓存项,不过期时间可以设置得相对短一些(比如几分钟),防止攻击者持续用同样的非法key频繁击穿缓存。
-
布隆过滤器(Bloom Filter):
- 在访问缓存之前,先通过布隆过滤器进行校验。布隆过滤器是一个空间效率极高的概率型数据结构,它可以用来判断一个给定的key是否可能存在。如果布隆过滤器认为某个key一定不存在,则直接返回,从而避免了对数据库的查询。
-
白名单或热点数据预加载:
- 针对某些特定业务场景,可以提前将合法的、可能会被大量查询的ID列表或者范围内的数据对应的键加载到缓存中,形成一种变相的白名单机制。
-
使用分布式锁:
- 当一个Key在缓存中未找到且需要查询数据库时,加分布式锁来确保同一时刻只有一个线程去数据库查询,并把查询结果(包括不存在的情况)放入缓存中。
-
服务层拦截与校验:
- 在服务层对可能引发穿透的非法请求进行拦截和校验,例如对用户输入的合法性进行严格限制和验证,减少不合理的请求到达数据库。
通过这些策略,可以有效地降低甚至避免缓存穿透的发生,保护后端数据库免受恶意攻击或者高并发下无效请求的冲击。
3 Golang GC 发展历史,版本号,优点
Go(Golang)语言的垃圾回收器(GC)在发展过程中不断进行优化和改进,以适应更高的性能需求和多核处理器环境。以下是其发展历史、重要版本及对应优点概述:
-
早期版本
- Go 1.0 (2012年):初始GC实现基于标记-清扫算法,并采用Stop-The-World (STW) 策略,确保在垃圾回收期间程序暂停执行。
-
并发标记与清扫
- Go 1.5 (2015年):引入了并发标记阶段,使得GC可以在应用程序运行时并发地进行部分工作,显著减少了STW时间。
-
写屏障与辅助清扫
- 后续版本中,为了解决并发标记期间的指针更新问题,GC引入了写屏障技术。同时开始利用空闲的工作线程进行辅助清扫,进一步减少STW时间。
-
抢占式标记
- Go 1.14 (2020年):新增了抢占式标记功能,允许GC在长时运行的goroutine上适时插入标记工作,从而降低最坏情况下的停顿时间。
-
持续优化
- 各个版本迭代中,Go团队不断对GC进行优化,包括:
- 提高内存分配效率和回收速度。
- 减少GC暂停时间和提高可预测性。
- 优化对大内存分配和释放的支持。
- 各个版本迭代中,Go团队不断对GC进行优化,包括:
-
优点
- 自动内存管理:Go的GC自动处理内存分配和回收,降低了程序员手动管理内存的复杂性和风险。
- 并发处理:GC支持并发标记,能够更好地利用多核CPU资源,减小对应用响应时间和吞吐量的影响。
- 可预测性:通过不断优化,Go GC努力提供更短且可预测的暂停时间,尤其对于实时性要求较高的服务而言极为重要。
总之,Go的GC从最初的简单实现逐步演变为一个高度并发且具有较强适应性的垃圾回收系统,其目标是为开发者提供一个高效、稳定且易于使用的内存管理方案。随着每次版本更新,Go团队都在不断地针对GC进行改进,力求在高性能和低延迟之间取得平衡。
Go 的垃圾回收机制在实践中有哪些需要注意的地方? - 知乎
4 并发调度模型GMP
并发调度模型GMP(Goroutine-Monitor-Processor)并非Go语言官方的术语,但这种表述可能是指代早期对Go语言并发调度机制的一种简化描述。实际上,Go语言的并发调度模型核心组件是Goroutines、线程(OS Threads或称为M:Machine)和处理器P(Processor),而非GMP。
Go语言中的并发调度机制如下:
-
Goroutines:
- Goroutines是Go语言中的轻量级线程,由Go运行时自动管理。它们可以快速创建和销毁,并且可以在多个操作系统线程之间进行上下文切换,实现高并发执行。
-
线程(M / Machine):
- M代表与操作系统交互的实际线程,每个M都绑定到一个操作系统线程上。
- M负责从处理器P那里获取并执行goroutine。
-
处理器(P / Processor):
- P表示逻辑处理器,它维护着一个工作队列,其中包含待执行的goroutine。
- 每个P都有一个本地的工作队列以及内存分配缓存,用于提高内存分配效率。
- Go程序启动时会创建一定数量的P,这个数量可以通过环境变量GOMAXPROCS来设置,从而控制并发执行的最大任务数。
-
调度过程:
- 当创建一个新的goroutine时,它会被添加到全局队列或某个P的本地队列中。
- M需要与P结合才能执行goroutine,M空闲时会尝试从全局队列或其它P的本地队列中“窃取”任务来执行。
- 在垃圾回收(GC)期间,所有活动的M都会暂停执行,等待GC完成后再继续。
通过上述设计,Go语言实现了高效的并发编程,使得开发者无需关注底层线程管理和同步细节,就能轻松地编写出高性能的并发程序。
5 go 静态属性
在Go语言中,静态属性(通常称为全局变量或包级变量)是指在包作用域内定义的变量,它们在整个程序运行期间都存在,并且对当前包内的所有函数都是可见的。这些变量在内存中的生命周期从程序启动开始到程序结束,存储在程序的数据段或BSS段,而非函数调用栈上。
例如:
Go
1package main 2 3// 静态属性(全局变量) 4var GlobalVar int = 100 5 6func main() { 7 // 在这个函数中可以直接访问GlobalVar 8 println(GlobalVar) 9 10 // 可以修改全局变量 11 GlobalVar = 200 12} 13 14// 其他在同一包内的函数也可以直接访问和修改此全局变量 15func SomeFunction() { 16 println(GlobalVar) 17}
需要注意的是,在并发编程中,多个goroutine同时读写全局变量可能会引发竞态条件,因此通常推荐使用互斥锁或其他同步机制来保护全局变量的安全访问。此外,过度依赖全局变量可能降低代码的可读性和可维护性,建议尽量减少全局变量的使用并遵循最小权限原则。
6 go 什么情况下出现逃逸
在Go语言中,变量逃逸(Escape Analysis)是指编译器分析程序中的局部变量是否能够在函数栈上分配内存,或者必须分配到堆上。如果一个局部变量满足以下条件之一,则可能逃逸:
- 返回局部变量的地址:
- 当函数返回一个指向局部变量的指针时,该局部变量需要保证在其生命周期外仍然可访问,因此它必须分配在堆上。
Go
1func createInt() *int { 2 i := new(int) // 或者 var i int; i = 0 3 *i = 42 4 return i 5}
- 全局或包级别变量引用了局部变量:
- 如果一个局部变量被全局或包级别的变量引用,那么这个局部变量可能会逃逸到堆上,以确保即使函数执行结束,这个局部变量依然可用。
Go
1var globalVar *int 2 3func foo() { 4 local := 10 5 globalVar = &local // 这将导致 local 可能会逃逸到堆上 6}
- 闭包引用了局部变量:
- 当一个闭包捕获并引用了其外部函数作用域内的局部变量时,为了保证闭包可以在其外部函数执行完毕后继续使用这些变量,这些局部变量会被复制或移动到堆上。
Go
1func createClosure() func() int { 2 var local int = 10 3 return func() int { 4 return local 5 } 6}
-
切片、映射等内部结构动态增长:
- 对于初始化时容量较小但可能在函数内扩容的切片、映射等数据结构,它们的内部元素可能会因为扩容而分配到堆上。
-
并发编程:
- 在并发环境中,如果多个goroutine共享访问一个局部变量,为了防止竞争条件和数据不一致,编译器可能会决定将该变量移到堆上。
总的来说,当局部变量的生命周期超出其所在函数的作用范围,或者因其他原因需要长期存在时,就可能发生逃逸,转而在堆上分配内存。
7 make 与 new
在Go语言中,make和new都是用于分配内存的内置函数,但它们之间存在一些关键的区别:
- new():
- new()函数主要用来为指定类型的值分配零值初始化的内存空间,并返回指向新分配内存的指针。
- 使用方式:var p *T = new(T),其中T是类型名,返回的是类型T的一个未初始化(零值)实例的指针。
例如:
Go
1type MyType struct { 2 a int 3 b string 4} 5 6func main() { 7 var x *MyType = new(MyType) 8 fmt.Println(x) // 输出:&{0 ""} 9}
- make():
- make()函数主要用于内建类型如切片(slice)、映射(map)以及通道(channel)的初始化。对于这些类型,直接使用new()并不能创建一个有效的实例,需要通过make()来完成。
- make()不仅分配内存,还会对数据结构进行初始化,使其可以立即使用。
例如:
Go
1func main() { 2 // 创建一个整数切片 3 slice := make([]int, 5) // 创建长度为5的切片,初始元素都为零值 4 fmt.Println(slice) // 输出:[0 0 0 0 0] 5 6 // 创建一个映射 7 mapExample := make(map[string]int) 8 mapExample["one"] = 1 9 fmt.Println(mapExample) // 输出:map[one:1] 10 11 // 创建一个无缓冲的通道 12 channel := make(chan int) 13 go func() { channel