Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go语言 堆内存

一文搞懂Go语言堆内存原理小结

作者:数据知道

堆内存是程序运行时动态分配的内存区域,与栈内存形成对比,本文就来详细的介绍一下Go语言堆内存原理小结, 具有一定的参考价值,感兴趣的可以了解一下

一、基本概念理解

1.1 什么是堆内存?

堆内存是程序内存中用于动态内存分配的部分。堆内存不是在编译过程中预先确定的,而是在程序运行过程中动态管理的。程序在执行过程中可以根据需要从堆中申请、释放内存。

在继续介绍之前,试着了解一下进程的内存布局,如下图所示,可以简单了解大致的内存布局。

+ - - - - - - - - - - - - - - - +
| Stack                         | ←- 栈,静态分配
| - - - - - - - - - - - - - - - | 
| Heap                          | ←- 堆,动态分配
| - - - - - - - - - - - - - - - | 
| Uninitialized Data            | ←- 未初始化数据
| - - - - - - - - - - - - - - - | 
| Initialized Data              | ←- 初始化数据
| - - - - - - - - - - - - - - - | 
| Code                          | ←- 代码(文本段)
+ - - - - - - - - - - - - - - - +

                     进程内存布局

我们来分解一下进程的内存布局,看看它们是如何协同工作的:

1.2 堆内存的特点

动态分配:内存在运行时申请、释放。
可变大小:分配的内存大小可以变化。
基于指针的管理:使用指针访问和控制内存。

+ - - - - - - - - - - -+
| Heap Memory.         | ←- 堆内存
| - - - - - - - - - - -| 
| Free Block           | ←- 空闲块
| - - - - - - - - - - -| 
| Allocated Block 1    | ←- 已分配块1
| [Pointer -> Data]    |
| - - - - - - - - - - -| 
| Free Block           | ←- 空闲块
| - - - - - - - - - - -| 
| Allocated Block 2    | ←- 已分配块2
| [Pointer -> Data]    |
| - - - - - - - - - - -| 
| Free Block.          | ←- 空闲块
+ - - - - - - - - - - -+

                   动态分配

多个空闲块和已分配块的存在表明,内存的分配和释放在程序运行过程中不断发生。由于内存分配和释放的时间不同,导致空闲内存段和已用内存段交替出现,堆就会出现这种碎片化现象。

1.3 前置知识:栈与堆的根本区别

要理解堆,必先理解栈。在 Go 程序中,每个 Goroutine 都有一个独立的,而所有 Goroutine 共享一个

特性
所有权Goroutine 独有进程内所有 Goroutine 共享
分配与释放编译器/运行时自动管理,函数入栈时分配,出栈时释放,速度极快垃圾回收器管理,分配相对较慢,释放时机不确定(GC时)
大小小而固定(初始几KB,可动态增长,但有上限)非常大(可达 TB 级别,受限于系统内存)
存储数据函数参数、局部变量、返回地址等生命周期明确的数据生命周期不确定的数据,比如函数返回后仍需被访问的对象
访问速度快(连续内存,CPU缓存友好)慢(内存不连续,可能涉及系统调用)

1.4 堆内存如何工作?

堆内存由操作系统管理。当程序请求内存时,操作系统会从进程的堆内存段中分配内存。这一过程涉及多个关键组件和功能:

主要组成部分:

1.5 核心原理:Go 对象如何分配到堆上?

当一个 Goroutine 需要在堆上分配内存时,流程如下:

  1. 获取 P:该 Goroutine 绑定到某个 P(逻辑处理器)上。
  2. 查找 mcache:从 P 中获取其专属的内存缓存 mcache
  3. 大小分级:根据要分配的对象大小,选择合适的规格:
    • 微小对象 (< 16 bytes):直接在 mcache 中用一个专门的 tiny 对象来处理,避免浪费。
    • 小对象 (< 32KB):在 mcache 中寻找对应大小规格的 mspan(内存跨度)来分配。
    • 大对象 (>= 32KB):直接跳过 mcachemcentral,向全局的 mheap 申请内存。
      关键点:绝大多数对象都是小对象,它们的分配都可以在 mcache 中无锁完成,速度极快。

核心问题:编译器如何决定一个对象放栈上还是堆上?
答案是:逃逸分析

Go 编译器会分析代码,如果一个局部变量的作用域超出了它所在的函数(即“逃逸”了),那么它就必须被分配在堆上。如:

// 情况一:不逃逸,分配在栈上
func stackExample() int {
    x := 10  // x 的生命周期只在 stackExample 函数内
    return x
}
// 情况二:逃逸,分配在堆上
func heapExample() *int {
    x := 10  // x 的指针被返回,作用域超出了函数,x 逃逸到堆上
    return &x
}

可以使用 go build -gcflags="-m" 命令来查看逃逸分析的结果:

$ go build -gcflags="-m" main.go
# command-line-arguments
./main.go:10:6: can inline heapExample
./main.go:11:2: leaking param: x
./main.go:11:2: moved to heap: x

