av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

面試官:小松子來聊一聊內(nèi)存逃逸

本文轉(zhuǎn)載自微信公眾號(hào)「Golang夢工廠」,作者AsongGo。轉(zhuǎn)載本文請聯(lián)系Golang夢工廠公眾號(hào)。

專注于為中小企業(yè)提供網(wǎng)站設(shè)計(jì)制作、網(wǎng)站設(shè)計(jì)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)新興免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了近1000家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。

前言

哈嘍,大家好,我是asong。最近無聊看了一下Go語言的面試八股文,發(fā)現(xiàn)面試官都喜歡問內(nèi)存逃逸這個(gè)話題,這個(gè)激起了我的興趣,我對(duì)內(nèi)存逃逸的了解很淺,所以找了很多文章精讀了一下,在這里做一個(gè)總結(jié),方便日后查閱、學(xué)習(xí)。

什么是內(nèi)存逃逸

初次看到這個(gè)話題,我是懵逼的,怎么還有內(nèi)存逃逸,內(nèi)存逃逸到底是干什么的?接下來我們一起來看看什么是內(nèi)存逃逸。

我們都知道一般情況下程序存放在rom或者Flash中,運(yùn)行時(shí)需要拷貝到內(nèi)存中執(zhí)行,內(nèi)存會(huì)分別存儲(chǔ)不同的信息,內(nèi)存空間包含兩個(gè)最重要的區(qū)域:堆區(qū)(Stack)和棧區(qū)(Heap),對(duì)于我這種C語言出身的人,對(duì)堆內(nèi)存和棧內(nèi)存的了解還是挺深的。在C語言中,棧區(qū)域會(huì)專門存放函數(shù)的參數(shù)、局部變量等,棧的地址從內(nèi)存高地址往低地址增長,而堆內(nèi)存正好相反,堆地址從內(nèi)存低地址往高地址增長,但是如果我們想在堆區(qū)域分配內(nèi)存需要我們手動(dòng)調(diào)用malloc函數(shù)去堆區(qū)域申請內(nèi)存分配,然后我使用完了還需要自己手動(dòng)釋放,如果沒有釋放就會(huì)導(dǎo)致內(nèi)存泄漏。寫過C語言的朋友應(yīng)該都知道C語言函數(shù)是不能返回局部變量地址(特指存放于棧區(qū)的局部變量地址),除非是局部靜態(tài)變量地址,字符串常量地址、動(dòng)態(tài)分配地址。其原因是一般局部變量的作用域只在函數(shù)內(nèi),其存儲(chǔ)位置在棧區(qū)中,當(dāng)程序調(diào)用完函數(shù)后,局部變量會(huì)隨此函數(shù)一起被釋放。其地址指向的內(nèi)容不明(原先的數(shù)值可能不變,也可能改變)。而局部靜態(tài)變量地址和字符串常量地址存放在數(shù)據(jù)區(qū),動(dòng)態(tài)分配地址存放在堆區(qū),函數(shù)運(yùn)行結(jié)束后只會(huì)釋放棧區(qū)的內(nèi)容,而不會(huì)改變數(shù)據(jù)區(qū)和堆區(qū)。

所以在C語言中我們想在一個(gè)函數(shù)中返回局部變量地址時(shí),有三個(gè)正確的方式:返回靜態(tài)局部變量地址、返回字符串常量地址,返回動(dòng)態(tài)分配在堆上的地址,因?yàn)樗麄兌疾辉跅^(qū),即使釋放函數(shù),其內(nèi)容也不會(huì)受影響,我們以在返回堆上內(nèi)存地址為例看一段代碼:

 
 
 
  1. #include "stdio.h"
  2. #include "stdlib.h"
  3. //返回動(dòng)態(tài)分配的地址 
  4. int* f1()
  5. {
  6.     int a = 9;
  7.     int *pa = (int*) malloc(8);
  8.     *pa = a;
  9.     return pa;
  10. }
  11. int main()
  12. {
  13.     int *pb;
  14.     pb = f1();
  15.     printf("after : *pb = %d\tpb = %p\n",*pb, pb);
  16.     free(pb);
  17.     return 1;
  18. }

