Go 语言的垃圾回收器(GC)自其诞生以来一直在不断演进和优化,以提高性能、减少暂停时间和对程序执行的影响。以下是一些关键的改进和变更点:
并发标记周期:
写屏障:
并发三色标记:
并发清扫与辅助清扫:
并行标记:
栈收缩与栈拷贝:
抢占式标记:
每个新版本的 Go 都致力于提升垃圾回收器的效率和对用户代码透明性,确保高并发、高性能的应用场景下能够更好地运行。随着Go社区和开发团队的努力,垃圾回收器的性能表现得到了持续改善。
缓存穿透是指在高并发场景下,查询请求针对的是数据库中不存在的数据(或已失效的缓存数据),由于缓存系统本身的设计是当缓存未命中时从后端存储(如数据库)加载数据并回填到缓存中,但如果数据库也查不到该数据,则不会将空值写入缓存。这样,每次对同一不存在数据发起查询时,都会直接绕过缓存去查询数据库,导致大量无效请求直接打到数据库上,增加了数据库的压力,同时也浪费了网络资源和计算资源。
这种现象称为“缓存穿透”,它会严重影响系统的性能,并可能导致数据库崩溃。解决缓存穿透的主要策略有:
缓存空对象:
布隆过滤器(Bloom Filter):
白名单或热点数据预加载:
使用分布式锁:
服务层拦截与校验:
通过这些策略,可以有效地降低甚至避免缓存穿透的发生,保护后端数据库免受恶意攻击或者高并发下无效请求的冲击。
Go(Golang)语言的垃圾回收器(GC)在发展过程中不断进行优化和改进,以适应更高的性能需求和多核处理器环境。以下是其发展历史、重要版本及对应优点概述:
早期版本
并发标记与清扫
写屏障与辅助清扫
抢占式标记
持续优化
优点
总之,Go的GC从最初的简单实现逐步演变为一个高度并发且具有较强适应性的垃圾回收系统,其目标是为开发者提供一个高效、稳定且易于使用的内存管理方案。随着每次版本更新,Go团队都在不断地针对GC进行改进,力求在高性能和低延迟之间取得平衡。
Go 的垃圾回收机制在实践中有哪些需要注意的地方? - 知乎
并发调度模型GMP(Goroutine-Monitor-Processor)并非Go语言官方的术语,但这种表述可能是指代早期对Go语言并发调度机制的一种简化描述。实际上,Go语言的并发调度模型核心组件是Goroutines、线程(OS Threads或称为M:Machine)和处理器P(Processor),而非GMP。
Go语言中的并发调度机制如下:
Goroutines:
线程(M / Machine):
处理器(P / Processor):
GOMAXPROCS
来设置,从而控制并发执行的最大任务数。调度过程:
通过上述设计,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同时读写全局变量可能会引发竞态条件,因此通常推荐使用互斥锁或其他同步机制来保护全局变量的安全访问。此外,过度依赖全局变量可能降低代码的可读性和可维护性,建议尽量减少全局变量的使用并遵循最小权限原则。
在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}
切片、映射等内部结构动态增长:
并发编程:
总的来说,当局部变量的生命周期超出其所在函数的作用范围,或者因其他原因需要长期存在时,就可能发生逃逸,转而在堆上分配内存。
在Go语言中,make
和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()
函数主要用于内建类型如切片(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 <- 1 }()
- 14 value := <-channel
- 15 fmt.Println(value) // 输出:1
- 16}
总结:
new()
。make()
来创建和初始化。