Golang 的内存管理

文章目录

  • 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.mfbz.cn/a/262581.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx快速入门:负载均衡upstream配置详解(四)

0. 引言 我们在第二章的时候简单演示了关于nginx实现负载均衡的演示&#xff0c;而实际上nginx支持很多负载均衡算法&#xff0c;并且多节点的转发也有多种策略。今天我们继续深入学习这块。 1. 负载均衡的应用场景 所谓负载均衡&#xff0c;Load Balance &#xff0c;就是将…

Jmeter自定义用户变量模拟多用户

java1234,56a801e9c869452fa092c9657cfc2051 jack,b6e528cca41143dea9c2c3e9ca5d6390

Linux环境安装Hadoop

&#xff08;1&#xff09;下载Hadoop安装包并上传 下载Hadoop安装包到本地&#xff0c;并导入到Linux服务器的/opt/software路径下 &#xff08;2&#xff09;解压安装包 解压安装文件并放到/opt/module下面 [roothadoop100 ~]$ cd /opt/software [roothadoop100 software…

基于SpringBoot的教学管理app的开发-计算机毕业设计源码65449

摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对教学管理等问题&#xff0c;对其进行研究分…

如何在本地安装Flask并将其web界面发布到公网上远程访问协同开发

目录 前言 1. 安装部署Flask 2. 安装Cpolar内网穿透 3. 配置Flask的web界面公网访问地址 4. 公网远程访问Flask的web界面 前言 本篇文章讲解如何在本地安装Flask&#xff0c;以及如何将其web界面发布到公网上并进行远程访问。 Flask是目前十分流行的web框架&#xff0c;…

电气 接近开关

npn&#xff1a;和负载&#xff08;控制器或者继电器&#xff09;共阳极&#xff0c;低电平响应 pnp&#xff1a;和负载共阴极&#xff0c;高电平响应

MyBatisX生成时的选项的含义

一般&#xff0c;annotation和template勾选MyBatis-Plus 3 options中各选项的作用 comment&#xff1a;实体类各属性的注释&#xff08;数据库中有的话&#xff09;以及生成TableId注解&#xff0c;同时会给serialVersionUID属性加上TableField(exist false) toString/hashCo…

OpenSergo使用详解

简介 OpenSergo是一个基于微服务治理的标准和生态&#xff0c;覆盖了服务元信息、流量治理、服务容错、数据库/缓存治理、服务注册发现、配置治理等十几个关键领域&#xff0c;覆盖了完整的微服务生命周期&#xff08;从开发态到测试态&#xff0c;到发布态&#xff0c;再到运…

不用翻箱倒柜找数据线,电脑投屏到另一台电脑,无线、远程投屏方法!

对于职场人士来说&#xff0c;电脑投屏电脑绝对是必不可少的工具&#xff0c;稳定高效的投屏开会是职场人渴望的技能&#xff0c;总比天天抱着笔记本走来走去&#xff0c;还要去翻箱倒柜的找数据线要强。如何实现电脑间的稳定投屏&#xff0c;AirDroid Cast提供了一个好思路。 …

ssm基于vue.js的连锁干洗店后台管理系统论文

摘 要 使用旧方法对连锁干洗店后台管理信息进行系统化管理已经不再让人们信赖了&#xff0c;把现在的网络信息技术运用在连锁干洗店后台管理信息的管理上面可以解决许多信息管理上面的难题&#xff0c;比如处理数据时间很长&#xff0c;数据存在错误不能及时纠正等问题。 这次…

最小操作次数问题

思路如下&#xff1a; 1.其他颜色变成红色球的情况:蓝色变红色需要2步 所以"2 绿色变红色需要1步 所以 绿色 2.其他颜色变成蓝色球的情况:绿色变蓝色需要2步 红色变蓝色需要1步 3.其他颜色变成绿色球的情况 红色变绿色需要2步 蓝色变绿色需要1步 代码如下&#xff1a…

免费分享一套Springboot+Vue前后端分离的个人博客系统,挺漂亮的

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringbootVue前后端分离的个人博客系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringbootVue个人博客系统 毕业设计 Java毕业设计_哔哩哔哩_bilibili【免费】SpringbootVue个人博客系统 毕业设计 …

Unity DOTS物理引擎的核心分析与详解

最近DOTS发布了正式的版本,同时基于DOTS的理念实现了一套高性能的物理引擎&#xff0c;今天我们来给大家分享和介绍一下这个物理引擎的使用。 Unity.Physics的设计哲学 Unity.Physics是基于DOTS设计思想的一个高性能C#物理引擎的实现, 包含了物理刚体的迭代计算与碰撞检测等查…

什么专业适合学网络安全?

1.选什么专业 想当黑客要学什么专业&#xff1f;首先要明白一个点&#xff0c;当黑客你只需要会攻击手段就可以了&#xff0c;但绝不是只会使用一些工具而已&#xff0c;不然那就是“脚本小子”。 不严格来说&#xff0c;计算机所有的专业都可以当黑客&#xff0c;因为基础的东…

《Nature》预测 2024 科技大事:GPT-5预计明年发布等

《Nature》杂志近日盘点了 2024 年值得关注的科学事件&#xff0c;包括 GPT-5 与新一代 AlphaFold、超算 Jupiter、探索月球任务、生产「超级蚊子」、朝向星辰大海、试验下一代新冠疫苗、照亮暗物质、意识之辩第二回合、应对气候变化。 今年以来&#xff0c;以 ChatGPT 为代表…

【飞凌 OK113i-C 全志T113-i开发板】一些有用的常用的命令测试

一些有用的常用的命令测试 一、系统信息查询 可以查询板子的内核信息、CPU处理器信息、环境变量等 二、CPU频率 从上面的系统信息查询到&#xff0c;这是一颗具有两个ARMv7结构A7内核的处理器&#xff0c;主频最高1.2GHz 可以通过命令查看当前支持的频率以及目前所使用主频 …

fastapi 处理请求参数整理

1、路径参数 app.get("/getfilecontent/{id}/{pre}") def get_filecontent(id,pre):"""路径参数&#xff0c;测试接口&#xff0c;读取当前工作目录下的file.txt文件&#xff0c;返回txt中的内容:param id: The ID of the item to retrieve.:param p…

解决使用傅里叶变换开源库fftw分析音频频谱结果与matlab或audacity不一致的问题

找的一些demo输出结果与实际结果相差巨大&#xff0c;修复后效果如下&#xff1a; 采用一个采样率48000&#xff0c;精度16bit&#xff0c;单通道的46Hz,振幅为32767的正弦波测试&#xff08;理论上应该得输出一个一模一样的正弦波&#xff09;。输出如下图&#xff0c;可以看…

工业5G路由器提升驾考效率,实现智慧驾考物联网

为了提高驾考的考试效率&#xff0c;更好地服务广大学员&#xff0c;车管所驻考场监控中心结合物联网技术采用智慧驾考系统&#xff0c;实现考场监控、考试员远程监考、学员视频实时回传、自动评判等功能&#xff0c;为驾考公平公正安全提供保障。 该系统由智能监控管理平台和…

百模大战中的AI行业:新趋势与未来发展

文章目录 每日一句正能量前言技术进步应用拓展行业变革人才竞争后记 每日一句正能量 人生最重要的价值是心灵的幸福&#xff0c;而不是任何身外之物。 前言 随着科技的迅猛发展&#xff0c;人工智能&#xff08;AI&#xff09;已经成为引领技术革命的重要驱动力之一。在当前的…
最新文章