内存管理:空间分配及逃逸分析 内存管理: 分配与逃逸分析(3)
内存管理:空间分配及逃逸分析

接下来,我们从虚拟内存的构成来分析下软件开发中的内存管理。
3. 内存管理
程序在虚拟内存上被分为栈区、堆区、数据区、全局数据区、代码段五个部分。
而内存的管理,就是对内存空间进行合理化使用,主要是堆区(Heap)和栈区(Stack)这两个重要区域的分配使用。
3.1 堆与栈
虚拟内存里有两块比较重要的地址空间,分别为堆和栈空间。对于 C++ 等底层的编程语言,栈上的内存空间由编译器统一管理,而堆上的内存空间需要程序员来手动管理进行分配和回收。
在 Go 语言中,栈上的内存空间也是由编译器来统一管理,而堆上的内存空间由编译器和垃圾收集器共同管理进行分配和回收,这给我们程序员带来了极大的便利性。
在栈上分配和回收内存的开销很低,只需要 2 个指令:PUSH 和 POP。PUSH 将数据压入栈中,POP 释放空间,消耗的仅是将数据拷贝到内存的时间。
而在堆上分配内存时,不仅分配的时候慢,而且垃圾回收的时候也比较费劲,比如说 Go 在 1.8 以后就用到了三色标记法+混合写屏障的技术来做垃圾回收。总体来看,堆内存分配比栈内存分配导致的开销要大很多。
3.2 堆栈内存分配
1)内存分配的挑战
- 像 C/C++ 这类由用户程序申请内存空间,可能会频繁地进行内存申请和回收,但每次内存分配时都需要进行系统调用(即只有进入内核态才可以申请内存),就会导致系统的性能很低。
- 除此之外,还可能会有多线程(Go语言里面也有协程)去访问同一个地址空间的情况,这时就必定需要对内存进行加锁,带来的开销也会比较大。
- 初始化时堆内存是一整块连续的内存,但随着系统运行过程中不断申请回收内存,可能会产生许多的内存碎片,导致内存的使用效率降低。
程序进行内存分配时,为了应对以上最常见的三种问题,Go 语言结合谷歌的 TCMalloc(ThreadCacheMalloc) 内存回收方法,做了一些改进。
同时,TCMalloc 和 Go 进行内存分配时都会引入线程缓存(mcentral of P)、中心缓存(mcentral)和页堆(mheap)三个组件进行分级管理内存。如图所示:

线程缓存属于每一个独立的线程或协程,里面存储了每个线程所用的内存块 span,由于内存块的大小不一,所以有上百个内存块类别 span class,这些内存块里面分别管理不同大小的内存空间(比如 8KB、16KB、32KB…)。由于不涉及多线程,所以不需要使用互斥锁来保护内存,以减少锁竞争带来的性能损耗。
当线程缓存的空间不够时,会使用中心缓存作为小对象内存的分配,中心缓存和线程缓存的每个 span class 一一对应,并且中心缓存的每个 span class 中有两个内存块,分别存储了分配过内存的空间和满内存空间,以提升内存分配的效率。如果中心缓存还不满足,就向页堆进行空间申请。
为了提升空间的利用率,当遇到中大对象(>=32KB)分配时,内存分配器会选择页堆直接进行分配。
Go 语言内存分配的核心是使用多级缓存将对象根据大小分类,并按照类别来实施不同的分配策略。如上图所示,应用程序在申请内存时会根据对象的大小(Tiny 小对象或者 Large and medium 中大对象),向不同的组件去申请内存空间。
2)栈内存分配
栈区的内存一般由编译器自动分配和释放,一般来说,栈区存储着函数入参以及局部变量,这些数据会随着函数的创建而创建,函数的返回而消亡,一般不会在程序中长期存在。
这种线性的内存分配策略有着极高地效率,但是工程师也往往不能控制栈内存的分配,这部分工作基本都是由编译器完成的。
栈空间在运行时中包含两个重要的全局变量,分别是 runtime.stackpool 和 runtime.stackLarge,这两个变量分别表示全局的栈缓存和大栈缓存,前者可以分配小于 32KB 的内存,后者用来分配大于 32KB 的栈空间:

