go语言实现堆 go语言如何利用多核

【golang】内存逃逸常见情况和避免方式

因为如果变量的内存发生逃逸,它的生命周期就是不可知的,其会被分配到堆上,而堆上分配内存不能像栈一样会自动释放,为了解放程序员双手,专注于业务的实现,go实现了gc垃圾回收机制,但gc会影响程序运行性能,所以要尽量减少程序的gc操作。

在新和等地区,都构建了全面的区域性战略布局,加强发展的系统性、市场前瞻性、产品创新能力,以专注、极致的服务理念,为客户提供成都网站制作、成都做网站 网站设计制作按需定制制作,公司网站建设,企业网站建设,成都品牌网站建设,营销型网站建设,成都外贸网站建设,新和网站建设费用合理。

1、在方法内把局部变量指针返回,被外部引用,其生命周期大于栈,则溢出。

2、发送指针或带有指针的值到channel,因为编译时候无法知道那个goroutine会在channel接受数据,编译器无法知道什么时候释放。

3、在一个切片上存储指针或带指针的值。比如[]*string,导致切片内容逃逸,其引用值一直在堆上。

4、因为切片的append导致超出容量,切片重新分配地址,切片背后的存储基于运行时的数据进行扩充,就会在堆上分配。

5、在interface类型上调用方法,在Interface调用方法是动态调度的,只有在运行时才知道。

1、go语言的接口类型方法调用是动态,因此不能在编译阶段确定,所有类型结构转换成接口的过程会涉及到内存逃逸发生,在频次访问较高的函数尽量调用接口。

2、不要盲目使用变量指针作为参数,虽然减少了复制,但变量逃逸的开销更大。

3、预先设定好slice长度,避免频繁超出容量,重新分配。

Go 语言中如何使用堆 Heap

Go 提供了 container/heap 这个包来实现堆的操作。堆实际上是一个树的结构,每个元素的值都是它的子树中最小的,因此根节点 index = 0 的值是最小的,即最小堆。

堆也是实现优先队列 Priority Queue 的常用方式。

堆中元素的类型需要实现 heap.Interface 这个接口:

其中 sort.Interface 包括 Len() , Less , Swap 方法:

一个完整的示例如下:

注意注释中的一句话 Push 和 Pop 方法需要使用指针,因为它们会修改 slice 的长度,而不仅仅只内容 。

Leetcode 692. Top K Frequent Words 也可以使用 Go 语言通过构造 PriorityQueue 来实现:

go程序如何分配堆栈的

在Go语言中有一些调试技巧能帮助我们快速找到问题,有时候你想尽可能多的记录异常但仍觉得不够,搞清楚堆栈的意义有助于定位Bug或者记录更完整的信息。

本文将讨论堆栈跟踪信息以及如何在堆栈中识别函数所传递的参数。

Functions

先从这段代码开始:

Listing 1

01 package main

02

03 func main() {

04     slice := make([]string, 2, 4)

05     Example(slice, "hello", 10)

06 }

07

08 func Example(slice []string, str string, i int) {

09     panic("Want stack trace")

10 }

Example函数定义了3个参数,1个string类型的slice, 1个string和1个integer, 并且抛出了panic,运行这段代码可以看到这样的结果:

Listing 2

Panic: Want stack trace

goroutine 1 [running]:

main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:9 +0x64

main.main()

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:5 +0x85

goroutine 2 [runnable]:

runtime.forcegchelper()

/Users/bill/go/src/runtime/proc.go:90

runtime.goexit()

/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1

goroutine 3 [runnable]:

runtime.bgsweep()

/Users/bill/go/src/runtime/mgc0.go:82

runtime.goexit()

/Users/bill/go/src/runtime/asm_amd64.s:2232 +0x1

堆栈信息中显示了在panic抛出这个时间所有的goroutines状态,发生的panic的goroutine会显示在最上面。

Listing 3

01 goroutine 1 [running]:

