Go内存详解
1.内存分区
代码经过预处理,编译,汇编,链接4步后,生成一个可执行程序.(编译原理中的必学内容)
三大平台都有size
命令,用来查看一个程序的信息
text:表示代码区的大小.程序编译完之后,代码区的大小是固定的.
data & objc(Windows中为bss): 程序中用到的数据内容
dec & hex:十进制大小和十六进制大小
在程序还没有运行之前,即还没有加载到内存前,其内部已经分好了三段信息,分别为:代码区(text),(初始化)数据区(data),和未初始化数据区(bss).
有时也会把data和bss合称为静态区或全局区
1.1 代码区(text)
存放CPU执行的机器指令.
通常代码区是可共享的,即另外的执行程序可以调用它,使其可共享的目的是: 对于频繁被执行的程序,只需要在内存中有一份代码即可.
代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令.
另外,代码区还规划了局部变量的相关信息.
1.2 全局初始化数据区/静态数据区(data)
该区包含了在程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量和局部静态变量),以及常量数据(如字符串常量等)
1.3 未初始化数据区(bss)
Go中bss为0,因为Go语言所有的数据,默认都是初始化的,如定义了一个字符串,其默认为””,因此是在data区域.
而对于C语言,如声明了一个int a
,是存在bss区域,而不是data区域---
如果程序中有用到该变量,而又没有初始化,可能会导致错误的产生..
Go这一点与C不同,摒弃了放在bss区,而是放在了data区.
text区,data区和bss区,栈区(stack),堆区(heap),合称内存四区
1.4 栈区(stack)
栈区大小是有限制的,大小为1M,
超过1M大小,就要放到堆区..
栈区大小限制可以扩展,Windows中最大可扩展为10M, Linux中最大可扩展为16M
1.5 堆区(heap)
堆区的大小和内存条的大小有关.一般堆区最大值不会超过内存大小的一半
Go用new()创建一块空间,不需要管释放..因为有垃圾回收机制,去自动释放
在Go中,对栈区和堆区集中管理,给用户即程序员暴露了一个虚拟的内存区域.
在32位系统上,这个地址一般以0xc0420
开头
栈区是从高地址向低地址进行数据的存储
代码区和数据区是所有程序共享的吗?
代码区是的,数据区是独有的,B程序获取不到A的数据区(但可用通过一些手段/第三方软件,如Cheat Engine软件,可以用来修改其他程序的内存地址,如修改一些游戏里面参数的值)
2.Go Runtime内存分配
即在内存四区
的基础上加了一层
可参考:
2.1 基本策略
Go启动后,内存消耗量会比较大,因为会申请一大块内存
每次从操作系统申请一大块内存,以减少系统调用(并不一定是”大块连续内存”)
将内存切分成小块,构成双向链表
为对象分配内存时,只需从大小合适的链表中提取一个小块即可
回收对象内存时,将该小块内存重新归还到原链表,以便复用(而不是归还给操作系统,这样能减少系统调用)
如果闲置内存太多,将尝试归还部分内存给操作系统,降低整体开销
Go的内存操作模型---
高端面试常问…
为什么用链表而不用数组?
因为申请的空间,并不是一个连续的空间
2.2 内存管理单元
_PageShift为上移或左移的意思
_PageSize为页大小
将1左移13,即1后面加上13个0,这个二进制值即是8KB
arean区域
bitmap区域
spans区域
bitmap即 位图
用4个bit(位)来标住对象包含的指针和GC信息
垃圾回收时,先在bitmap中查找相关信息
2.3 内存管理组件
内存分配由内存分配器完成,分配器由三种组件构成:
- cache: 每个运行期工作线程都会绑定一个cache,用于无锁object的分配
- central: 为所有的cache提供切分好的后辈span资源
- heap: 管理闲置span,需要时向操作系统申请内存
2.3.1 cache
通过mcache,去管理mspan
span存的是页的信息,一页有8KB.
//runtime/sizeclasses.go1
2
3
4
5
6
7
8
9
10const (
_MaxSmallSize = 32768
smallSizeDiv = 8
smallSizeMax = 1024
largeSizeDiv = 128
_NumSizeClasses = 67
_PageShift = 13
)
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
一个”页”分成了67个块内容,加起来正好是8KB大小.
而bitmap标识页里面的这些个块,到底有没使用
2.3.2 central
//runtime/mcentral.go
2.3.3 heap
三个层次,层层递进
//runtime/mheap.go
三者关系
mheap负责管理申请堆区空间
sizeclass
,译作”规格”
最接近数据块的是mcache
,其每一个对象都绑定着相关信息,想使用时找mcentral
,去找到一个对应规格的mspan.
由mheap
去分配
2.4 分配流程
(一层层逐级向上找)
计算待分配对象的规格(size_class)
从cache_alloc数组中找到规格相同的span
从span.manualFreeList链表主提取可用的object
如果span.manualFreeList为空,从central获取新的span
如果central.nonempty为空,从heap.free/freelarge获取,并切分成object链表
如果heap没有大小合适的span,向操作系统申请新的内存
2.5 释放流程
将标记为可回收的object交还给所属的span.freelist(在bitmap里面有标记)
该span被放回central,可以提供cache重新获取
如果span已全部回收object,将其交还给heap,以便重新分切复用
定期扫描heap里闲置的span,释放其占用的内存
注: 以上流程不包括大对象,它直接从heap分配和释放
2.6 总结
Go语言的内存分配非常复杂,一个原则就是能复用的一定要复用
Go在程序启动时,会向操作系统申请一大块内存,之后自行管理
Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object
mcache,mcentral,mheap是go内存管理的三大组件,层层递进. mcache管理线程在本地缓存的mspan; mcentral管理全局的mspan供所有线程使用; mheap管理Go的所有动态分配内存.
一般小对象通过mspan分配内存; 大对象则直接由mheap分配内存
3. Go GC 垃圾回收
Go的垃圾回收也是并行的
参考:
2021补充参考:
已将Tony Bai大佬博客添加友链 ^_^
原文作者: fliter
原文链接:
http://www.dashen.tech/2014/04/01/Go内存详解/版权声明: 转载请注明出处