栈分配时,根据线程缓存和申请栈的大小,Go 语言会通过三种不同的方法分配栈空间:
- 如果栈空间较小,使用全局栈缓存或者线程缓存上固定大小的空闲链表分配内存;
- 如果栈空间较大,从全局的大栈缓存
runtime.stackLarge中获取内存空间; - 如果栈空间较大并且
runtime.stackLarge空间不足,在堆上申请一片大小足够内存空间。
在 Go1.4 以后,最小的栈内存大小为 2KB,即一个 goroutine 协程的大小。所以,当程序里的协程数量超过栈内存可分配的最大值后,就会分配在堆空间里面。也就是说,虽然 Go 语言里面可以用 go 关键字分配不限数量的 goroutine 协程,但是在性能上,我们分配的 goroutine 个数最好不要超过栈空间的最大值。
假设,栈内存的最大值为 8MB,那分配的 goroutine 数量最好不要超过 4000 个(8MB/2KB)。
4. 逃逸分析
4.1 Go如何做逃逸分析
在 C 语言和 C++ 这类需要手动管理内存的编程语言中,将对象或者结构体分配到栈上或者堆上是由工程师来决定的,这也为工程师的工作带来的挑战:如何精准地为每一个变量分配合理的空间,提升整个程序的运行效率和内存使用效率。但是 C 和 C++ 的这种手动分配内存会导致如下的两个问题:
- 不需要分配到堆上的对象分配到了堆上 — 浪费内存空间;
- 需要分配到堆上的对象分配到了栈上 — 产生野指针、影响内存安全;
与野指针相比,浪费内存空间反而是小问题。在 C 语言中,栈上的变量被函数作为返回值返回给调用方是一个常见的错误,在如下所示的代码中,栈上的变量 i 被错误返回:
int *dangling_pointer() {
int i = 2;
return &i;
}
当 dangling_pointer 函数返回后,它的本地变量会被编译器回收(栈上空间的机制),调用方获取的是危险的野指针。如果程序里面出现大量不合法的指针值,在大型项目中是比较难以发现和定位的。
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称野指针,或称悬空指针、迷途指针。——wiki百科
那么,在 Go 语言里面,编译器该如何知道某个变量需要分配在堆,还是栈上而避免出现这种问题呢?
编译器决定内存分配位置的方式,就称之为逃逸分析。逃逸分析由编译器完成,作用于编译阶段。在编译器优化中,逃逸分析是用来决定指针动态作用域的方法。Go 语言的编译器使用逃逸分析决定哪些变量应该在栈上分配,哪些变量应该在堆上分配。
其中包括使用 new、make 和字面量等方法隐式分配的内存,Go 语言的逃逸分析遵循以下两个不变性:
- 指向栈对象的指针不能存在于堆中;
- 指向栈对象的指针不能在栈对象回收后存活。
什么意思呢?我们来翻译一下:
- 首先,如果堆的指针指向了栈对象,那么栈对象的内存就需要分配到堆上;
- 如果栈对象回收后,指针还存活,那么这个对象就只能分配到堆上。
我们在进行内存分配时,编译器会遵循上述两个原则,对我们申请的变量或对象进行内存分配到栈上或者是堆上。
换言之,当我们分配内存时,违反了上述两个原则之一,本来想分配到栈上的变量可能就会“逃逸”到堆上,被称作内存逃逸。如果程序中出现大量的内存逃逸,势必会带来意外的负面影响:比如垃圾回收缓慢,内存溢出等问题。
4.2 四种逃逸场景
Go 语言中,由于以下四种情况,栈上的内存可能会发生逃逸。
1. 指针逃逸
指针逃逸很容易理解,我们在函数中创建一个对象时,对象的生命周期随着函数结束而结束,这时候对象的内存就分配在栈上。
而如果返回了一个对象的指针,这种情况下,函数虽然退出了,但指针还在,对象的内存不能随着函数结束而回收,因此只能分配在堆上。
package main
package main
type User struct {
ID int64
Name string
Avatar string
}
// 要想不发生逃逸,返回 User 对象即可。
func GetUserInfo() *User {
return &User{
ID: 666666,
Name: "sim lou",
Avatar: "https://www.baidu.com/avatar/666666",
}
}
func main() {
u := GetUserInfo()
println(u.Name)
}
上面例子中,如果返回的是 User 对象,而非对象指针 *User,那么它就是一个局部变量,会分配在栈上;反之,指针作为引用,在 main 函数中还会继续使用,因此内存只能分配到堆上。
我们可以用编译器命令 go build -gcflags -m main.go 来查看变量逃逸的情况:
相关阅读
-
微信摄像头怎么开美颜 设置视频美颜教程
正文核心介绍:微信摄像头怎么开美颜方面的介绍,1、打开手机设置,点击【其他设置】。2、点击【视频通话美颜】。3、打开【微信】右侧的开关,可自主调节美颜效果。
-
笔记本硬盘坏了怎么办 移动硬盘坏了修复方法
本文核心内容:笔记本硬盘坏了怎么办和移动硬盘坏了修复方法的相关经验,很不错的方法小知识,建议收藏哦! 有许多朋友遇到过电脑硬盘出现问题的状况,今天分享修复硬盘坏道的技巧,
-
驱动精灵护眼卸载教程 驱动精灵护眼模式找不到了
一个电脑小知识,为大家介绍驱动精灵护眼卸载教程的内容,关于驱动精灵护眼卸载教程 驱动精灵护眼模式找不到了,接下来一起来看看吧。 不少的IT袋用户下载驱动精灵安装驱动之后都被他
-
怎么连接共享打印机步骤 关于打印机无线连接方法
本文为你介绍怎么连接共享打印机步骤和关于打印机无线连接方法的电脑方面的小经验,接下来IT袋小编就来介绍。 在办公室里打印机多半是通过无线共享的方式进行使用的,那在链接无线打