02 main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:9 +0x64

03 main.main()

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:5 +0x85

第1行显示最先发出panic的是goroutine 1, 第二行显示panic位于main.Example中, 并能定位到该行代码,在本例中第9行引发了panic。

下面我们关注参数是如何传递的:

Listing 4

// Declaration

main.Example(slice []string, str string, i int)

// Call to Example by main.

slice := make([]string, 2, 4)

Example(slice, "hello", 10)

// Stack trace

main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

这里展示了在main中带参数调用Example函数时的堆栈信息,比较就能发现两者的参数数量并不相同,Example定义了3个参数,堆栈中显示了6个参数。现在的关键问题是我们要弄清楚它们是如何匹配的。

第1个参数是string类型的slice,我们知道在Go语言中slice是引用类型,即slice变量结构会包含三个部分:指针、长度(Lengthe)、容量(Capacity)

Listing 5

// Slice parameter value

slice := make([]string, 2, 4)

// Slice header values

Pointer:  0x2080c3f50

Length:   0x2

Capacity: 0x4

// Declaration

main.Example(slice []string, str string, i int)

// Stack trace

main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

因此,前面3个参数会匹配slice, 如下图所示:

Figure 1

figure provided by Georgi Knox

我们现在来看第二个参数,它是string类型,string类型也是引用类型,它包括两部分:指针、长度。

Listing 6

// String parameter value

"hello"

// String header values

Pointer: 0x425c0

Length:  0x5

// Declaration

main.Example(slice []string, str string, i int)

// Stack trace

main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

可以确定,堆栈信息中第4、5两个参数对应代码中的string参数,如下图所示:

Figure 2

figure provided by Georgi Knox

最后一个参数integer是single word值。

Listing 7

// Integer parameter value

10

// Integer value

Base 16: 0xa

// Declaration

main.Example(slice []string, str string, i int)

// Stack trace

main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)

现在我们可以匹配代码中的参数到堆栈信息了。

Figure 3

figure provided by Georgi Knox

Methods

如果我们将Example作为结构体的方法会怎么样呢?

Listing 8

01 package main

02

03 import "fmt"

04

05 type trace struct{}

06

07 func main() {

08     slice := make([]string, 2, 4)

09

10     var t trace

11     t.Example(slice, "hello", 10)

12 }

13

14 func (t *trace) Example(slice []string, str string, i int) {

15     fmt.Printf("Receiver Address: %p\n", t)

16     panic("Want stack trace")

17 }

如上所示修改代码,将Example定义为trace的方法,并通过trace的实例t来调用Example。

再次运行程序,会发现堆栈信息有一点不同:

Listing 9

Receiver Address: 0x1553a8

panic: Want stack trace

01 goroutine 1 [running]:

02 main.(*trace).Example(0x1553a8, 0x2081b7f50, 0x2, 0x4, 0xdc1d0, 0x5, 0xa)

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:16 +0x116

03 main.main()

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:11 +0xae

首先注意第2行的方法调用使用了pointer receiver,在package名字和方法名之间多出了"*trace"字样。另外,参数列表的第1个参数标明了结构体(t)地址。我们从堆栈信息中看到了内部实现细节。

Packing

如果有多个参数可以填充到一个single word, 则这些参数值会合并打包:

Listing 10

01 package main

02

03 func main() {

04     Example(true, false, true, 25)

05 }

06 

07 func Example(b1, b2, b3 bool, i uint8) {

08     panic("Want stack trace")

09 }

这个例子修改Example函数为4个参数:3个bool型和1个八位无符号整型。bool值也是用8个bit表示,所以在32位和64位架构下,4个参数可以合并为一个single word。

Listing 11

01 goroutine 1 [running]:

02 main.Example(0x19010001)

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:8 +0x64

03 main.main()

/Users/bill/Spaces/Go/Projects/src/github点抗 /goinaction/code/

temp/main.go:4 +0x32

