浅谈golang内存管理

内存管理

1. 简介

程序要运行,离不开内存的分配;内存作为计算机中稀缺资源,内存管理的好坏直接决定程序的优劣。 在编程语言中内存的管理分为两类:

  1. 程序员手动管理(诸如C、C++)
  2. 由编程语言自己管理(比如,python、ruby、go等)

我们总是希望程序能够占用尽量少的内存,实现尽量高的性能;因此无论是操作系统层面,还是编程语言层面都做了许多的努力。

当然go语言为了实现这个目标,对内存的使用和回收,也是精打细算,做了很多优化,下面我们一起来看看。

2. 程序内存该如何分配才好?

假设我们是一门程序语言的实现者,要实现一个高性能的内存管理语言,我们不禁要问,什么样的内存分配才是好的分配?

首先,内存的申请需要通过操作系统的系统调用实现,而系统调用是有开销的;如果我们每次需要用一点内存,都去通过操作系统系统调用,那么开销可想而知,因此我们不希望频繁的向操作系统申请内存,最好一次申请一大块。

另外,进程拿到一大块内存后,是交给多个线程使用的,也就是都有使用权,在某一个时刻线程A和线程B都需要内存,为了避免他们拿到同一个内存地址,那么此时需要对整块内存加锁。

有了竞争,然后有了锁,既然有锁那么就有开销,我们该如何减少锁的开销呢?

您可以继续思考下去...

经过前面的思考,相信你已经对内存的分配有了一个感官的认识,下面我们看看go是怎么做的。

3. go内存分配框架

go的内存管理是基于TCMalloc(Thread Cache Malloc)核心思想实现的,那什么是TCMalloc呢?

每个线程会维护一个线程内存缓存(ThreadCache),从而减少直接向上层(CentralCache)获取时的锁竞争,每个线程需要内存时优先从线程缓存中获取,由于ThreadCache是每个线程独享的,此时无需加锁;如果ThreadCahe不足,则会从CentralCache获取,centralCache是所有缓存共享的,因此此时需要加锁。

go在借鉴TCMallco的同时做了进一步细化,总体上分了三层:mcache、mcentral、mheap,总体结构如下:

Mcache、Mcentral、MHeap粒度依次由小到大

  1. Mcache 和GMP模型中的P绑定,每个P都有一个Mcache,因此每个Goroutine运行时优先从Mcache中获取内存,如果Mcache中内存不足,才向上级(Mcentral)申请,直接在Mcache中获取内存无需加锁

  2. Mcentral Mcentral介于Mcache和Mheap之间,当Mcache中内存不够时从Mcentral申请;Mcentral中内存不够时向Mheap申请,Mcentral按照不同的对象大小刻度(比如:8B、16B、32B...)做了区分,因此从指定大小的Mcentral申请内存,只需要锁定对应对象大小的Mcentral就行

  3. MHeap MHeap是go内存管理的最大粒度层,它是全局共享的;当Mcentral中没有足够内存时,会向MHeap中获取,此时加的锁也最大;如果Mheap中内存不够,则会向操作系统申请,发起系统调用。

另外,并非是所有大小内存的分配都需要按照上面的层级逐层申请,有时候是可以跨越层级的。

在go中将对象的大小分为三个层级:

  1. tiny微对象 < 16B ------ 直接在Mcache中分配
  2. 小对象 16B ~ 32KB ------ 逐层走流程正常申请
  3. 大对象 > 32KB ------ 直接到Mheap中申请(跳过Mcentral

之所以这样设计也很好理解,微对象在Mcache中做了专门的处理,目的是减少内存的浪费;大对象直接从Mheap,这样效率会更高。

4. size_class、mspan、object

前面我们从整体层的角度认识了go内存分配,但是缺乏对内存分配细节的把控,这里我们进一步拿出放大镜看看go内存基本单位mspan。

Mcache和Mcentral中都有mspan,那什么是mspan呢? 在go中也像操作系统内存中page的概念,只不过含义不同,一个page的大小为8KB,一个mspan由整数个page组成。

这很好理解,画出来大概这样。

从这么看,page就是最小的组成了,真的是这样么? No,在go中实际上会根据刻度大小------size_class来划定每个mspan的object大小。

我们假定这个mspan就一个page,那么表示出来应该是这样的。

size_class依次为8B、16B、32B、48B、64B...总共67种规格,当需要分配内存时,选择对应的规格取出object使用即可。

因此,P与Mcache内存交换单位是object,而Mcentral和Mheap之间内存交换单位是Mspan。

我来看一眼size_class表,有个影响即可

go 复制代码
// 这只是一部分
// class - 也就size_class 
// bytes/object object对象大小
// bytes/span span的大小
// objects 有多少个object
// tail waste span被分配为指定规格object会有多少内存浪费
// max waste 最大浪费

// class  bytes/obj  bytes/span  objects  tail waste  max waste
//     1          8        8192     1024           0        87.50%
//     2         16        8192      512           0        43.75%
//     3         32        8192      256           0        46.88%
//     4         48        8192      170          32        31.52%
//     5         64        8192      128           0        23.44%
//     6         80        8192      102          32        19.07%
//     7         96        8192       85          32        15.95%
//     8        112        8192       73          16        13.56%
//     9        128        8192       64           0        11.72%
//    10        144        8192       56         128        11.82%

前面我们已经了解了mspan、object、page、size_class这些概念,但是没有结合Mcache看看,下面我们一起看下它们整体是如何搭配的。

5. 总结

总体来看,go对内存的管理非常精细

  1. 通过分级缓存的方式,极大的减少锁的竞争;
  2. 通过Mcache和P绑定的方式,一个goroutine需要内存直接从Mcache中获取提高内存访问局部性。
  3. 利用size_class, 将内存细化为不同的规格大小,极大的减少内存碎片,提高内存分配效率。

参考资料:

  1. Golang三关-典藏版一站式Golang内存洗髓经
  2. Go 内存分配器可视化指南
相关推荐
金銀銅鐵3 小时前
[Java] 用 GUI 展示 EnumSet 增减元素时的内部变化(上)
java·后端·ai编程
念何架构之路4 小时前
go-grpc设计思想
开发语言·后端·golang
system_error_x4 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
步行cgn6 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
码事漫谈6 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780517 小时前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123457 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒7 小时前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记7 小时前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598787 小时前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端