0
点赞
收藏
分享

微信扫一扫

Golang M 2023 7 * theory


阅读目录

  • ​​1 特性篇​​
  • ​​什么是协程(Goroutine)​​
  • ​​进程、线程、协程​​
  • ​​Golang 使用什么数据类型?​​
  • ​​字符串的小问题​​
  • ​​数组定义问题​​
  • ​​对 rune 字面量的理解和数组的语法​​
  • ​​内存四区​​
  • ​​Go 支持什么形式的类型转换?​​
  • ​​空结构体的作用​​
  • ​​单引号,双引号,反引号的区别?​​
  • ​​* 如何停止一个 Goroutine?​​
  • ​​Go 语言中 cap 函数可以作用于哪些内容?​​
  • ​​Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同?​​
  • ​​*golang 中 make 和 new 的区别?​​
  • ​​for-range 切片的时候,它的地址会发生变化么?​​
  • ​​context 使用场景和用途?​​
  • ​​常量计数器 iota​​
  • ​​defer 特性相关​​
  • ​​defer 遇见 panic​​
  • ​​介绍下 rune 类型​​
  • ​​介绍一下 interface​​
  • ​​接口的类型检查① 断言​​
  • ​​go 语言如何实现面对对象编程​​
  • ​​go的结构体能不能比较?​​
  • ​​waitGroup对象,可以实现同一时间启动n个协程​​
  • ​​切片与数组的区别​​
  • ​​切片的创建​​
  • ​​直接声明​​
  • ​​为什么map的遍历是无需的?​​
  • ​​map 的删除​​
  • ​​nil map 和空 map 有何不同?​​
  • ​​map 中删除一个 key,它的内存会释放么?​​
  • ​​Student 结构值运行下面程序发生什么?​​
  • ​​2 channel 篇​​
  • ​​并行与并发、进程与线程与协程​​
  • ​​Go 缓冲通道与无缓冲通道的区别​​
  • ​​GMP 调度模型篇​​
  • ​​调度器有哪些设计策略?​​
  • ​​3 内存逃逸篇​​
  • ​​什么是内存逃逸,为什么需要内存逃逸?​​
  • ​​如何打印逃逸分析信息​​
  • ​​Golang GC、三色标记、混合写屏障机制​​
  • ​​背景知识​​
  • ​​GC 相关术语​​
  • ​​Go 的 GC 发展演变史​​
  • ​​v 1.3-标记清除法​​
  • ​​v1.5 三色标记法​​
  • ​​1、初始时,所有对象被标记为白色​​
  • ​​2、GC开始,遍历rootset,将直接可达的对象标记为灰色​​
  • ​​3、遍历灰色对象,将直接可达对象标记为灰色,并将自身标记为黑色​​
  • ​​4、重复第3步,直到标记完所有的对象​​
  • ​​5、将标记为白色的对象当做垃圾回收掉​​
  • ​​总结​​
  • ​​Redis​​
  • ​​Redis hash 冲突​​

1 特性篇

什么是协程(Goroutine)

协程是用户态轻量级线程,是线程调度的基本单位。

通常在函数前加上go关键字就能实现并发。

一个Goroutine会以一个很小的栈启动2KB或4KB,当遇到栈空间不足时,栈会自动伸缩, 因此可以轻易实现成千上万个goroutine同时启动。

进程、线程、协程

进程是一个程序的数据集合。

线程是进程的一个最小单位。

协程是用户控制的轻量级线程,它是一种特殊的线程,可以在单个线程中实现多任务的并发处理。

goroutine 是轻量级的线程,占用资源很少,但如果一直得不到释放并且还在不断创建新协程,毫无疑问是有问题的,并且是要在程序运行几天,甚至更长的时间才能发现的问题。

Golang 使用什么数据类型?

  • 布尔型
  • 数值型(整型、浮点型)
  • 字符串
  • 指针
  • 数组
  • 结构体
  • 切片
  • map
  • chan
  • 接口
  • 函数

字符串的小问题

  • ①可以用 ​​==​​ 比较。
  • ②不可以通过下标的方式改变某个字符,字符串是只读的。
  • ③不能和 ​​nil​​ 比较。