通過上面的例子我們知道在C語言中動(dòng)態(tài)內(nèi)存的分配與釋放完全交與程序員的手中,這樣就會(huì)導(dǎo)致我們在寫程序時(shí)如履薄冰,好處是我們可以完全掌控內(nèi)存,缺點(diǎn)是我們一不小心就會(huì)導(dǎo)致內(nèi)存泄漏,所以很多現(xiàn)代語言都有GC機(jī)制,Go就是一門帶垃圾回收的語言,真正解放了我們程序員的雙手,我們不需要在像寫C語言那樣考慮是否能返回局部變量地址了,內(nèi)存管理交與給編譯器,編譯器會(huì)經(jīng)過逃逸分析把變量合理的分配到"正確"的地方。

說到這里,可以簡單總結(jié)一下什么是內(nèi)存逃逸了:

在一段程序中,每一個(gè)函數(shù)都會(huì)有自己的內(nèi)存區(qū)域存放自己的局部變量、返回地址等,這些內(nèi)存會(huì)由編譯器在棧中進(jìn)行分配,每一個(gè)函數(shù)都會(huì)分配一個(gè)棧楨,在函數(shù)運(yùn)行結(jié)束后進(jìn)行銷毀,但是有些變量我們想在函數(shù)運(yùn)行結(jié)束后仍然使用它,那么就需要把這個(gè)變量在堆上分配,這種從"棧"上逃逸到"堆"上的現(xiàn)象就成為內(nèi)存逃逸。

什么是逃逸分析

上面我們知道了什么是內(nèi)存逃逸,下面我們就來看一看什么是逃逸分析?

上文我們說到C語言使用malloc在堆上動(dòng)態(tài)分配內(nèi)存后,還需要手動(dòng)調(diào)用free釋放內(nèi)存,如果不釋放就會(huì)造成內(nèi)存泄漏的風(fēng)險(xiǎn)。在Go語言中堆內(nèi)存的分配與釋放完全不需要我們?nèi)ス芰?,Go語言引入了GC機(jī)制,GC機(jī)制會(huì)對(duì)位于堆上的對(duì)象進(jìn)行自動(dòng)管理,當(dāng)某個(gè)對(duì)象不可達(dá)時(shí)(即沒有其對(duì)象引用它時(shí)),他將會(huì)被回收并被重用。雖然引入GC可以讓開發(fā)人員降低對(duì)內(nèi)存管理的心智負(fù)擔(dān),但是GC也會(huì)給程序帶來性能損耗,當(dāng)堆內(nèi)存中有大量待掃描的堆內(nèi)存對(duì)象時(shí),將會(huì)給GC帶來過大的壓力,雖然Go語言使用的是標(biāo)記清除算法,并且在此基礎(chǔ)上使用了三色標(biāo)記法和寫屏障技術(shù),提高了效率,但是如果我們的程序仍在堆上分配了大量內(nèi)存,依賴會(huì)對(duì)GC造成不可忽視的壓力。因此為了減少GC造成的壓力,Go語言引入了逃逸分析,也就是想法設(shè)法盡量減少在堆上的內(nèi)存分配,可以在棧中分配的變量盡量留在棧中。

小結(jié)逃逸分析:

逃逸分析就是指程序在編譯階段根據(jù)代碼中的數(shù)據(jù)流,對(duì)代碼中哪些變量需要在棧中分配,哪些變量需要在堆上分配進(jìn)行靜態(tài)分析的方法。堆和棧相比,堆適合不可預(yù)知大小的內(nèi)存分配。但是為此付出的代價(jià)是分配速度較慢,而且會(huì)形成內(nèi)存碎片。棧內(nèi)存分配則會(huì)非常快。棧分配內(nèi)存只需要兩個(gè)CPU指令:“PUSH”和“RELEASE”,分配和釋放;而堆分配內(nèi)存首先需要去找到一塊大小合適的內(nèi)存塊,之后要通過垃圾回收才能釋放。所以逃逸分析更做到更好內(nèi)存分配,提高程序的運(yùn)行速度。

Go語言中的逃逸分析

Go語言的逃逸分析總共實(shí)現(xiàn)了兩個(gè)版本:

  • 1.13版本前是第一版
  • 1.13版本后是第二版