moved to heap: x 明确告诉我们变量 x 被分配到了堆上。

1.6 Go 内存分配器:TCMalloc 的继承与演进

Go 的内存分配器高度借鉴了 Google 的 TCMalloc。其核心思想是:避免多线程竞争,将内存管理工作分摊到每个处理器(P)上
这带来了两个核心设计:

  1. 无锁化:每个 P 都有自己的本地内存缓存,大部分分配操作都在 P 内部完成,无需加锁。
  2. 分级管理:将内存按大小分为不同级别,用不同策略管理,提高分配效率。

二、Go 如何管理堆内存

Go 为堆内存管理提供了内置函数和数据结构,如 new、make、slices、maps 和 channels。这些函数和数据结构抽象掉了底层细节,在内部与操作系统的内存管理机制进行了交互。

2.1 案例

我们通过一个简单的 Go 程序来理解,该程序为整数片段分配内存、初始化数值并打印。(main.go)

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // 为包含10个整数的切片分配内存(动态数组)
    memorySize := 10
    slice := make([]int, memorySize)

    // 初始化并使用分配的内存
    for i := 0; i < len(slice); i++ {
        slice[i] = 5 // 为每个元素赋值
    }

    // 打印值
    for i := 0; i < len(slice); i++ {
        fmt.Printf("%d ", slice[i])
    }
    fmt.Println()

    // 通过强制垃圾收集演示内存释放
    runtime.GC()
}

为了了解 Go 如何与 Linux 内存管理库交互,可以使用 strace(centos系统可通过:yum install strace安装)来跟踪 Go 程序进行的系统调用。

2.2 内存分配中的系统调用

$ go build -o memory_allocation main.go
$ strace -f -e trace=mmap,munmap ./memory_allocation

执行结果如下:

mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50caa6b000
mmap(NULL, 131072, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50caa4b000
mmap(NULL, 1048576, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50ca94b000
mmap(NULL, 8388608, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50ca14b000
mmap(NULL, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50c614b000
mmap(NULL, 536870912, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f50a614b000
mmap(NULL, 536870912, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f508614b000
mmap(0xc000000000, 67108864, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xc000000000
mmap(NULL, 33554432, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f508414b000
mmap(NULL, 69648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084139000
mmap(0xc000000000, 4194304, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xc000000000
mmap(0x7f50caa4b000, 131072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50caa4b000
mmap(0x7f50ca9cb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50ca9cb000
mmap(0x7f50ca551000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50ca551000
mmap(0x7f50c817b000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50c817b000
mmap(0x7f50b62cb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50b62cb000
mmap(0x7f50962cb000, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f50962cb000
mmap(NULL, 1048576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084039000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084029000
mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5084019000
mmap(NULL, 1439992, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083eb9000
strace: Process 1425438 attached
[pid 1425437] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083e79000
strace: Process 1425439 attached
strace: Process 1425440 attached
[pid 1425437] mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083e39000
strace: Process 1425441 attached
5 5 5 5 5 5 5 5 5 5 
[pid 1425437] mmap(NULL, 65536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5083e29000
[pid 1425440] +++ exited with 0 +++
[pid 1425439] +++ exited with 0 +++
[pid 1425438] +++ exited with 0 +++
[pid 1425441] +++ exited with 0 +++
+++ exited with 0 +++
+ - - - - - - - - - - -+
| Go Program           | ←- Go 程序
| - - - - - - - - - - -| 
| Calls Go Runtime     | ←- 调用 Go 运行时
| - - - - - - - - - - -| 
| Uses syscalls:       | ←- 系统调用:mmap,munmap
| mmap, munmap         |
| - - - - - - - - - - -| 
| Interacts with OS    | ←- 与操作系统内存管理器交互
| Memory Manager       |
+ - - - - - - - - - - -+
                      系统调用的简化示例

strace 输出解释

2.3 内存分配过程的各个阶段:

+ - - - - - - - - - - -+
| Initialize Slice     | ←- 初始化切片
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| - - - - - - - - - - -|
| Set Values           | ←- 设置值
| [5, 5, 5, 5, 5, 5, 5, 5, 5, 5] |
| - - - - - - - - - - -| 
| Print Values         | ←- 打印值
| 5 5 5 5 5 5 5 5 5 5  |
| - - - - - - - - - - -| 
| Force GC             | ←- 强制垃圾回收
| - - - - - - - - - - -|

上图说明了 Go 动态内存分配和管理的逐步过程。

三、 堆内存管理:三级结构

Go 的堆内存管理是一个精密的三级(或四级)结构,理解它就理解了 Go 内存管理的核心。

3.1 第一级:mcache (Per-P Cache)

3.2 第二级:mcentral (Central Cache)

3.3 第三级:mheap (Heap Manager)

3.4 基础单元:mspan (Memory Span)

流程串联: Goroutine -> mcache (无锁) -> mcentral (加锁) -> mheap (全局锁) -> OS

到此这篇关于一文搞懂Go语言堆内存原理小结的文章就介绍到这了,更多相关Go语言 堆内存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文