数组定义问题

数组是可以以指定下标的方式定义的,例如:

表示 array[9]==34  则 len(array) 就是 10
array := [...]int{1,2,3,9:34}

import "fmt"

func main() {
array := [...]int{1, 2, 3, 9: 34}
fmt.Printf("值:%#v len%d", array, len(array))
}

PS E:\TEXT\test_go\test\case> go run .\case.go
值:[10]int{1, 2, 3, 0, 0, 0, 0, 0, 0, 34} len10
PS E:\TEXT\test_go\test\case>

对 rune 字面量的理解和数组的语法

package main

import (
"fmt"
)

func main() {
m := [...]int{
'a': 1,
'b': 2,
'c': 3,
}
m['a'] = 3
fmt.Println(len(m)) // 输出:100
}

原因:

以下标的方式定义数组内的元素,'c’的ascll为99,故长度为100。

内存四区

  • 代码区:存放代码
  • 全局区:常量+全局变量。
    最终在进程退出时,由操作系统回收。
  • 堆区:空间充裕,数据存放时间较久。
    一般由开发者分配,启动 Golang 的 GC 由 GC 清除机制自动回收。
  • 栈区:空间较小,要求数据读写性能高,数据存放时间较短暂。由编译器自动分配和释放,存放函数的参数值、局部变量、返回值等、局部变量等(局部变量如果产生逃逸现象,可能会挂在在堆区)

Go 支持什么形式的类型转换?

Go支持显示类型的转换,以满足严格的类型要求。

空结构体的作用

不包含任何字段的结构体叫做空结构体 ​​struct{}​​。

定义:

var et struct{}

et := struct{}{}

type ets struct {}

et := ets{}

var et ets

特性:

所有的空结构体的地址都是同一地址,都是 ​​zerobase​​ 的地址,且大小为 0。

使用场景:

​1​​ 用于保存不重复的元素的集合,Go 的 map 的 key 是不允许重复的,用空结构体作为 value,不占用额外空间。

​2​​ 用于channel 中信号传输,当我们不在乎传输的信号的内容的时候,只是说只要用信号过来,通知到了就行的时候,用空结构体作为 channel 的类型。

​3​​ 作为方法的接收者,然后该空结构体内嵌到其他结构体,实现继承。

单引号,双引号,反引号的区别?

  • 单引号,表示 byte 或者 rune 类型,对应 uint8 和 int32 类型;默认直接赋值的话是 rune 类型。
  • 双引号,字符串类型,不允许修改。实际上是字符数组,可以用下标索引其中的某个字节。
  • 反引号,表示字符串字面量,反引号中的字符不支持任何转义,写什么就是什么。

* 如何停止一个 Goroutine?

  • ① for - select 方法,采用通道,通知协程退出。
  • ②采用 context 包。

Go 语言中 cap 函数可以作用于哪些内容?

  • 数组
  • 切片
  • 通道

Printf(),Sprintf(),FprintF() 都是格式化输出,有什么不同?

  • Printf():是标准输出,一般用于打印。
  • Sprintf():把格式化字符串输出到字符串,并返回。
  • FprintF():把格式化字符串输出到实现了 io.witer方法的类型,比如文件,写入文件。

*golang 中 make 和 new 的区别?

共同点:都会分配内存空间(堆上)

不同点:

  • ① 作用变量不同,new 可以为任意类型分配内存;但是 make 只能给,切片、map、chan分配内存。
  • ② 返回类型不同,new 返回的是指向变量的指针;make 返回的是上边三种变量类型本身。
  • ③ new 对分配的内存清零;make会根据你的设定进行初始化,比如在设置长度、容量的时候。

for-range 切片的时候,它的地址会发生变化么?

在 ​​for a,b := range slice​​ 的遍历中,a 和 b 内存中的地址只有一份,每次循环遍历的时候,都会对其进行覆盖,其地址始终不会改变。

对于切片遍历的话,b 是复制的切片中的元素,改变 b,并不会影响切片中的元素。

