欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Golang學(xué)習(xí)之內(nèi)存逃逸分析

 更新時(shí)間:2023年01月29日 10:35:18   作者:某不知名Gopher  
內(nèi)存逃逸分析是go的編譯器在編譯期間,根據(jù)變量的類型和作用域,確定變量是堆上還是棧上。本文將帶大家分析一下Golang中的內(nèi)存逃逸,需要的可以了解一下

在開始剖析Go逃逸分析前,我們要先清楚什么是堆棧。數(shù)據(jù)結(jié)構(gòu)中有堆棧,內(nèi)存分配中也有堆棧,兩者在定義和用途上雖不同,但也有些許關(guān)聯(lián),內(nèi)存分配中棧的壓棧和出棧操作,類似于數(shù)據(jù)結(jié)構(gòu)中的棧的操作方式

內(nèi)存分配中的堆棧

程序在運(yùn)行過程中,必不可少的會(huì)使用變量、函數(shù)和數(shù)據(jù),變量和數(shù)據(jù)在內(nèi)存中存儲(chǔ)的位置可以分為:堆區(qū)(Heap)和棧區(qū)(Stack),一般由C或C++編譯的程序占用內(nèi)存為:

  • 棧區(qū)
  • 堆區(qū)
  • 全局區(qū)
  • 常量區(qū)
  • 程序代碼區(qū)

軟件程序中的數(shù)據(jù)和變量都會(huì)被分配到程序所在的虛擬內(nèi)存空間中

每個(gè)函數(shù)都有自己獨(dú)立的棧空間,函數(shù)的調(diào)用參數(shù)、返回值以及局部變量大都被分配到該函數(shù)的??臻g中, 這部分內(nèi)存由編譯器進(jìn)行管理,編譯時(shí)確定分配內(nèi)存的大小。??臻g有特定的結(jié)構(gòu)和尋址方式,所以尋址十分迅速、開銷小,只需要2條 CPU 指令,即壓棧出棧 PUSH 和 RELEASE,由于函數(shù)棧內(nèi)存的大小在編譯時(shí)確定, 所以當(dāng)局部變量數(shù)據(jù)太大就會(huì)發(fā)生棧溢出(Stack Overflow)。當(dāng)函數(shù)執(zhí)行完畢后, 函數(shù)的??臻g被回收, 無需手動(dòng)去釋放。

區(qū)別于堆空間,通過 malloc 出來的內(nèi)存,函數(shù)執(zhí)行完畢后需要“手動(dòng)”釋放,“手動(dòng)”釋放在有垃圾回收的語言中,表現(xiàn)為垃圾回收系統(tǒng),比如 Golang 語言的 GC 系統(tǒng),GC 系統(tǒng)通過標(biāo)記等手段,識(shí)別出需要回收的空間。

堆空間沒有特定的結(jié)構(gòu),也沒有固定的大小,可以動(dòng)態(tài)進(jìn)行分配和調(diào)整,所以內(nèi)存占用較大的局部變量會(huì)放在堆空間上,在編譯時(shí)不知道該分配多少大小的變量,在運(yùn)行時(shí)也會(huì)分配到堆上,在堆上分配內(nèi)存開銷比在棧上大,而且堆上分配的內(nèi)存需要手動(dòng)釋放,對于 Golang 這種有 GC 機(jī)制的語言, 也會(huì)增加 GC 壓力, 也容易造成內(nèi)存碎片。

注:棧是線程級(jí)的,堆是進(jìn)程級(jí)的

內(nèi)存逃逸

所謂內(nèi)存逃逸,就是本該分配于??臻g的變量,被分配到了堆空間,過多的內(nèi)存逃逸會(huì)導(dǎo)致GC壓力變大,堆空間碎片化。

Go語言中,變量不能顯示的指定分配在棧空間還是堆空間,但是官方回復(fù)中大致表示了一個(gè)原則:如果局部變量被其他函數(shù)捕獲,那么就分配在堆上。

逃逸分析

在編程語言的編譯優(yōu)化原理中,分析指針動(dòng)態(tài)范圍的方法稱之為逃逸分析,通俗來說,當(dāng)一個(gè)對象的指針被多個(gè)方法或線程引用時(shí),我們稱這個(gè)指針發(fā)生了逃逸。逃逸分析有兩個(gè)基本的不變性:

  • 指向棧對象的指針不能存儲(chǔ)在堆中
  • 指向棧對象的指針不能超過該棧對象的存活期(即指針不能在棧對象被銷毀后依舊存活)

分析工具

通過編譯工具查看詳細(xì)的逃逸分析過程 go build -gcflags '-m -l' xxx.go 編譯參數(shù)(-gcflags):

  • -N:禁止編譯優(yōu)化
  • -l:禁止內(nèi)聯(lián)
  • -m:逃逸分析
  • -benchmem:壓測時(shí)打印內(nèi)存分配統(tǒng)計(jì)

通過逃逸分析判斷一個(gè)變量到底是分配在堆上還是棧上

