Golang 的内存管理

news2025/2/5 19:50:46

文章目录

  • 1.内存管理角色
  • 1.常见的内存分配方法
    • 线性分配器
    • 空闲链表分配器
    • TCMalloc
  • 2.Go 内存管理组件
    • mspan
    • mcache
      • 初始化
      • 替换
      • 微分配器
    • mcentral
    • mheap
  • 3.内存分配
  • 4.内存管理思想
  • 参考文献

1.内存管理角色

内存管理一般包含三个不同的组件,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector),当用户程序申请内存时,它会通过内存分配器申请新内存,而分配器会负责从堆中初始化相应的内存区域。

在这里插入图片描述

你可能会问,为什么用户程序叫作 Mutator?

在计算机科学中,特别是在与垃圾回收和并发编程相关的领域,“Mutator”(变异者)是指程序中能够修改共享状态的部分。这个术语通常与 “Collector”(收集器)一起使用,Collector 负责执行垃圾回收,而 Mutator 负责运行和修改程序的状态。

不过本文的介绍的不是 Mutator 和 Collector,而是负责分配内存的 Allocator。

1.常见的内存分配方法

线性分配器

线性分配器(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们使用线性分配器时,只需要在内存中维护一个指向内存特定位置的指针,如果用户程序向分配器申请内存,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针:
在这里插入图片描述

空闲链表分配器

空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表:

在这里插入图片描述

TCMalloc

TCMalloc 是由 Google 开发的一种内存分配器,主要用于优化多线程环境下的内存分配和释放性能。TCMalloc 是Thread-Caching Malloc 的缩写,即线程缓存分配器。

TCMalloc 比 glibc 中的 malloc 还要快很多。Go 的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心思想是使用多级缓存并将对象根据大小分类,按照类别实施不同的分配策略。

TCMalloc 中将内存分成三类,即小对象,小于256K的,中型对象,介于256K到1M的,大于1M的为大对象。

TCMalloc 不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,分为线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)。

在这里插入图片描述

2.Go 内存管理组件

Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件:

  • runtime.mspan
  • runtime.mcache
  • runtime.mcentral
  • runtime.mheap

mspan

runtime.mspan 是 Go 内存管理的基本单元,该结构体中包含 next 和 prev 两个字段,它们分别指向了前一个和后一个 runtime.mspan。

多个连续的 Page 会组成一个 Span。Go 中的一个 Page 为 8KB。

type mspan struct {
	...
	next *mspan
	prev *mspan

    startAddr uintptr // 起始地址
	npages    uintptr // 页数
	freeindex uintptr

	allocBits  *gcBits
	gcmarkBits *gcBits
	allocCache uint64
	...
}
  • startAddr 和 npages — 确定该结构体管理的多个页所在的内存。
  • freeindex — 扫描页中空闲对象的初始索引。
  • allocBits 和 gcmarkBits — 分别用于标记内存的占用和回收情况。
  • allocCache — allocBits 的补码,可以用于快速查找内存中未被使用的内存。

当用户程序或者线程向 runtime.mspan 申请内存时,它会使用 allocCache 字段以对象为单位在管理的内存中快速查找待分配的空间:
在这里插入图片描述
如果我们能在内存中找到空闲的内存单元会直接返回。如果找不到,上一级的组件 runtime.mcache 会为调用 runtime.mcache.refill 更新内存管理单元以满足为更多对象分配内存的需求。

runtime.spanClass 是 runtime.mspan 的跨度类,表示内存管理单元中存储的对象的大小:

type mspan struct {
	...
	spanclass   spanClass
	...
}

type spanClass uint8

Go 的内存管理模块中一共包含 67 种跨度类,表示 67 种预先设定好的对象大小。对象大小与占用的页数存储在 runtime.class_to_size 和 runtime.class_to_allocnpages 变量。

var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 24, 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}
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

runtime.spanClass 是一个 uint8 类型的整数,它的前 7 位存储着跨度类的 ID,最后一位表示是否包含指针,垃圾回收会对包含指针的 runtime.mspan 结构体进行扫描。

mcache

runtime.mcache 是 Go 的线程缓存,它会与线程上的处理器(P)一一绑定,主要用来缓存用户程序申请的微小对象。每一个线程缓存都持有 68 * 2 个 runtime.mspan,这些内存管理单元都存储在结构体的 alloc 字段中。

type mcache struct {
	...
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
	...
}

const numSpanClasses = _NumSizeClasses << 1

在这里插入图片描述

其中 scan 的 mspan 表示这个 span 包含指针需要进行垃圾回收扫描。扫描的目的是找到并标记所有可达的对象,以便进行垃圾回收。

