浅谈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 内存分配器可视化指南
相关推荐
小池先生32 分钟前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
小蜗牛慢慢爬行2 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10432 小时前
java web springboot
java·spring boot·后端
龙少95434 小时前
【深入理解@EnableCaching】
java·后端·spring
溟洵6 小时前
Linux下学【MySQL】表中插入和查询的进阶操作(配实操图和SQL语句通俗易懂)
linux·运维·数据库·后端·sql·mysql
SomeB1oody8 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody8 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
啦啦右一10 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring
森屿Serien10 小时前
Spring Boot常用注解
java·spring boot·后端
盛派网络小助手12 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#