逃逸場景

指針逃逸

指針逃逸應(yīng)該是最容易理解的一種情況了,即在函數(shù)中創(chuàng)建了一個(gè)對象,返回了這個(gè)對象的指針。這種情況下,函數(shù)雖然退出了,但是因?yàn)橹羔樀拇嬖?,對象的?nèi)存不能隨著函數(shù)結(jié)束而回收,因此只能分配在堆上。

// main.go
package main

import "fmt"

type Demo struct {
	name string
}

func createDemo(name string) *Demo {
	d := new(Demo) // 局部變量 d 逃逸到堆
	d.name = name
	return d
}

func main() {
	demo := createDemo("demo")
	fmt.Println(demo)
}

在這個(gè)例子中,函數(shù)createDemo的局部變量d發(fā)生了逃逸,d作為返回值在main函數(shù)中繼續(xù)使用,因此d指向的內(nèi)存不能分配在棧上,只能分配在堆上,借助分析工具查看逃逸情況

    $ go build -gcflags=-m main.go 
    ./main.go:10:6: can inline createDemo
    ./main.go:17:20: inlining call to createDemo
    ./main.go:18:13: inlining call to fmt.Println
    ./main.go:10:17: leaking param: name
    ./main.go:11:10: new(Demo) escapes to heap
    ./main.go:17:20: new(Demo) escapes to heap   //指針逃逸
    ./main.go:18:13: demo escapes to heap        //interface{}動(dòng)態(tài)類型逃逸
    ./main.go:18:13: main []interface {} literal does not escape
    ./main.go:18:13: io.Writer(os.Stdout) escapes to heap
    <autogenerated>:1: (*File).close .this does not escape

escapes to heap表示逃逸到堆上了

動(dòng)態(tài)反射interface{}變量

在 Go 語言中,接口即 interface{} 可以表示任意的類型,如果函數(shù)參數(shù)為 interface{},編譯期間很難確定其參數(shù)的具體類型,也會(huì)發(fā)生逃逸。仍以上面的例子

func main() {
   demo := createDemo("demo")
   fmt.Println(demo)
}

./main.go:18:13: demo escapes to heap

demo是main函數(shù)的一個(gè)局部變量,該變量作為實(shí)參傳遞給fmt.Println(),但是因?yàn)?code>fmt.Println()的參數(shù)類型是interface{},因此也發(fā)生了逃逸

解釋fmt.Println 之類的底層系統(tǒng)函數(shù),實(shí)現(xiàn)邏輯會(huì)基于interface{} 做反射,通過 reflect.TypeOf(arg).Kind() 獲取接口對象的底層數(shù)據(jù)類型,創(chuàng)建具體類型對象時(shí),會(huì)發(fā)生內(nèi)存逃逸。由于 interface{} 的變量,編譯時(shí)無法確定變量類型以及申請空間大小,所以不能在??臻g上申請內(nèi)存,需要在 runtime 時(shí)動(dòng)態(tài)申請,理所應(yīng)當(dāng)?shù)匕l(fā)生內(nèi)存逃逸。

申請棧空間過大

棧空間大小是有限的,如果編譯時(shí)發(fā)現(xiàn)局部變量申請的空間過大,則會(huì)發(fā)生內(nèi)存逃逸,在堆空間上給大變量分配內(nèi)存

func main() {
   num := make([]int, 0, 10000)
   _ = num
}

.\main.go:404:13: make([]int, 0, 10000) escapes to heap   //發(fā)生逃逸

經(jīng)過測試,num := make([]int, 0, 8193) 時(shí)剛好發(fā)生內(nèi)存逃逸。在 64 位機(jī)上 int 類型為 8B,即 8192 * 8B = 64KB

func main() {
   num1 := make([]int, 0, 8192)
   _ = num1
   
   num2 := make([]int, 0, 8193)
   _ = num2
}

.\main.go:404:14: make([]int, 0, 8192) does not escape
.\main.go:407:14: make([]int, 0, 8193) escapes to heap

切片變量自身和元素的逃逸

1.未指定slice的lencap時(shí),slice自身未發(fā)生逃逸,slice的元素發(fā)生逃逸。因此slice會(huì)動(dòng)態(tài)擴(kuò)容,編譯器不知道容量大小,無法提前在棧空間分配內(nèi)存,擴(kuò)容后slice的元素可能會(huì)被分配到堆空間,所以slice容器自身也不能被分配到??臻g

type person struct {
   Name string
}

func main() {
   var num []*person
   p1 := &person{
      Name: "ss",
   }
   num = append(num, p1)
}

.\main.go:409:8: &person{...} escapes to heap

2.只指定slice的長度即array,數(shù)組本身和元素均在棧上分配,均未發(fā)生逃逸

閉包

所謂閉包,就是函數(shù)與其所處環(huán)境捆綁的組合,也就是說,閉包可以讓你在一個(gè)內(nèi)部函數(shù)中訪問到其外部函數(shù)的作用域