这是本例的堆栈信息,看下图的具体分析:

Listing 12

// Parameter values

true, false, true, 25

// Word value

Bits    Binary      Hex   Value

00-07   0000 0001   01    true

08-15   0000 0000   00    false

16-23   0000 0001   01    true

24-31   0001 1001   19    25

// Declaration

main.Example(b1, b2, b3 bool, i uint8)

// Stack trace

main.Example(0x19010001)

以上展示了参数值是如何匹配到4个参数的。当我们看到堆栈信息中包括十六进制值,需要知道这些值是如何传递的。

Golang 1.14中内存分配、清扫和内存回收

Golang的内存分配是由golang runtime完成,其内存分配方案借鉴自tcmalloc。

主要特点就是

本文中的element指一定大小的内存块是内存分配的概念,并为出现在golang runtime源码中

本文讲述x8664架构下的内存分配

Golang 内存分配有下面几个主要结构

Tiny对象是指内存尺寸小于16B的对象,这类对象的分配使用mcache的tiny区域进行分配。当tiny区域空间耗尽时刻,它会从mcache.alloc[tinySpanClass]指向的mspan中找到空闲的区域。当然如果mcache中span空间也耗尽,它会触发从mcentral补充mspan到mcache的流程。

小对象是指对象尺寸在(16B,32KB]之间的对象,这类对象的分配原则是:

1、首先根据对象尺寸将对象归为某个SpanClass上,这个SpanClass上所有的element都是一个统一的尺寸。

2、从mcache.alloc[SpanClass]找到mspan,看看有无空闲的element,如果有分配成功。如果没有继续。

3、从mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合适的mspan,返回给mcache。如果没有找到就进入mcentral.grow()—mheap.alloc()分配新的mspan给mcentral。

大对象指尺寸超出32KB的对象,此时直接从mheap中分配,不会走mcache和mcentral,直接走mheap.alloc()分配一个SpanClass==0 的mspan表示这部分分配空间。

对于程序分配常用的tiny和小对象的分配,可以通过无锁的mcache提升分配性能。mcache不足时刻会拿mcentral的锁,然后从mcentral中充mspan 给mcache。大对象直接从mheap 中分配。

在x8664环境上,golang管理的有效的程序虚拟地址空间实质上只有48位。在mheap中有一个pages pageAlloc成员用于管理golang堆内存的地址空间。golang从os中申请地址空间给自己管理,地址空间申请下来以后,golang会将地址空间根据实际使用情况标记为free或者alloc。如果地址空间被分配给mspan或大对象后,那么被标记为alloc,反之就是free。

Golang认为地址空间有以下4种状态:

Golang同时定义了下面几个地址空间操作函数:

在mheap结构中,有一个名为pages成员,它用于golang 堆使用虚拟地址空间进行管理。其类型为pageAlloc

pageAlloc 结构表示的golang 堆的所有地址空间。其中最重要的成员有两个:

在golang的gc流程中会将未使用的对象标记为未使用,但是这些对象所使用的地址空间并未交还给os。地址空间的申请和释放都是以golang的page为单位(实际以chunk为单位)进行的。sweep的最终结果只是将某个地址空间标记可被分配,并未真正释放地址空间给os,真正释放是后文的scavenge过程。

在gc mark结束以后会使用sweep()去尝试free一个span;在mheap.alloc 申请mspan时刻,也使用sweep去清扫一下。

清扫mspan主要涉及到下面函数

如上节所述,sweep只是将page标记为可分配,但是并未把地址空间释放;真正的地址空间释放是scavenge过程。

真正的scavenge是由pageAlloc.scavenge()—sysUnused()将扫描到待释放的chunk所表示的地址空间释放掉(使用sysUnused()将地址空间还给os)

golang的scavenge过程有两种:


本文标题:go语言实现堆 go语言如何利用多核
当前链接:http://hbruida.cn/article/ddjgsgh.html