粗略看了一下逃逸分析的代碼,大概有1500+行(go1.15.7)。代碼我倒是沒仔細(xì)看,注釋我倒是仔細(xì)看了一遍,注釋寫的還是很詳細(xì)的,代碼路徑:src/cmd/compile/internal/gc/escape.go,大家可以自己看一遍注釋,其逃逸分析原理如下:

  • pointers to stack objects cannot be stored in the heap:指向棧對(duì)象的指針不能存儲(chǔ)在堆中
  • pointers to a stack object cannot outlive that object:指向棧對(duì)象的指針不能超過該對(duì)象的存活期,也就說指針不能在棧對(duì)象被銷毀后依舊存活。(例子:聲明的函數(shù)返回并銷毀了對(duì)象的棧幀,或者它在循環(huán)迭代中被重復(fù)用于邏輯上不同的變量)

我們大概知道它的分析準(zhǔn)則是什么就好了,具體逃逸分析是怎么做的,感興趣的同學(xué)可以根據(jù)源碼自行研究。

既然逃逸分析是在編譯階段進(jìn)行的,那我們就可以通過go build -gcflags '-m -m -l'命令查看到逃逸分析的結(jié)果,我們之前在分析內(nèi)聯(lián)優(yōu)化時(shí)使用的-gcflags '-m -m',能看到所有的編譯器優(yōu)化,這里使用-l禁用掉內(nèi)聯(lián)優(yōu)化,只關(guān)注逃逸優(yōu)化就好了。

現(xiàn)在我們也知道了逃逸分析,接下來我們就看幾個(gè)逃逸分析的例子。

幾個(gè)逃逸分析的例子

1. 函數(shù)返回局部指針變量

先看例子:

 
 
 
  1. #include "stdio.h"
  2. #include "stdlib.h"
  3. //返回動(dòng)態(tài)分配的地址 
  4. int* f1()
  5. {
  6.     int a = 9;
  7.     int *pa = (int*) malloc(8);
  8.     *pa = a;
  9.     return pa;
  10. }
  11. int main()
  12. {
  13.     int *pb;
  14.     pb = f1();
  15.     printf("after : *pb = %d\tpb = %p\n",*pb, pb);
  16.     free(pb);
  17.     return 1;
  18. }

查看逃逸分析結(jié)果:

 
 
 
  1. go build -gcflags="-m -m -l" ./test1.go
  2. # command-line-arguments
  3. ./test1.go:6:9: &res escapes to heap
  4. ./test1.go:6:9:         from ~r2 (return) at ./test1.go:6:2
  5. ./test1.go:4:2: moved to heap: res

分析結(jié)果很明了,函數(shù)返回的局部變量是一個(gè)指針變量,當(dāng)函數(shù)Add執(zhí)行結(jié)束后,對(duì)應(yīng)的棧楨就會(huì)被銷毀,但是引用已經(jīng)返回到函數(shù)之外,如果我們在外部解引用地址,就會(huì)導(dǎo)致程序訪問非法內(nèi)存,就像上面的C語言的例子一樣,所以編譯器經(jīng)過逃逸分析后將其在堆上分配內(nèi)存。

2. interface類型逃逸

先看一個(gè)例子:

 
 
 
  1. func main()  {
  2.  str := "asong太帥了吧"
  3.  fmt.Printf("%v",str)
  4. }

查看逃逸分析結(jié)果:

 
 
 
  1. go build -gcflags="-m -m -l" ./test2.go 
  2. # command-line-arguments
  3. ./test2.go:9:13: str escapes to heap
  4. ./test2.go:9:13:        from ... argument (arg to ...) at ./test2.go:9:13
  5. ./test2.go:9:13:        from *(... argument) (indirection) at ./test2.go:9:13
  6. ./test2.go:9:13:        from ... argument (passed to call[argument content escapes]) at ./test2.go:9:13
  7. ./test2.go:9:13: main ... argument does not escape

str是main函數(shù)中的一個(gè)局部變量,傳遞給fmt.Println()函數(shù)后發(fā)生了逃逸,這是因?yàn)閒mt.Println()函數(shù)的入?yún)⑹且粋€(gè)interface{}類型,如果函數(shù)參數(shù)為interface{},那么在編譯期間就很難確定其參數(shù)的具體類型,也會(huì)發(fā)送逃逸。

觀察這個(gè)分析結(jié)果,我們可以看到?jīng)]有moved to heap: str,這也就是說明str變量并沒有在堆上進(jìn)行分配,只是它存儲(chǔ)的值逃逸到堆上了,也就說任何被str引用的對(duì)象必須分配在堆上。如果我們把代碼改成這樣:

 
 
 
  1. func main()  {
  2.  str := "asong太帥了吧"
  3.  fmt.Printf("%p",&str)
  4. }