func Increase() func() int {
	n := 0
	return func() int {
		n++
		return n
	}
}

func main() {
	in := Increase()
	fmt.Println(in()) // 1
	fmt.Println(in()) // 2
}

Increase() 返回值是一個(gè)閉包函數(shù),該閉包函數(shù)訪問了外部變量 n,那變量 n 將會(huì)一直存在,直到 in 被銷毀。很顯然,變量 n 占用的內(nèi)存不能隨著函數(shù) Increase() 的退出而回收,因此將會(huì)逃逸到堆上。

.\main.go:408:2: moved to heap: n
.\main.go:409:9: func literal escapes to heap
.\main.go:417:13: ... argument does not escape
.\main.go:417:16: in() escapes to heap
.\main.go:418:13: ... argument does not escape
.\main.go:418:16: in() escapes to heap

逃逸分析的作用

  • 通過逃逸分析能確定哪些變量分配到??臻g,哪些分配到堆空間,對空間需要 GC 系統(tǒng)回收資源,GC 系統(tǒng)會(huì)有微秒級(jí)的 STW,降低 GC 的壓力能提高系統(tǒng)的運(yùn)行效率。
  • ??臻g的分配比堆空間更快性能更好,對于熱點(diǎn)數(shù)據(jù)分配到棧上能提高接口的響應(yīng)。
  • ??臻g分配的內(nèi)存,在函數(shù)執(zhí)行完畢后由系統(tǒng)回收資源,不需要 GC 系統(tǒng)參與,也不需要 GC 標(biāo)記清除,可降低內(nèi)存的占用

到此這篇關(guān)于Golang學(xué)習(xí)之內(nèi)存逃逸分析的文章就介紹到這了,更多相關(guān)Golang內(nèi)存逃逸內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go語言時(shí)間處理必備技巧全解析

    Go語言時(shí)間處理必備技巧全解析

    Golang 的時(shí)間處理是 Golang 編程中的一個(gè)重要方面,它涉及到了時(shí)間類型、時(shí)間格式化、時(shí)間計(jì)算、時(shí)區(qū)處理以及定時(shí)器和超時(shí)機(jī)制等多個(gè)方面。在本文中,我們將從更深入的角度來探討 Golang 的時(shí)間處理
    2023-04-04
  • 揭秘Go Json.Unmarshal精度丟失之謎

    揭秘Go Json.Unmarshal精度丟失之謎

    我們知道在json反序列化時(shí)是沒有整型和浮點(diǎn)型的區(qū)別,數(shù)字都使用同一種類型,在go語言的類型中這種共同類型就是float64,下面我們就來探討一下Json.Unmarshal精度丟失之謎吧
    2023-08-08
  • go項(xiàng)目中環(huán)境變量的配置

    go項(xiàng)目中環(huán)境變量的配置

    本文主要介紹了go項(xiàng)目中環(huán)境變量的配置,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-07-07
  • Golang常量iota的使用實(shí)例

    Golang常量iota的使用實(shí)例

    今天小編就為大家分享一篇關(guān)于Golang常量iota的使用實(shí)例,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧
    2019-01-01
  • golang?JSON序列化和反序列化示例詳解

    golang?JSON序列化和反序列化示例詳解

    通過使用Go語言的encoding/json包,你可以輕松地處理JSON數(shù)據(jù),無論是在客戶端應(yīng)用、服務(wù)器端應(yīng)用還是其他類型的Go程序中,這篇文章主要介紹了golang?JSON序列化和反序列化,需要的朋友可以參考下
    2024-04-04
  • Go1.21新增slices包中函數(shù)的用法詳解

    Go1.21新增slices包中函數(shù)的用法詳解

    Go?1.21新增的?slices?包提供了很多和切片相關(guān)的函數(shù),可以用于任何類型的切片,本文為大家整理了部分函數(shù)的具體用法,感興趣的小伙伴可以了解一下
    2023-08-08
  • gtoken替換jwt實(shí)現(xiàn)sso登錄的問題小結(jié)

    gtoken替換jwt實(shí)現(xiàn)sso登錄的問題小結(jié)

    這篇文章主要介紹了gtoken替換jwt實(shí)現(xiàn)sso登錄,主要介紹了替換jwt的原因分析及gtoken的優(yōu)勢,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-05-05
  • Golang之defer 延遲調(diào)用操作

    Golang之defer 延遲調(diào)用操作

    這篇文章主要介紹了Golang之defer 延遲調(diào)用操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-12-12
  • go的strings用法小結(jié)

    go的strings用法小結(jié)

    strings 是 Go 語言標(biāo)準(zhǔn)庫中提供的一個(gè)包,用于處理字符串相關(guān)的操作,本文主要介紹了go的strings用法小結(jié),具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-08-08
  • Go prometheus metrics條目自動(dòng)回收與清理方法

    Go prometheus metrics條目自動(dòng)回收與清理方法

    這篇文章主要為大家介紹了Go prometheus metrics條目自動(dòng)回收與清理方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-11-11

最新評論