在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
go设计与实现把go内存分配器介绍的很详细,起始一般情况下程序员不怎么会用到。需要简单了解下即可。如果没时间看,看看下述内容即可。 栈区堆区概念要理解。分配方法其实就是基于算法中的数组和链表,优缺点都类似。 go采用的空闲链表分配。并采取了隔离适应来规避链表的缺陷。 通过将对象大小分成微对象[<16B]小对象[16-32KB] 大对象[>32KB],并且通过多级缓存提高分配效率。 通过将对象大小分成微对象[<16B]小对象[16-32KB] 大对象[>32KB],并且通过多级缓存提高分配效率。 通过将对象大小分成微对象[<16B]小对象[16-32KB] 大对象[>32KB],并且通过多级缓存提高分配效率。(重要的事情说三遍,将对象分类并使用多级缓存是go内存管理的重要思想)
最重要的内存管理组件。内存管理单元mspan、线程缓存mcache、中心缓存mcentral、页堆mheap简单的了解下。 这些组件的源码都在runtime包中 go以页为单位管理内存(跟操作系统的页不同),每个mspan会持有多个页。 mspan是基础,mcache是为每个goroutine各自分配的缓存,mcentral可以理解成是内存池,mheap是更大的池。 当mspan不足,会向mcentral申请,mcentral不足向mheap申请,mheap不足向操作系统申请。 一般了解到这些内容就可以了。有时间可以详细往下看。 一、内存管理的基础概念 内存空间分为两个重要区域。栈区Stack和堆区Heap。函数调用的参数,返回值以及局部遍历大都分配到栈上,由编译器管理。不同编程语言使用不同的方法管理堆区的内存,C++ 等编程语言会由工程师主动申请和释放内存,Go 以及 Java 等编程语言会由工程师和编译器共同管理,堆中的对象由内存分配器分配并由垃圾收集器回收。 设计原理 内存管理一般包含三个不同的组件,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector),当用户程序申请内存时,它会通过内存分配器申请新的内存,而分配器会负责从堆中初始化相应的内存区域。 分配方法 编程语言的内存分配器一般包含两种分配方法,一种是线性分配器(Sequential Allocator,Bump Allocator),另一种是空闲链表分配器(Free-List Allocator) 线性分配器线性分配(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们在编程语言中使用线性分配器,我们只需要在内存中维护一个指向内存特定位置的指针,当用户程序申请内存时,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针:
优点:执行快,容易实现 缺点:无法重用内存,需要搭配合适的垃圾回收算法
回收中产生的内存碎片,需要搭配合适的垃圾回收算法。标记压缩(Mark-Compact)、复制回收(Copying GC)和分代回收(Generational GC)等算法可以通过拷贝的方式整理存活对象的碎片,将空闲内存定期合并,这样就能利用线性分配器的效率提升内存分配器的性能了。 空闲链表分配器空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表:
优点:内存复用方便 缺点:分配内存需要遍历整个链表,耗时长。 空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择,最常见的就是以下四种方式: 首次适应(First-Fit)— 从链表头开始遍历,选择第一个大小大于申请内存的内存块;
Go 语言使用的内存分配策略与第四种策略有些相似,我们通过下图了解一下该策略的原理:
如上图所示,该策略会将内存分割成由 4、8、16、32 字节的内存块组成的链表,当我们向内存分配器申请 8 字节的内存时,我们会在上图中的第二个链表找到空闲的内存块并返回。隔离适应的分配策略减少了需要遍历的内存块数量,提高了内存分配的效率。 分级分配线程缓存分配(Thread-Caching Malloc,TCMalloc)是用于分配内存的的机制,它比 glibc 中的 对象大小Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种:
多级缓存内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存: 线程缓存属于每一个独立的线程,它能够满足线程上绝大多数的内存分配需求,因为不涉及多线程,所以也不需要使用互斥锁来保护内存,这能够减少锁竞争带来的性能损耗。当线程缓存不能满足需求时,就会使用中心缓存作为补充解决小对象的内存分配问题;在遇到 32KB 以上的对象时,内存分配器就会选择页堆直接分配大量的内存。 这种多层级的内存分配设计与计算机操作系统中的多级缓存也有些类似,因为多数的对象都是小对象,我们可以通过线程缓存和中心缓存提供足够的内存空间,发现资源不足时就从上一级组件中获取更多的内存资源。
内存管理组件 Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件,这几种最重要组件对应的数据结构
所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局,每一个处理器都会被分配一个线程缓存 每个类型的内存管理单元都会管理特定大小的对象,当内存管理单元中不存在空闲对象时,它们会从 在 amd64 的 Linux 操作系统上, 内存管理单元
type mspan struct { next *mspan prev *mspan ... } 串联后的上述结构体会构成如下双向链表,运行时会使用
页和内存每个 type mspan struct { startAddr uintptr // 起始地址 npages uintptr // 页数 freeindex uintptr allocBits *gcBits gcmarkBits *gcBits allocCache uint64 ... }
图 7-12 内存管理单元与页 当用户程序或者线程向
如果我们能在内存中找到空闲的内存单元,就会直接返回,当内存中不包含空闲的内存时,上一级的组件 跨度类
type mspan struct { ... spanclass spanClass ... } Go 语言的内存管理模块中一共包含 67 种跨度类,每一个跨度类都会存储特定大小的对象并且包含特定数量的页数以及对象,所有的数据都会被预选计算好并存储在
跨度类的数据 上表展示了对象大小从 8B 到 32KB,总共 66 种跨度类的大小、存储的对象数以及浪费的内存空间 除了上述 66 个跨度类之外,运行时中还包含 ID 为 0 的特殊跨度类,它能够管理大于 32KB 的特殊对象 线程缓存
线程缓存与内存管理单元 线程缓存在刚刚被初始化时是不包含 微分配器线程缓存中还包含几个用于分配微对象的字段,下面的这三个字段组成了微对象分配器,专门为 16 字节以下的对象申请和管理内存: type mcache struct { tiny uintptr tinyoffset uintptr local_tinyallocs uintptr } 微分配器只会用于分配非指针类型的内存,上述三个字段中 中心缓存
type mcentral struct { lock mutex spanclass spanClass nonempty mSpanList empty mSpanList nmalloc uint64 } 每一个中心缓存都会管理某个跨度类的内存管理单元,它会同时持有两个
中心缓存和内存管理单元 该结构体在初始化时,两个链表都不包含任何内存,程序运行时会扩容结构体持有的两个链表, 内存管理单元线程缓存会通过中心缓存的
页堆
页堆中包含一个长度为 134 的
go语言设计与实现书中提到的东西远不止这些。还有很多更详细更深奥的东西。如果有兴趣可以买书或者看电子书。 问题:为什么分成67类对象?微对象,小对象,大对象分类的标准是什么,为什么这么分? |
请发表评论