context 使用场景和用途?

context 的主要作用:

协调多个 groutine 中的代码执行 “取消” 操作,并且可以存储键值对。最重要的是它是并发安全的。

① 可以存储键值对,供上下文(协程间)读取【建议不要使用】

② 优雅的主动取消协程(Cancel)。主动取消子协程运行,用不到子协程了,回收资源。比如一个http请求,客户端突然断开了,就直接cancel,停止后续的操作;

③ 超时退出协程(Timeout),比如如果三秒之内没有执行结束,直接退出该协程;

④ 截止时间退出协程(Deadline),如果一个业务,2点到4点为业务活动期,4点截止结束任务(协程)

常量计数器 iota

iota 常量计数器,具有自增的特点,可以简化有关于数字增长的常量的定义。

特点:

① iota只能出现在const代码块中。
② 不同 const 代码块中的 iota 互不影响。

③ 从第一行开始算,ioat 出现在第几行,它的值就是第几行减一 【所有注释行和空白行忽略;​​_​​ 代表一行,不能忽略;这一行即使没有iota 也算一行。】

④ 没有表达式的常量定义复用上一行的表达式。

defer 特性相关

​1​​ defer 的作用:

​defer为延迟函数​​,为防止开发人员,在函数退出的时候,忘记释放资源或者执行某些收尾工作;比如,解锁、关闭文件、异常捕获等操作;

​2​​ defer 的执行顺序:

每个defer对应一个实例,多个defer,也就是多个实例,使用指针连接成一个单链表,每次写一个defer实例,就插入到这个单链表的头部,函数结束的时候,从头部依次取出,并执行defer。可以类比“栈”的先进后出方式。

​3​​​ defer 与 return 先后顺序:
return 后的语句先执行,defer 后的语句后执行。

​4​​ 具名返回值遇到defer的情况(看下面的例子)

return虽先执行,但是defer中有改变具名返回值的操作,导致返回值发生了改变(至于为什么,只能说Go就是这样定义的)

package main
import "fmt"

// t 初始化 0, 并且作用域为该函数全域
func returnButDefer() (t int) {
defer func() {
t = t * 10
}()
return 1
}
func main() {
fmt.Println(returnButDefer()) //输出 10
}

defer 遇见 panic

遇见 return(​​或函数体到末尾​​)和遇见 panic 都会触发 defer。

① defer遇见panic,但是并不捕获异常的情况。

和 return 一样,只不过 panic 前面的 defer 执行完之后,跳出函数,直接报异常。

② defer遇见panic,并捕获异常。

和上述不同的是,当运行的defer中捕获异常,并恢复之后,跳出函数,不会报异常,会继续执行。

但是需要注意的是,在发生恐慌的函数内,panic之后的程序都不会被执行。

package main

import "fmt"

func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}

func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}

func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}

func DeferFunc4() (t int) {
//传入的实参t,将defer放入链表时的t,并不是执行defe时候的t
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t) //传入实参t
t = 1
return 2
}

func main() {
fmt.Println(DeferFunc1(1))
fmt.Println(DeferFunc2(1))
fmt.Println(DeferFunc3(1))
DeferFunc4()
}

输出:
这类题目记住,return 返回值的时候,是赋值操作,并没指针。

PS E:\TEXT\test_go\test\case> go run .\case.go
4
1
3
0
2
PS E:\TEXT\test_go\test\case>

介绍下 rune 类型

rune 是 int32 的别名,等同于int32,常用来处理 unicode 或 utf-8 字符,用来区分字符值和整数值。

这里和 byte 进行对比,byte是uint8,常用来处理 ascii 字符。

那么有什么不同呢?

举个例子

package main

import (
"fmt"
"unicode/utf8"
)

func main() {

var str = "hello 世界"
/*
golang中string底层是通过byte数组实现的,
直接求len 实际是在按字节长度计算,
所以一个汉字占3个字节算了3个长度。
*/
fmt.Println("len(str):", len(str))

//以下两种都可以得到str的字符串长度

//golang中的unicode/utf8包提供了用utf-8获取长度的方法
fmt.Println("RuneCountInString:", utf8.RuneCountInString(str))

//通过rune类型处理unicode字符
fmt.Println("rune:", len([]rune(str)))

}