查看逃逸分析結(jié)果:

 
 
 
  1. go build -gcflags="-m -m -l" ./test2.go
  2. # command-line-arguments
  3. ./test2.go:9:18: &str escapes to heap
  4. ./test2.go:9:18:        from ... argument (arg to ...) at ./test2.go:9:12
  5. ./test2.go:9:18:        from *(... argument) (indirection) at ./test2.go:9:12
  6. ./test2.go:9:18:        from ... argument (passed to call[argument content escapes]) at ./test2.go:9:12
  7. ./test2.go:9:18: &str escapes to heap
  8. ./test2.go:9:18:        from &str (interface-converted) at ./test2.go:9:18
  9. ./test2.go:9:18:        from ... argument (arg to ...) at ./test2.go:9:12
  10. ./test2.go:9:18:        from *(... argument) (indirection) at ./test2.go:9:12
  11. ./test2.go:9:18:        from ... argument (passed to call[argument content escapes]) at ./test2.go:9:12
  12. ./test2.go:8:2: moved to heap: str
  13. ./test2.go:9:12: main ... argument does not escape

這回str也逃逸到了堆上,在堆上進(jìn)行內(nèi)存分配,這是因?yàn)槲覀冊L問str的地址,因?yàn)槿雲(yún)⑹莍nterface類型,所以變量str的地址以實(shí)參的形式傳入fmt.Printf后被裝箱到一個(gè)interface{}形參變量中,裝箱的形參變量的值要在堆上分配,但是還要存儲(chǔ)一個(gè)棧上的地址,也就是str的地址,堆上的對(duì)象不能存儲(chǔ)一個(gè)棧上的地址,所以str也逃逸到堆上,在堆上分配內(nèi)存。(這里注意一個(gè)知識(shí)點(diǎn):Go語言的參數(shù)傳遞只有值傳遞)

3. 閉包產(chǎn)生的逃逸

 
 
 
  1. func Increase() func() int {
  2.  n := 0
  3.  return func() int {
  4.   n++
  5.   return n
  6.  }
  7. }
  8. func main() {
  9.  in := Increase()
  10.  fmt.Println(in()) // 1
  11. }

查看逃逸分析結(jié)果:

 
 
 
  1. go build -gcflags="-m -m -l" ./test3.go
  2. # command-line-arguments
  3. ./test3.go:10:3: Increase.func1 capturing by ref: n (addr=true assign=true width=8)
  4. ./test3.go:9:9: func literal escapes to heap
  5. ./test3.go:9:9:         from ~r0 (assigned) at ./test3.go:7:17
  6. ./test3.go:9:9: func literal escapes to heap
  7. ./test3.go:9:9:         from &(func literal) (address-of) at ./test3.go:9:9
  8. ./test3.go:9:9:         from ~r0 (assigned) at ./test3.go:7:17
  9. ./test3.go:10:3: &n escapes to heap
  10. ./test3.go:10:3:        from func literal (captured by a closure) at ./test3.go:9:9
  11. ./test3.go:10:3:        from &(func literal) (address-of) at ./test3.go:9:9
  12. ./test3.go:10:3:        from ~r0 (assigned) at ./test3.go:7:17
  13. ./test3.go:8:2: moved to heap: n
  14. ./test3.go:17:16: in() escapes to heap
  15. ./test3.go:17:16:       from ... argument (arg to ...) at ./test3.go:17:13
  16. ./test3.go:17:16:       from *(... argument) (indirection) at ./test3.go:17:13
  17. ./test3.go:17:16:       from ... argument (passed to call[argument content escapes]) at ./test3.go:17:13
  18. ./test3.go:17:13: main ... argument does not escape

因?yàn)楹瘮?shù)也是一個(gè)指針類型,所以匿名函數(shù)當(dāng)作返回值時(shí)也發(fā)生了逃逸,在匿名函數(shù)中使用外部變量n,這個(gè)變量n會(huì)一直存在直到in被銷毀,所以n變量逃逸到了堆上。

4. 變量大小不確定及??臻g不足引發(fā)逃逸