noscan 的 mspan 表示这个 span 不包含指针,无需进行垃圾回收扫描。这样的 span 可能存储的是不包含指针的对象,例如基本类型的数据。

注意,线程缓存在刚刚被初始化时是不包含 mspan 的,只有当用户程序申请内存时才会从上一级组件获取新的 mspan 满足内存分配的需求。

初始化

运行时在初始化处理器(P)时会调用 runtime.allocmcache 初始化线程缓存,该函数会在系统栈中使用 runtime.mheap 中的线程缓存分配器初始化新的 runtime.mcache 结构体:

// dummy mspan that contains no free objects.
var emptymspan mspan

func allocmcache() *mcache {
	var c *mcache
	systemstack(func() {
		lock(&mheap_.lock)
		c = (*mcache)(mheap_.cachealloc.alloc())
		c.flushGen = mheap_.sweepgen
		unlock(&mheap_.lock)
	})
	for i := range c.alloc {
		c.alloc[i] = &emptymspan
	}
	c.nextSample = nextSample()
	return c
}

就像我们在上面提到的,初始化后的 runtime.mcache 中的所有 runtime.mspan 都是空的占位符 emptymspan。

替换

runtime.mcache.refill 会为 mcache 获取一个指定跨度类的 mspan,被替换的 mspan 不能包含空闲的内存空间,而获取的 mspan 中需要至少包含一个空闲对象用于分配内存。

func (c *mcache) refill(spc spanClass) {
	s := c.alloc[spc]
	s = mheap_.central[spc].mcentral.cacheSpan()
	c.alloc[spc] = s
}

如上述代码所示,该方法会从中心缓存中申请新的 runtime.mspan 存储到线程缓存中,这也是向线程缓存插入内存管理单元的唯一方法。

微分配器

线程缓存中还包含几个用于分配微对象的字段,下面的这三个字段组成了微对象分配器,专门管理 16 字节以下的对象:

type mcache struct {
    ...
    // Allocator cache for tiny objects w/o pointers.
	// See "Tiny allocator" comment in malloc.go.

	// tiny points to the beginning of the current tiny block, or
	// nil if there is no current tiny block.
	//
	// tiny is a heap pointer. Since mcache is in non-GC'd memory,
	// we handle it by clearing it in releaseAll during mark
	// termination.
	//
	// tinyAllocs is the number of tiny allocations performed
	// by the P that owns this mcache.
	tiny       uintptr
	tinyoffset uintptr
	tinyAllocs uintptr
    ...
}

微分配器只会用于分配非指针类型的内存,上述三个字段中 tiny 会指向堆中的一片内存,tinyoffset 是下一个空闲内存所在的偏移量,最后的 tinyAllocs 会记录内存分配器中分配的对象个数。

mcentral

runtime.mcentral 是内存分配器的中心缓存,与线程缓存不同,访问中心缓存中的内存管理单元需要使用互斥锁。

// Central list of free objects of a given size.
type mcentral struct {
	spanclass spanClass
	partial  [2]spanSet
	full     [2]spanSet
}

每个中心缓存都会管理某个跨度类的内存管理单元,它会同时持有两个 runtime.spanSet,分别存储包含空闲对象和不包含空闲对象的内存管理单元。

当 mcache 的某个类别 span 的内存被分配光时,它会会通过中心缓存的 runtime.mcentral.cacheSpan 方法获取新的内存管理单元。

mheap

runtime.mheap 页堆是内存分配的核心结构体,Go 语言程序会将其作为全局变量存储,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。

页堆中包含一个长度为 136 的 runtime.mcentral 数组,其中 68 个为跨度类需要 scan 的中心缓存,另外的 68 个是 noscan 的中心缓存:

type mheap struct {
    ...
	// central free lists for small size classes.
	// the padding makes sure that the mcentrals are
	// spaced CacheLinePadSize bytes apart, so that each mcentral.lock
	// gets its own cache line.
	// central is indexed by spanClass.
	central [numSpanClasses]struct {
		mcentral mcentral
		pad      [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
	}
    ...
}

Go 所有的内存空间都由如下所示的二维矩阵 runtime.heapArena 管理,这个二维矩阵管理的内存可以是不连续的。

在这里插入图片描述

type mheap struct {
    ...
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    ...
}

3.内存分配

堆上所有的对象都会通过调用 runtime.newobject 函数分配内存,该函数会调用 runtime.mallocgc 分配指定大小的内存空间,这也是用户程序向堆上申请内存空间的必经函数。

Go 中的内存大小分类并不像 TCMalloc 那样分成小、中、大对象,而是分成微对象、小对象和大对象三种。Go 的内存分配器会根据申请分配的内存大小选择不同的处理逻辑。

类别大小
微对象(0, 16B)
小对象[16B, 32KB]
大对象(32KB, +∞)
  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存。
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存。
  • 大对象 (32KB, +∞) — 直接在堆上分配内存。

4.内存管理思想

Go 内存管理核心思想可以分为以下几点:

  • 每次从操作系统申请一大块儿的内存,由 Go 对这块儿内存做分配,减少系统调用。
  • 内存分配借鉴了 Google 的 TCMalloc(Thead-Caching Malloc)算法。

TCMalloc 的核心思想是:

(1)内存切分,减少碎片。

  • 采用了 span 机制来减少内存碎片。多个连续的内存页(8KB)组成 span,每个 span 又划分成大小固定的多个 slot。
  • slot size 有 67 种,每个 size 有两种类型,scan 和 noscan,表示分配的对象是否包含指针。

(2)分级管理,无锁并降低锁的粒度。

  • 多层次的分配 Cache,每个 P 上有一个 mcache,mcache 会为每个 size 最多缓存一个 span,用于无锁分配。
  • 全局每个 size 的 span 都有一个 mcentral,锁的粒度相对于全局的 mheap 小很多。每个 mcentral 可以看成是每个 size 的 span 的一个全局后备 cache。获取不到再上升到全局的 mheap。mheap 获取不到再向系统申请。从无锁到全局 1/(67*2)力度的锁,再到全局锁,再到系统调用。

(3)回收复用

  • 内存由 GC 进行释放。回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。
  • 只有内存闲置过多的时候,sysmon 协程会定时把 mheap 空余的内存归还给操作系统,降低整体开销。

参考文献

图解 TCMalloc
内存分配器 - Go语言设计与实现
超干货!彻底搞懂Golang内存管理和垃圾回收 - 腾讯云
golang内存管理和分配机制 - Levon’s Blog

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1328118.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高校/企业如何去做数据挖掘呢?

随着近年来人工智能及大数据、云计算进入爆发时期&#xff0c;依托三者进行的数据分析、数据挖掘服务已逐渐成为各行业进行产业升级的载体&#xff0c;缓慢渗透进我们的工作和生活&#xff0c;成为新时代升级版的智能“大案牍术”。 那么对于多数企业来说&#xff0c;如何做数据…

性能压力测试--确保企业数字化业务稳健运行

随着企业的数字化转型和依赖云计算的普及&#xff0c;软件系统的性能已经成为企业成功运营的关键因素之一。性能压力测试作为确保系统在各种条件下都能高效运行的关键步骤&#xff0c;对企业的重要性不可忽视。以下是性能压力测试对企业的几个重要方面的影响和作用&#xff1a;…

在Portainer创建Nginx容器并部署Web静态站点实现公网访问

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;…

Ubuntu 22.04 禁用(彻底移除)Snap

什么是Snaps Snaps 是 Ubuntu 的母公司 Canonical 于 2016 年 4 月发布 Ubuntu 16.04 LTS&#xff08;Long Term Support&#xff0c;长期支持版&#xff09;时引入的一种容器化的软件包格式。自 Ubuntu 16.04 LTS 起&#xff0c;Ubuntu 操作系统可以同时支持 Snap 及 Debian …

汽车制造厂设备故障预测与健康管理PHM

在现代汽车制造工业中&#xff0c;设备的可靠性和稳定性对于保证生产线的高效运行至关重要。为了提高生产效率、降低维修成本以及确保产品质量&#xff0c;汽车制造厂逐渐采用设备故障预测与健康管理&#xff08;PHM&#xff09;系统&#xff0c;以实现对设备状态的实时监测和预…

TypeScript实战——ChatGPT前端自适应手机端,PC端

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 可以在线体验哦&#xff1a;体验地址 文章目录 前言引言先看效果PC端手机端 实现原理解释 包的架构目录 引言 ChatGPT是由OpenAI开发的一种基于语言模型的对话系统。它是GPT&#xff08;…

ChatGPT 发布了免费提示指南 用PROMPT法打造完美的ChatGPT对话

ChatGPT 刚刚发布了免费提示指南。 但大多数人仍在为课程付费。 这里有 6 种免费策略&#xff0c;可以帮助您获得更好的即时结果&#xff1a; 1. 明确的指示 → 准确表达您的要求。 → 对于简短的回复&#xff0c;请提及。 → 需要更多细节&#xff1f;直接询问。 → 展示您…

C++的泛型编程—模板

目录 一.什么是泛型编程&#xff1f; ​编辑 ​编辑 二.函数模板 函数模板的实例化 当不同类型形参传参时的处理 使用多个模板参数 三.模板参数的匹配原则 四.类模板 1.定义对象时要显式实例化 2.类模板不支持声明与定义分离 3.非类型模板参数 4.模板的特化 函数模板…

零基础学C语言——表达式和语句

这是一个C语言系列文章&#xff0c;如果是初学者的话&#xff0c;建议先行阅读之前的文章。笔者也会按照章节顺序发布。 在上一篇的示例中其实出现过很多表达式和语句&#xff0c;例如&#xff1a; int a 1 2;其中&#xff0c;1 2是一个表达式&#xff0c; a 1 2也是一个…

【算法刷题】Day22

文章目录 1. 按摩师题干&#xff1a;算法原理&#xff1a;&#xff08;dp&#xff09;1. 状态表示&#xff1a;2. 状态转移方程3. 初始化4. 填表顺序5. 返回值 代码&#xff1a; 2. 寻找数组的中心下标题干&#xff1a;算法原理&#xff1a;&#xff08;前缀和&#xff09;代码…

【hcie-cloud】【11】华为云Stack资源与服务扩建【云服务扩容、自动化变更平台公共服务组件、华为云Stack典型高阶服务扩容简介、缩略词】【下】

文章目录 扩容工程简介、扩容管理规模、计算资源扩容与减容云服务扩容云服务扩容简介华为云Stack云服务扩容背景华为云Stack可选基础云服务华为云Stack可选网络服务华为云Stack可选高阶服务华为云Stack云服务扩容流程 自动化变更平台&公共服务组件背景介绍自动化变更平台&a…

ESPCN训练报错解决

ESPCN训练报错解决 超分重建ESPCN训练自己的数据集时会出现报错,主要为制作数据集时的错误,本文将给出报错的解决方法和错误说明。 目录 ESPCN训练报错解决错误一一、错误说明二、解决方法三、错误说明错误二一、错误说明二、解决方法错误一 一、错误说明 在使用自己的数据…

flowable流程部署后act_re_procdef表无对应模型得数据

情况1&#xff1a; 修改前&#xff1a; public static final String XML ".xml"; repositoryService.createDeployment().key(model.getKey()).name(model.getName()).addBytes(model.getName() XML, bpmnXML).deploy();修改后&#xff1a; public static final …

Java_集合进阶Map集合

一、Map集合 1.1 Map概述体系 各位同学&#xff0c;前面我们已经把单列集合学习完了&#xff0c;接下来我们要学习的是双列集合。首先我们还是先认识一下什么是双列集合。 所谓双列集合&#xff0c;就是说集合中的元素是一对一对的。Map集合中的每一个元素是以keyvalue的形式…

Tomcat转SpringBoot、tomcat升级到springboot、springmvc改造springboot

Tomcat转SpringBoot、tomcat升级到springboot、springmvc改造springboot 起因&#xff1a;我接手tomcat-springmvc-hibernate项目&#xff0c;使用tomcat时问题不大。自从信创开始&#xff0c;部分市场使用国产中间件&#xff0c;例如第一次听说的宝兰德、东方通&#xff0c;还…

众和策略:美股全线上涨 中概股大涨

当地时间12月21日&#xff0c;欧洲股市全线下跌&#xff0c;英国富时100指数、法国CAC40指数、德国DAX指数均小幅下跌。美国通胀降温&#xff0c;美股商场三大指数尾盘飙升&#xff0c;纳斯达克指数、标普500指数均涨逾1%&#xff0c;大型科技股多数上涨&#xff0c;特斯拉涨近…

Jenkins自动化构建打包,部署

1.环境准备 上传jdk&#xff0c;maven和tomcat的包&#xff0c;解压到/usr/local下并配置环境变量。 配置jdk [rootserver04 ~]# vim /etc/profile.d/java.sh JAVA_HOME/usr/local/java export PATH$JAVA_HOME/bin:$PATH##加载环境变量 [rootserver04 ~]# source /etc/profi…

Mybatis之增删改查

一、引言 书接上回&#xff0c;我们在了解完mybatis之后&#xff0c;肯定要知道怎么使用&#xff0c;本文就来详细讲解Mybatis的增删改查事务&#xff0c;还不了解怎么配置mybatis的童鞋可以去这篇文章了解一下通俗易懂讲解javaweb之mybatis-CSDN博客 二、Mybatis——增 举例…

Android笔记(二十):JetPack DataStore 之 Proto DataStore

Jetpack DataStore 是一种数据存储解决方案&#xff0c;主要适用于小型数据的处理。它可以通过协议缓冲区存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。DataStore有两种实现方式&#xff08;1&#xff09;Preferences DataStor…