PS E:\TEXT\test_go\test\case> go run .\case.go
len(str): 12
RuneCountInString: 8
rune: 8
PS E:\TEXT\test_go\test\case>

package main

import (
"fmt"
)

func main() {

var str = "hello 世界"
fmt.Println(string([]rune(str)[7:]))
//就能取出‘界’
}

介绍一下 interface

interface 特性,

  • interface 是 method 方法的集合。
  • interface是一种类型,并且是指针类型
  • 实现统一的接口(成为接口类型的数据)
  • 利用统一的接口各干各的事(方法的不同实现方式)
  • interface的更重要的作用在于多态实现

interface 使用

  • 接口的使用不仅仅针对结构体,自定义类型、变量等等都可以实现接口。
  • 如果一个接口没有任何方法,我们称为空接口,由于空接口没有方法,所以任何类型都实现了空接口。
  • 要实现一个接口,必须实现该接口里面的所有方法。

接口的类型检查① 断言

package main

import (
"fmt"
)

type Student struct {
Name string
Age int
}

func main() {
stu := &Student{
Name: "小有",
Age: 22,
}

var i interface{} = stu
// 断言成功,s1为*Student类型 不安全断言
s1 := i.(*Student)
fmt.Println(s1)

//断言失败,ok为false 安全型断言
s2, ok := i.(Student)
if ok {
fmt.Println("success:", s2)
}
fmt.Println("failed:", s2)
}

② 如果接口类型可能有多种情况的话,采用 Type Switch 方法。

func typeCheck(v interface{}){

// switch v.(type) { //只用判断类型,不需要值
switch msg := v.(type) { //值和判断类型都需要
case int :
...
case string:
...
case Student:
...
case *Student:
...
default:
...
}

}

go 语言如何实现面对对象编程

面对对象编程的三个基本特征:

  • 封装
  • 继承
  • 多态

go 通过结构体实现:

  • 封装
  • 继承
  • 通过接口实现多态

go的结构体能不能比较?

结构体中含有不能比较的类型时,不能比较;

声明两个比较值的结构体的名字不同,即使字段名、类型、顺序相同,也不能比较(强转类型可以比较),说白了,必须用同一个结构体类型声明的值,才能比较;

sn1 := struct {
age int
name string
}{age: 11, name: "qq"}

sn2 := struct {
age int
name string
}{age: 11, name: "qq"}

//这种情况是可以比较的
if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}

type s1 struct {
age int
name string
}
type s2 struct {
age int
name string
}

sn1 := s1{age: 11, name: "qq"}
sn2 := s2{age: 11, name: "qq"}

//这种情况,直接编译失败
if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}

waitGroup对象,可以实现同一时间启动n个协程

一个waitGroup对象,可以实现同一时间启动n个协程,并发执行,等n个协程全部执行结束后,在继续往下执行的一个功能。

通过 Add() 方法设置启动了多少个协程,在每一个协程结束的时候调用 Done() 方法,计数减一,同时使用 wait() 方法阻塞主协程,等待全部的协程执行结束。

切片与数组的区别

共同点:

① 都是存储一系列相同类型的数据结构。
② 都可以通过下标来访问。
③ 都有 len 和 cap 这种概念。

不同点:

① 数组是定长的,且大小不能更改,是值类型。比如在函数参数传入的时候,形参和实参类型必须一模一样的。

② 切片是不定长的,容量是可以自动扩容的。切片传入函数的时候是值类型。

package main

import "fmt"

func main() {
//创建一个长度和容量均为3的切片
arr := []int{1, 2, 3}
fmt.Println(arr) // [1 2 3]
//-------
addNum(arr)
//-------
fmt.Println(arr) // [1,2,3]
}

func addNum(sli []int) {
//使用appedn添加"4"
sli = append(sli, 4)
fmt.Println(sli) // [1,2,3,4]
sli[0] = 666
fmt.Println(sli) // [666 2 3 4]
}

