Golang - 内存逃逸分析

面试必问:

1、什么是内存逃逸

2、内存逃逸的场景有哪些

3、分析内存逃逸的意义

4、怎么避免内存逃逸

1、什么是内存逃逸

在了解什么是内存逃逸之前,我们先来简单地熟悉一下两个概念。栈内存堆内存

Golang的GC主要是针对堆的,不是栈。

值类型的全局变量分配在栈上,引用类型的全局变量分配在堆上。

局部变量内存分配可能在栈上也可能在堆上。

一个对象本应该分配在栈上面,结果分配在了堆上面,这就是内存逃逸。

2、内存逃逸的场景有哪些

分析内存逃逸的命令: go build -gcflags '-m' main.go

常见的场景有四种:局部指针返回,栈空间不足,动态类型,闭包引用

1)、局部指针返回

当我们在某个方法内定义了一个局部指针,并且将这个指针作为返回值返回时,此时就发生了逃逸。

这种类型的逃逸是比较常见的,如下:

2)、栈空间不足

众所周知,在系统中栈空间相比与总的内存来说是非常小的。如下,我的Mac是16G*512G的,可是整个系统中栈空间大小也才8M。

而在我们的实际编码过程中,大部分Goroutine的占用空间不到10KB(这也是Golang能支持高并发的原因之一)。

而其中分配给栈的更是少之又少。所以一旦某个对象体积过大时候就会发生逃逸,从栈上面转到堆上面。

有两个map,space1和space2,space1长度大小都是100,space2长度大小都是10000,结果space2发生了逃逸,space1没有。如下:

3)、动态类型

这种内存逃逸应该是最多的,最常见的,而且还无法避免。

简单地说就是被调用函数的入参是interface或者是不定参数,此时就会发生内存逃逸。如下:

一个简简单单的Println居然也会发生内存逃逸。那么问题来了,这个是怎么导致的呢?

废话不多说,直接拔掉底裤撸源码。此处就是所谓的动态类型。

4)、闭包调用

这种场景是非常少的,一般没有人写这种可读性这么差的代码,我们只需要知道这种场景即可,大概率是碰不上的。

3、分析内存逃逸的意义

简单的总结就是两点:减轻GC的压力,提高分配速度

前面已经提及,Golang的GC主要是针对堆的,而不是栈。

试想一下,如果大量的对象从栈逃逸到堆上,是不是就会增加GC的压力。

在GC的过程中会占用比较大的系统开销(一般可达到CPU容量的25%),而且目前所有的GC都有STW这个死结,而STW会造成用户直观的“卡顿”,非常影响用户体验。

STW:"Stop the World" 阶段,当前运行的所有程序将被暂停,扫描内存的 root 节点和添加写屏障 (write barrier)。

GC过程有两次STW:第一次STW会准备根对象的扫描,启动写屏障(Write Barrier)和辅助GC(mutator assist);第二次STW会重新扫描(rescan)部分根对象,禁用写屏障(Write Barrier)和辅助GC(mutator assist)。

此外,堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆内存分配首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。

通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少GC的压力,提高程序的运行速度。

4、怎么避免内存逃逸

首先需要注意的是,Golang在编译的时候就可以确立逃逸,并不需要等到运行时。这样就给了咱们避免内存逃逸的机会。另外明确一点,小编认为没有任何方式能绝对避免内存逃逸

存在【动态类型】这种逃逸方式,几乎所有的库函数都是动态类型的

主要的避免原则分别针对上面几种场景:

1)尽量减少外部指针引用,必要的时候可以使用值传递;

2)对于自己定义的数据大小,有一个基本的预判,尽量不要出现栈空间溢出的情况;

3)Golang中的接口类型的方法调用是动态调度,如果对于性能要求比较高且访问频次比较高的函数调用,应该尽量避免使用接口类型;

4)尽量不要写闭包函数,可读性差且发生逃逸。

posted @   李若盛开  阅读(497)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· DeepSeek 开源周回顾「GitHub 热点速览」
点击右上角即可分享
微信分享提示