Go 中的内存分配

1、概述

内存分配的基本策略:

  1. 每次从操作系统申请一大块内存(比如1MB),以减少系统调用。
  2. 将申请到的大块内存按照特定大小预先切分成小块,构成链表。
  3. 为对象分配内存时,只须从大小合适的链表提取一小块即可。
  4. 回收对象内存时,将该小块内存重新归还原链表,以便复用。
  5. 如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销。

内存块
分配器将其管理的内存块分为两种。

  • span: 将多个地址连续的页(page)组成的大块内存。
  • object: 将 span 按特定大小切分成多个小块,没个小块可存储一个对象。

分配器按页数来区分不同大小的 span。比如,以页数为单位将 span 存放到管理数组中,需要时就以页数为索引进行查找。当然,span大小并非固定不变。在获取闲置span时,如果没找到大小合适的,那就返回页数更多的,此时会引发裁剪操作,多余部分将构成新的 span 被释放回管理数组。分配器还会尝试将地址相邻的空闲 span 合并,已构建更大的内存块,减少碎片,提高更灵活的分配策略。
malloc.go

_PageShift      = 13
_PageSize = 1 << _PageShift   // 8KB

用于存储对象的 object,按8字节倍数分为n种。
若对象超出特定阈值限制,会被当做大对象(large object)特别对待。
malloc.go

_MaxSmallSize   = 32768    // 32KB

管理组件
分配器由三种组件组成。

  • cache: 每个运行期工作线程都会绑定一个 cache,用于无锁 object 分配。
  • central:为所有cache 提供切分好的后备 span 资源。
  • heap:管理闲置 span,需要时间向操作系统申请新内存。

2、初始化

因为内存分配器和垃圾回收算法都依赖连续地址,所以在初始化阶段,预先保留了很大的一段虚拟地址空间。
注意:保留地址空间,并不会分配内存。
该段空间被划分成三个区域:

简单点说,就是用三个数组组成一个高性能内存管理结构。

  1. 使用 area 地址向操作系统申请内存,其大小决定了可分配用户内存的上限。
  2. 位图 bitmap 为每个对象提供 4bit 标记位,用以保存指针、GC 标记等信息。
  3. 创建 span 时,按页填充对应 spans 空间。在回收 object 时,只续将其地址按页对齐后就可找到所属 span。分配器还用此访问相邻 span,做合并操作。
    注意:
    操作系统大多采取机会主义分配策略,申请内存时,仅承诺但不立即分配物理内存。

3、分配

为对象分配内存须区分是在栈上还是在堆上完成。通常情况下,编译器有责任尽可能使用寄存器和栈来存储对象,这有助于提升性能,减少垃圾回收器的压力。
但千万不要以为用了 new 函数就一定会分配在堆上,即使是相同的源码也有不同的结果。

package main

func test() *int {
	x := new(int)
	*x = 0xAABB
	return x
}

func main()  {
	println(*test())
}

go build -gcflags “-l” -o test test.go // 关闭内联优化
go tool objdump -s “main.test” test

  test.go:3		0x104e580		65488b0c2530000000	MOVQ GS:0x30, CX			
  test.go:3		0x104e589		483b6110		CMPQ 0x10(CX), SP			
  test.go:3		0x104e58d		7639			JBE 0x104e5c8				
  test.go:3		0x104e58f		4883ec18		SUBQ $0x18, SP				
  test.go:3		0x104e593		48896c2410		MOVQ BP, 0x10(SP)			
  test.go:3		0x104e598		488d6c2410		LEAQ 0x10(SP), BP			
  test.go:4		0x104e59d		488d05fca50000		LEAQ runtime.types+42272(SB), AX	
  test.go:4		0x104e5a4		48890424		MOVQ AX, 0(SP)				
  test.go:4		0x104e5a8		e8c3befbff		CALL runtime.newobject(SB)	// 在堆上分配	
  test.go:4		0x104e5ad		488b442408		MOVQ 0x8(SP), AX			
  test.go:5		0x104e5b2		48c700bbaa0000		MOVQ $0xaabb, 0(AX)			
  test.go:6		0x104e5b9		4889442420		MOVQ AX, 0x20(SP)			
  test.go:6		0x104e5be		488b6c2410		MOVQ 0x10(SP), BP			
  test.go:6		0x104e5c3		4883c418		ADDQ $0x18, SP				
  test.go:6		0x104e5c7		c3			RET					
  test.go:3		0x104e5c8		e8f384ffff		CALL runtime.morestack_noctxt(SB)	
  test.go:3		0x104e5cd		ebb1			JMP main.test(SB)			
  :-1			0x104e5cf		cc			INT $0x3