实际上,切片的传参是使用值传递。

函数能够对切片进行修改,是因为在函数中,拷贝切片所指的数组发生了变化,因此原切片的结果也发生变化。

func addNum(sli []int) {
//使用appedn添加"4"
// sli = append(sli, 4)
// fmt.Println(sli) // [1,2,3,4]
sli[0] = 666
fmt.Println(sli) // [666 2 3 4]
}

PS E:\TEXT\test_go\test\case> go run .\case.go
[1 2 3]
[666 2 3]
[666 2 3]
PS E:\TEXT\test_go\test\case>

答:一个方法就是用指针。

func main() {
arr := []int{1, 2, 3, 4}
fmt.Println(arr) //[1 2 3 4]
// -----
addNum(&arr)
// -----
fmt.Println(arr) //[1 2 3 4 5]
}

func addNum(sli *[]int) {
*sli = append(*sli, 5)
fmt.Println(*sli) //[1 2 3 4 5]
}

切片的创建

序号

方式

示例

1

直接声明

var slice []int

2

new

slice := *new([]int)

3

字面量

slice := []int{1,2,3,4,5}

4

make

slice := make([]int,10)

5

从切片或数组截取

slice := array[:5] 或 slice := souceSlice[2:4]

直接声明

这里重点说一下 nil 切片和空切片。

nil 切片

var slice []int
slice := *new([]int) //new前的*是解引用

空切片

slice := []int{}
slice := make([]int)

这两种方式的 len 和 cap 均为 0。

但是不同的是:

  • nil 切片和 nil 的比较结果是 true。
  • 空切片和 nil 的比较结果是 false,且同一程序里面,任何类型的空切片的底层数组指针的都指向同一地址。

为什么map的遍历是无需的?

① 遍历的起始位置每次都是随机的。
② 由于扩容,会导致 key 所处的桶发生变化。

map 的删除

key,value 清零。
对应位置的 tophash 置为 Empty。

1、所有Go版本通用方法

package main

import "fmt"

func main() {
a := make(map[string]int)

a["a"] = 1
a["b"] = 2
fmt.Println(a)

// clear all
a = make(map[string]int)
fmt.Println(a)
}

PS E:\TEXT\test_go\test\case> go run .\case.go
map[a:1 b:2]
map[]
PS E:\TEXT\test_go\test\case>

func TestMapDelete(t *testing.T) {
var data = map[string]string{
"name": "Elliot",
}
// "Elliot"
fmt.Println(data["name"])

delete(data, "name")

// 无任何输出
fmt.Println(data["name"])
}

nil map 和空 map 有何不同?

nil map表示未初始化的map,等同于 ​​var m map[string]int​​ 。

空 map 表示 map 已经被初始化,只是长度为 0,还并未赋于键值对。

  • ① 直接读取 ​​nil map:m[“a”]​​ 并不会报错,会返回默认类型的空值。
  • ② 直接给 nil map 赋值:​​m[“a”] = 1​​ 直接报错。
  • ③ 需要通过 map == nil 来判断,是否为 ​​nil map​​。

map 中删除一个 key,它的内存会释放么?

① 如果删除的键值对都是值类型(int,float,bool,string以及数组和struct),map的内存不会自动释放。

② 如果删除的键值对中有(指针,slice,map,chan等),且该引用未被程序的其他位置使用,则该引用的内存会被释放,但是map中为存放这个类型而申请的内存不会被释放。

上述两种情况,map为存储键值所申请的空间,均不会被立即释放。等待GC到来,才会被释放。

③ 将map设置为nil后,内存被回收。

Student 结构值运行下面程序发生什么?

package main

import "fmt"

type Student struct {
Name string
}

var list map[string]Student

func main() {

list = make(map[string]Student)

student := Student{"Aceld"}

list["student"] = student
list["student"].Name = "LDB"

fmt.Println(list["student"])
}

编译失败:

​map[string]Student​​​ 的 value 是一个 ​​Student​​​ 结构值,所以当​​list[“student”] = student​​,是一个值拷贝过程。

