01
内存分布
什么是虚拟内存?
64 位 linux 进程内存分布情况
堆和栈:
02
数据类型的内存结构
基础类型
类型 | 长度 | 默认值 | 说明 |
---|---|---|---|
bool | 1 | false | |
byte | 1 | 0 | uint8 |
int,uint | 4,8 | 0 | 默认整数类型,依据目标平台,32 或 64 位 |
int8,uint8 | 1 | 0 | -128~127,0~255 |
int16,uint16 | 2 | 0 | -32,768~32,767,0~65,535 |
int32,uint32 | 4 | 0 | -21亿~21 亿,0~42 亿 |
int64,uint64 | 8 | 0 | |
float32 | 4 | 0.0 | |
float64 | 8 | 0.0 | 默认浮点数类型 |
complex64 | 8 | ||
complex128 | 16 | ||
rune | 4 | 0 | Unicode Code Point,int32 |
uintptr | 4,8 | 0 | 足以存储指针的uint |
string | "" | 字符串,默认值为空字符串,而非 NULL | |
array | 数组 | ||
struct | 结构体 | ||
function | nil | 函数 | |
interface | nil | 接口 | |
map | nil | 字典,引用类型 | |
slice | nil | 切片,引用类型 | |
channel | nil | 通道,引用类型 |
字符串
type stringStruct struct {
str unsafe.Pointer
len int
}
// Variant with *byte pointer type for DWARF debugging.
type stringStructDWARF struct {
str *byte
len int
}
字符串长度
通过实例代码,查看字符串长度:
str := "hello北京"
fmt.Println(len(str)) //11
fmt.Println(utf8.RuneCountInString(str)) // 7
for index, runeValue := range str {
fmt.Printf("%d: %c\n", index, runeValue)
}
// 0:h
// 1:e
// 2:l
// 3:l
// 4:o
// 5:北
// 8:京
使用 len() 获取字符串长度,返回的是字节长度,如果想要获取 unicode 长度,需要使用 utf8 包的方法。
需要注意的是,在使用 for range 遍历字符串时,index 是按照字节顺序产生的,value 是以 unicode 顺序产生的。
字符串连接
由于字符串是只读的,所以字符串连接操作必然会涉及到内存拷贝。
方式 | 用法 | 特点 | 适用场景 |
---|---|---|---|
+操作符 | s += "hello" + "北京" + "2021" | 每次拼接,都申请新的内存块,只能是字符串类型使用,可读性强,性能一般 | 少量字符串拼接时 |
fmt.Sprint | s = fmt.Sprint("hello","北京",2021) | 内部使用[]byte实现,涉及到类型转换,可以拼接其他类型,性能一般 | 少量非字符串类型拼接时 |
strings.Join | s = strings.Join([]string{"hello","北京","2021"},"") | 只能拼接字符串数组,不灵活 | 已存在字符串数组时 |
bytes.Buffer | var b bytes.Bufferb.WriteString("hello")b.WriteString("北京")b.WriteString("2021")s = b.String() | 拼接字符串、字符和 unicode,底层使用[]byte,设计到 string 和 []byte 之间转换 | 少量字符串拼接时 |
strings.Builder | var b strings.Builderb.WriteString("hello")b.WriteString("北京")b.WriteString("2021")s = b.String() | 拼接字符串、字符和 unicode,使用unsafe.Pointer 优化了 string 和 []byte 之间的转换 | 大量字符串拼接 |
少量字符串拼接时,推荐使用+操作符,可读性强;如果性能要求高时,推荐使用 string.Builder。
切片 - slice
type slice struct {
array unsafe.Pointer
len int
cap int
}
增加切片元素:append 方法 cap 不够时,cap < 1024,cap 容量成倍增加;cap >= 1024 时,按照 1.25 倍扩容。
a := []int{1}
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
a = append(a, 2)
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
a = append(a, 3)
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
a = append(a, 4, 5)
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
// 输出结果:
// len: 1 cap: 1 data: [1]
// len: 2 cap: 2 data: [1 2]
// len: 3 cap: 4 data: [1 2 3]
// len: 5 cap: 8 data: [1 2 3 4 5]
底层数组扩容时,运行时会新生成一块扩容后大小的内存,然后把数据拷贝过去,这里涉及到一定的内存拷贝开销,建议尽量计算好需要使用的容量,避免自动扩容。
切片作为函数参数传递
func main() {
a := []int{1,2,3}
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
appendSlice(a)
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
}
func appendSlice(a []int) {
a = append(a, 4)
}
// 输出结果:
// len: 3 cap: 3 data: [1 2 3]
// len: 3 cap: 3 data: [1 2 3]
通过代码输出结果可以看到,切片作为函数参数传递,没有追加成功。
a 传入 appendSlice 后,属于值传递,新生成一个和 a 一样的切片结构体 a1,指向同样的底层数组。
a = append(a, 4)
实际上是操作 a1,a 的 len、cap 未变,所以两次打印的数据一样。
注意:切片是结构体,传入函数会新生成结构体实参,如果需要在函数内部改变切片值,需要显示返回:
func appendSlice(a []int) []int {
a = append(a, 4)
return a
}
另外,还可以使用指针传递。
指针传参
func main() {
a := []int{1,2,3}
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
appendSlice(a)
fmt.Printf("len: %d cap: %d data: %+v\n", len(a), cap(a), a)
}
func appendSlice(a *[]int) {
*a = append(*a, 4)
}
// 输出结果:
// len: 3 cap: 3 data: [1 2 3]
// len: 4 cap: 6 data: [1 2 3 4]
Golang 语言中的指针 简约版 C 语言指针
Map
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // # live cells == size of map. Must be first (used by len() builtin)
flags uint8
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
hash0 uint32 // hash seed
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
extra *mapextra // optional fields
}
map 作为函数的参数传递时,建议使用指针的方式传递。
interface
type Stringer interface {
String() string
}
type Binary uint64
func (i Binary) String() string {
return strconv.FormatUint(i.Get(), 2)
}
func (i Binary) Get() uint64 {
return uint64(i)
}
func main() {
var b Binary = 200
s := Stringer(b)
fmt.Print(s.String())
}
chan
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex
}
chan 作为函数参数传递时,建议使用指针的方式传递。
03
编译器处理
Go 语言编译器与内存 和 C 语言编译器一样,Go 语言编译器也将 Go 代码转换为符合 Linux 进程内存规范的二进制代码:
逃逸分析
逃逸的几种典型情况
检查是否逃逸:go build -gcflags "-m"
type S struct {}
func main() {
var x S
_ = *ref(x)
}
func ref(z S) *S {
return &z
}
04
内存分配
内存分配算法的基本策略:
优秀的内存分配器必须要在性能和内存利用率之间做到平衡,Golang 语言的内存分配器使用的内存分配算法是 tcmalloc。
在 tcmalloc 内存管理内部又分为两部分:线程内存(thread memory)和页堆(page heap)。
Golang 语言的内存分配器由三种组件组成,
Golang 语言的内存分配器分配流程:
05
总结
本文开篇简要介绍了内存分布的相关知识,接着主要是介绍 Golang 语言数据类型的内存结构,最后介绍 Golang 语言的编译器和内存分配的知识。限于篇幅,本文未介绍垃圾回收(GC)相关的知识,我准备单开一篇文章来介绍。
本文重点是希望可以帮助读者了解 Golang 语言数据类型的内存结构,有助于在开发时避开一些隐藏的「坑」。
关于 Golang 编译器和内存分配的知识,有很多文章或书籍做了深入讲解,文末参考资料也列出一些,读者可以自行查找相关资料或查看 runtime 源码。推荐阅读「深入理解计算机系统」,可以帮助您更好地去理解Golang
语言的内存分配。
关注微信公众号,加入读者微信群
发送关键字「资料」,免费获取 Go 语言学习资料。
参考资料:
https://zh.wikipedia.org/wiki/虚拟内存
https://github.com/golang/go/tree/master/src/runtime
https://github.com/qyuhen/book
https://blog.csdn.net/K346K346/article/details/80849966
https://www.cnblogs.com/iiiiiher/p/12259162.html
https://qcon.infoq.cn/2019/guangzhou/schedule
https://blog.csdn.net/thisinnocence/article/details/84480669
https://qiankunli.github.io/2020/11/22/go_mm.html
?更多精彩内容,请点击「阅读原文」