我們先使用ulimit -a查看操作系統(tǒng)的??臻g:

 
 
 
  1. ulimit -a
  2. -t: cpu time (seconds)              unlimited
  3. -f: file size (blocks)              unlimited
  4. -d: data seg size (kbytes)          unlimited
  5. -s: stack size (kbytes)             8192
  6. -c: core file size (blocks)         0
  7. -v: address space (kbytes)          unlimited
  8. -l: locked-in-memory size (kbytes)  unlimited
  9. -u: processes                       2784
  10. -n: file descriptors                256

我的電腦的棧空間大小是8192,所以根據(jù)這個(gè)我們寫一個(gè)測試用例:

 
 
 
  1. package main
  2. import (
  3.  "math/rand"
  4. )
  5. func LessThan8192()  {
  6.  nums := make([]int, 100) // = 64KB
  7.  for i := 0; i < len(nums); i++ {
  8.   nums[i] = rand.Int()
  9.  }
  10. }
  11. func MoreThan8192(){
  12.  nums := make([]int, 1000000) // = 64KB
  13.  for i := 0; i < len(nums); i++ {
  14.   nums[i] = rand.Int()
  15.  }
  16. }
  17. func NonConstant() {
  18.  number := 10
  19.  s := make([]int, number)
  20.  for i := 0; i < len(s); i++ {
  21.   s[i] = i
  22.  }
  23. }
  24. func main() {
  25.  NonConstant()
  26.  MoreThan8192()
  27.  LessThan8192()
  28. }

查看逃逸分析結(jié)果:

 
 
 
  1. go build -gcflags="-m -m -l" ./test4.go
  2. # command-line-arguments
  3. ./test4.go:8:14: LessThan8192 make([]int, 100) does not escape
  4. ./test4.go:16:14: make([]int, 1000000) escapes to heap
  5. ./test4.go:16:14:       from make([]int, 1000000) (non-constant size) at ./test4.go:16:14
  6. ./test4.go:25:11: make([]int, number) escapes to heap
  7. ./test4.go:25:11:       from make([]int, number) (non-constant size) at ./test4.go:25:11

我們可以看到,當(dāng)??臻g足夠時(shí),不會(huì)發(fā)生逃逸,但是當(dāng)變量過大時(shí),已經(jīng)完全超過棧空間的大小時(shí),將會(huì)發(fā)生逃逸到堆上分配內(nèi)存。

同樣當(dāng)我們初始化切片時(shí),沒有直接指定大小,而是填入的變量,這種情況為了保證內(nèi)存的安全,編譯器也會(huì)觸發(fā)逃逸,在堆上進(jìn)行分配內(nèi)存。

參考文章(建議大家閱讀一遍)

  • https://driverzhang.github.io/post/golang%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90/
  • https://segmentfault.com/a/1190000039843497
  • https://tonybai.com/2021/05/24/understand-go-escape-analysis-by-example/
  • https://cloud.tencent.com/developer/article/1732263
  • https://geektutu.com/post/hpg-escape-analysis.html

總結(jié)

本文到這里結(jié)束了,這篇文章我們一起分析了什么是內(nèi)存逃逸以及Go語言中的逃逸分析,上面只列舉了幾個(gè)例子,因?yàn)榘l(fā)生的逃逸的情況是列舉不全的,我們只需要了解什么是逃逸分析,了解逃逸的策略就可以了,后面在實(shí)戰(zhàn)中可以根據(jù)具體代碼具體分析,寫出更優(yōu)質(zhì)的代碼。

最后對(duì)逃逸做一個(gè)總結(jié):

  • 逃逸分析在編譯階段確定哪些變量可以分配在棧中,哪些變量分配在堆上
  • 逃逸分析減輕了GC壓力,提高程序的運(yùn)行速度
  • 棧上內(nèi)存使用完畢不需要GC處理,堆上內(nèi)存使用完畢會(huì)交給GC處理
  • 函數(shù)傳參時(shí)對(duì)于需要修改原對(duì)象值,或占用內(nèi)存比較大的結(jié)構(gòu)體,選擇傳指針。對(duì)于只讀的占用內(nèi)存較小的結(jié)構(gòu)體,直接傳值能夠獲得更好的性能
  • 根據(jù)代碼具體分析,盡量減少逃逸代碼,減輕GC壓力,提高性能

文章名稱:面試官:小松子來聊一聊內(nèi)存逃逸
標(biāo)題網(wǎng)址:http://uogjgqi.cn/article/codgsps.html
掃二維碼與項(xiàng)目經(jīng)理溝通

我們在微信上24小時(shí)期待你的聲音

解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流