而 ​​list[“student”]​​ 则是一个值引用。

那么值引用的特点是只读。

所以对 ​​list[“student”].Name = "LDB"​​ 的修改是不允许的。

package main

import "fmt"

type Student struct {
Name string
}

var list map[string]*Student

func main() {

list = make(map[string]*Student)

student := Student{"Aceld"}

list["student"] = &student
list["student"].Name = "LDB"

fmt.Println(list["student"])
fmt.Println(list["student"].Name)
}

PS E:\TEXT\test_go\test\case> go run .\case.go
&{LDB}
LDB
PS E:\TEXT\test_go\test\case>

2 channel 篇

并行与并发、进程与线程与协程

并行指物理上同时执行,并发指能够让多个任务在逻辑上交织执行的程序设计。

Go 缓冲通道与无缓冲通道的区别

​1​​ 无缓冲通道,在初始化的时候,不用添加缓冲区大小;

​2​​ 无缓冲通道的发送与接收(或者接收与发送)是同步的;

​3​​​ 发送者的发送操作将被阻塞,直到有接收者接收数据;
接收者的接受操作将被阻塞,直到有发送者发送数据。

​1​​ 有缓冲通道,在初始化的时候,需要指定缓冲区大小;

​2​​ 有缓冲通道的发送与接收(或者接收与发送)是不同步的,也就是异步的;

​3​​ 有缓冲通道,缓冲区满后,再继续执行发送操作,会被阻塞。(其实无缓冲通道可以想象为是一个一直满的通道)

GMP 调度模型篇

  • G代表协程;
  • P代表协程处理器;
  • M代表内核级线程。

什么是GMP模型?

当我们写一个并发程序,操作系统会对其进行调度,线程是操作系统调度的最小单位,而不是协程,所以GMP模型就是想办法将用户创建的众多协程分配到线程上的这么一个过程。

调度器有哪些设计策略?

复用线程:避免重复的创建、销毁线程,而是对线程的复用(work stealing机制和hand off机制)

抢占机制:一个协程占用cpu的时长是有时间限制的,当该协程运行超时之后,会被其他协程抢占,防止其他协程被饿死,

3 内存逃逸篇

什么是内存逃逸,为什么需要内存逃逸?

定义:
一个在栈区存储的变量,
因为被堆区的变量引用,
使得该变量会从栈区逃逸到堆区;

原因:

go 语言并不需要程序员像使用c/c++那样,需要自己去释放内存,go做了自动化处理。

所以go申请的局部变量(无论是 var 还是 new 申请的),只要没有超过一定大小,都被先分配到栈上,但是如果该变量被堆上的变量引用了得话,该变量必须逃逸到堆上,防止栈区的内存会被系统全部自动释放掉,从而导致被引用的变量丢失,产生野指针。

逃逸的堆区的变量,在需要被回收的时候,会被 GC 进行回收。

如何打印逃逸分析信息

逃逸分析在编译阶段进行,由编译器完成。

go run -gcflags "-m -l" *.go

​-m​​​ 设置打印信息
​​​-l​​ 禁止内联,内联编译,编译器会优化代码,可能导致不会发生逃逸。

Golang GC、三色标记、混合写屏障机制

没有躺赢的命,那就站起来跑!

讲讲垃圾回收机制

圾回收机制,是一种自动内存管理机制。

在程序中定义一个变量后,会在内存中开辟相应空间进行存储。当不需要此变量后,需要手动销毁此对象,并释放内存。

对这种不再使用的内存资源进行自动回收的功能为垃圾回收。

圾回收机制,采用三色标记法:

  • 灰:遍历的时候为黑色。
  • 白:初始化为白色。
  • 黑:遍历时自身为黑色。

背景知识

什么是GC?

垃圾回收(Garbage Collection,缩写为GC),是一种自动内存管理机制。

在程序中定义一个变量后,会在内存中开辟相应空间进行存储。当不需要此变量后,需要手动销毁此对象,并释放内存。

对这种不再使用的内存资源进行自动回收的功能为垃圾回收。