当使用默认参数时,函数 test 会被 main 内联,此时结果就变得不同了。
go build -o ./test ./test.go // 默认优化
go tool objdump -s “main.main” test

  test.go:9		0x104e580		65488b0c2530000000	MOVQ GS:0x30, CX			
  test.go:9		0x104e589		483b6110		CMPQ 0x10(CX), SP			
  test.go:9		0x104e58d		7635			JBE 0x104e5c4				
  test.go:9		0x104e58f		4883ec10		SUBQ $0x10, SP				
  test.go:9		0x104e593		48896c2408		MOVQ BP, 0x8(SP)			
  test.go:9		0x104e598		488d6c2408		LEAQ 0x8(SP), BP			
  test.go:10		0x104e59d		90			NOPL					
  test.go:10		0x104e59e		e89d50fdff		CALL runtime.printlock(SB)		
  test.go:10		0x104e5a3		48c70424bbaa0000	MOVQ $0xaabb, 0(SP)			
  test.go:10		0x104e5ab		e81058fdff		CALL runtime.printint(SB)		
  test.go:10		0x104e5b0		e81b53fdff		CALL runtime.printnl(SB)		
  test.go:10		0x104e5b5		e80651fdff		CALL runtime.printunlock(SB)		
  test.go:11		0x104e5ba		488b6c2408		MOVQ 0x8(SP), BP			
  test.go:11		0x104e5bf		4883c410		ADDQ $0x10, SP				
  test.go:11		0x104e5c3		c3			RET					
  test.go:9		0x104e5c4		e8f784ffff		CALL runtime.morestack_noctxt(SB)	
  test.go:9		0x104e5c9		ebb5			JMP main.main(SB)			
  :-1			0x104e5cb		cc			INT $0x3				
  :-1			0x104e5cc		cc			INT $0x3				
  :-1			0x104e5cd		cc			INT $0x3				
  :-1			0x104e5ce		cc			INT $0x3				
  :-1			0x104e5cf		cc			INT $0x3

从编译的结果来看,内联优化后的代码没有调用 newobject 在堆上分配内存。
编译器这么做,道理很简单。没有内联时,需要在两个栈帧间传递对象,因此会在堆上分配而不是返回一个失效栈帧里的数据。而当内联时,它实际就成了 main 栈帧内的局部变量,无须去堆上操作。
Go 编译器支持逃逸分析,它会在编译器通过构建调用图来分析局部变量是否会被外部引用,从而决定是否可直接分配在栈上。
编译参数 -gcflags “-m” 可输出编译优化信息,其中包括内联和逃逸分析。
对象分配策略:

  • 大对象直接从 heap 获取 span
  • 小对象从 cache.alloc[sizeclass].freelist 获取 object。
  • 微小对象组合使用 cache.tiny object。

4、回收

之所以说回收而非释放,是因为整个内存分配器的核心是内存复用,不再使用的内存会被放回合适位置,等下次分配时再次使用。只有当空闲内存资源过多时,才会考虑释放。
基于效率考虑,回收操作自然不会直接盯着单个对象,而是以 span 为基本单位。通过比对 bitmap 里的扫描标记,逐步将 object 收归原 span,最终上交 central 或 heap 复用。
在回收器遍历span 时,将收集到的不可达 object 合并到 freelist 链表。如该 span 已收回全部 object,那么将这块完全自由的内存还给 heap,以便后续复用。
无论是向操作系统申请内存,还是清理回收内存,只要往 heap 里放 span,都会尝试合并左右相邻的闲置 span,已构成更大的自由块。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值