GC 相关术语

GC的行话,先普及一下,不然后文读起来会稍微有点懵。

赋值器:说白了就是你写的程序代码,在程序的执行过程中,可能会改变对象的引用关系,或者创建新的引用。

回收器:垃圾回收器的责任就是去干掉那些程序中不再被引用得对象。

STW:全称是stop the word,GC期间某个阶段会停止所有的赋值器,中断你的程序逻辑,以确定引用关系。

举个栗子,有一个大院,孩子特别多,老师希望他们以班长为起点手牵手在一起,但总有几个不听话的孩子,没有牵手,你为了找出这些不听话的孩子,你会以班长为起点,一个一个的往后捋。但是如果有一个名叫张三的孩子,之前在队尾,后来在你数到队伍中间的时候,又跑到了队头和班长牵手去了,当你数完后,因为没有统计到张三,你就认为张三没有听话,没有奖励小红花,岂不让孩子比窦娥还冤…,所以这种情况下,你需要先让孩子们不动【映射到程序的概念,即STW停止程序运行】,然后再统计。

root对象:根对象是指赋值器不需要通过其他对象就可以直接访问到的对象,通过Root对象, 可以追踪到其他存活的对象。

常见的 root 对象有:

  • 全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
  • 执行栈:每个 goroutine (包括main函数)都拥有自己的执行栈,这些执行栈上包含栈上的变量及堆内存指针。【堆内存指针即在gorouine中申请或者引用了在堆内存的变量】

Go 的 GC 发展演变史

v 1.3-标记清除法

标记清除法主要包含两个步骤:

  • 标记
  • 清除

示例如下:

开启 STW,停止程序的运行,图中是本次GC涉及到的root节点和相关对象。

Golang M 2023 7 * theory_后端


从根节点出发,标记所有可达对象。

Golang M 2023 7 * theory_开发语言_02


停止STW,然后回收然后回收所有未被标记的对象。

Golang M 2023 7 * theory_golang_03


标记清除法的最大弊端就是在整个GC期间需要STW,将整个程序暂停。因为如果不进行STW的话,会出现已经被标记的对象A,引用了新的未被标记的对象B,但由于对象A已经标记过了,不会再重新扫描A对B的可达性,从而将B对象当做垃圾回收掉。

说实话这种全程STW的GC算法真的是如过街老鼠,人见人打…好家伙,让我程序停下来,专门去做垃圾回收这件事,在追求高性能的今天,很难有人可以接受这种性能损耗。

所以Golang团队这个时期就开始专注于如何能提升GC的性能,这里希望各位道友能明白Golang团队对GC算法优化的方向是什么,或者目标是什么,那就是让GC和用户程序可以互不干扰,并发进行。所以才有了后面的三色标记法。

v1.5 三色标记法

三色标

Golang M 2023 7 * theory_golang_04

1、初始时,所有对象被标记为白色

Golang M 2023 7 * theory_数组_05

2、GC开始,遍历rootset,将直接可达的对象标记为灰色

Golang M 2023 7 * theory_后端_06

3、遍历灰色对象,将直接可达对象标记为灰色,并将自身标记为黑色

Golang M 2023 7 * theory_Go_07

4、重复第3步,直到标记完所有的对象

Golang M 2023 7 * theory_开发语言_08

5、将标记为白色的对象当做垃圾回收掉

总结

Golang v1.3之前采用传统采取标记-清除法,需要STW,暂停整个程序的运行。

在v1.5版本中,引入了三色标记法和插入写屏障机制,其中插入写屏障机制只在堆内存中生效。但在标记过程中,最后需要对栈进行STW。

在v1.8版本中结合删除写屏障机制,推出了混合屏障机制,屏障限制只在堆内存中生效。避免了最后节点对栈进行STW的问题,提升了GC效率。

Redis

Redis hash 冲突

Redis hash冲突是指在Redis中存储数据时,由于哈希函数的不同,可能会出现多个键映射到同一个哈希桶的情况,这种情况就是Redis hash冲突。


举报

相关推荐

0 条评论