内存分配管理

内存分配管理

1.基本概念

Go构建了一套自主内存管理分配,没有使用传统的内存分配器malloc,器,原理类似于tcmalloc。为了方便管理内存,首先会向系统申请一大块内存,将内存切分成小块,通过内存分配的办法进行管理。

 

1.1 基本策略:

1>. 每次从操作系统申请一大块内存(比如 1MB),以减少系统调用。

2>. 将申请到的大块内存按照特定大小预先切分成小块,构成链表。

3>. 为对象分配内存时,只需从大小合适的链表提取一个小块即可。

4>. 回收对象内存时,将该小块内存重新归还到原链表,以便复用。

5>. 如闲置内存过多,则尝试归还部分内存给操作系统,降低整体开销

 

1.2 基本结构:

1.2.1虚拟地址空间

 

1>. 使用arena 地址向操作系统申请内存,大小决定了可分配用户内存上限。

2>. 位图 bitmap 为每个对象提供 4bit 标记位,用以保存指针、GC 标记等信息。

3>. 创建 span 时,按页填充对应 spans 空间。在回收 object 时,只需将其地址按页对齐后就可找到所属 span。分配器还用此访问相邻 span,做合并操作。

arena

arena区域就是我们通常说的heap, go从heap分配的内存都在这个区域中.

 

bitmap

以bitmap区域的大小是 512GB / 指针大小(8 byte) / 4 = 16GB.

spans

spans区域中一个指针(8 byte)对应了arena区域中的一页(在go中一页=8KB).

512GB / 页大小(8KB) * 指针大小(8 byte) = 512MB.

 

 

GC Bitmap

bitmap区域: 涵盖了arena区域, 使用2 bit表示一个指针大小的内存

函数信息: 涵盖了函数的栈空间, 使用1 bit表示一个指针大小的内存 (位于stackmap.bytedata)

类型信息: 在分配对象时会复制到bitmap区域, 使用1 bit表示一个指针大小的内存 (位于_type.gcdata)

 

需要注意的是span结构本身的内存是从系统分配的, 上面提到的spans区域和bitmap区域都只是一个索引.

 

span中的元素大小是8 byte, span本身占1页也就是8K, 一共可以保存1024个对象.

首先从P的缓存(mcache)获取, 如果有缓存的span并且未满则使用, 这个步骤不需要锁

然后从全局缓存(mcentral)获取, 如果获取成功则设置到P, 这个步骤需要锁

最后从mheap获取, 获取后设置到全局缓存, 这个步骤需要锁

 

 

1.2.2 内存块种类

分配器将其管理的内存块分为两种:

span: 由多个地址连续的页(page)组成的大块内存。

object: 将 span 按特定特定切分成多个小块,每个小块可存储一个对象。

 

1.2.3 分配器组件

分配器由三种组件组成。

cache: 每个运行期间作线程都会绑定一个 cache,用于无锁 object 分配。

central: 为所有 cache 提供切分好的后备 span 资源。

heap: 管理闲置 span,需要时向操作系统申请新内存

 

1.3 分配流程

1>. 计算待分配对象对应规格(size class)。

2>. 从 cache.alloc 数组找到规格相同的 span。

3>. 从 span.freelist 链表提取可用object。

4>. 如 span.freelist 为空,从 central 获取新 span。

5>. 如 central.nonempty 为空,从 heap.free/freelarge 获取,并切分成 object 链表。

6>. 如 heap 没有大小合适的闲置 span,向操作系统申请新内存块。

 

1.4  释放流程

1>. 将标记为可回收 object 交还给所属 span.freelist。

2>. 该 span 被放回 central,可供任意 cache 重新获取使用。

3>. 如 span 已收回全部 object,则将其交还给 heap,以便重新切分复用。

4>. 定期扫描 heap里长时间闲置的 span,释放其占用内存

 

假如 cache1 获取一个 span 后,仅使用了一部分 object,那么剩余空间就可能会被浪费。而回收

操作将该 span 交还给 central 后,该 span 完全可以被 cache2、cacheN 获取使使用。此时,cache1已不再持有该 span,完全不会造成问题。

 

 

2. 源码分析

2.1 newobject

go从堆分配对象时会调用newobject函数, 这个函数的流程大致如下:

这三个阶段的详细结构如下图:

 

内存分配初始化工作:

1. 创建对象规格大小对照表。

2. 计算相关区域大小,并尝试从某个指定位置开始保留地址空间。

3. 在 heap里保存区域信息,包括起始位置和大小。

4. 初始化 heap 其他属性。

 

mheap内存管理示意图如下:

 

type mheap struct {

    spans **mspan

    spans_mapped uintptr

    bitmap uintptr

    bitmap_mapped uintptr

    arena_start uintptr

    arena_used uintptr

    arena_end uintptr

    arena_reserved bool

}

 

func mallocinit() {

    //初始化规格对照表

    initSizes()

    //初始化head属性

    mheap_.spans = (**mspan)(unsafe.Pointer(p1))

    mheap_.bitmap = p1 + spansSize

    mheap_.arena_start = p1 + (spansSize + bitmapSize)

    mheap_.arena_used = mheap_.arena_start

    mheap_.arena_end = p + pSize

    mheap_.arena_reserved = reserved

    //初始化heap

    mHeap_Init(&mheap_, spansSize)

}

 

内存虚拟内存和物理内存的使用情况

1. 尽管初始化时预留了 544GB 的虚拟地址空间,但并没有分配内存。

2. 操作系统大多采取机会主义分配策略。申请内存时,仅承诺但不立即即分配物理内存。

3. 物理内存分配发生在写操作导致缺页异常调度时,且按页提供。

 

为对象分配内存需区分在栈还是堆上完成。通常情况下,编译器有责任尽可能使用寄存器

和栈来存储对象,这有助于提升性能,减少垃圾回收器压力。

 

newobject 具体是如何为对象分配内存的

type mcache struct {

    // Allocator cache for tiny objects w/o pointers.

    tiny unsafe.Pointer

    tinyoffset uintptr

    alloc [_NumSizeClasses]*mspan

}

 

func newobject(typ *_type) unsafe.Pointer {

    return mallocgc(uintptr(typ.size), typ, flags)

}

func mallocgc(size uintptr, typ *_type, flags uint32) unsafe.Pointer {

 

}

大对象直接从 heap 获取 span。

小对象从 cache.alloc[sizeclass].freelist 获取 object。

微小对象组合使用cache.tiny object。

 

内存回收

回收操作自然不会直接盯着单个对象,而是以 span 为基本单位。通过对

对 bitmap里的扫描标记,逐步将 object 收归原 span,最终上交 central 或 heap 复用。

 

func mSpan_Sweep(s *mspan, preserve bool) bool {

//为span空闲object设置标记,无需再次扫描

for link := s.freelist; link.ptr() != nil; link = link.ptr().next {

heapBitsForAddr(uintptr(link)).setMarkedNonAtomic()

}

}

 

内存释放

在入口函数main.mian里,会专门启动一个监控任务sysmon,每隔一段时间就会检查heap里的闲置内存块。

func sysmon() {

    scavengelimit := int64(5 * 60 * 1e9)

    for {

        usleep(delay)

        if lastscavenge+scavengelimit/2 < now {

            mHeap_Scavenge(int32(nscavenge), uint64(now), uint64(scavengelimit))

            lastscavenge = now

        }

    }

}

两种对象

整个进程内的对象可分为两类。其一,自然是从 arena 区域分配的用户

对象;另一种,则是运行时自身运行和管理所需,比如管理 arena 内存片段的 mspan,提

供无锁分配的 mcache 等等

 

固定分配器

type fixalloc struct {

    size uintptr

    first unsafe.Pointer

    arg unsafe.Pointer

    list *mlink

    chunk *byte

    nchunk uint32

    inuse uintptr

}

在运行时在初始化 heap 时,一共构建了 4 种固定分配器。

func mHeap_Init(h *mheap, spans_size uintptr) {

    fixAlloc_Init(&h.spanalloc, unsafe.Sizeof(mspan{}), recordspan, ...)

    fixAlloc_Init(&h.cachealloc, unsafe.Sizeof(mcache{}), nil, nil, ...)

    fixAlloc_Init(&h.specialfinalizeralloc, unsafe.Sizeof(specialfinalizer{}), nil, ...)

    fixAlloc_Init(&h.specialprofilealloc, unsafe.Sizeof(specialprofile{}), nil, ...)

}

总结

Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。

 

Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域

arena区域按页划分成一个个小块

span管理一个或多个页

mcentral管理多个span供线程申请使用

mcache作为线程私有资源,资源来源于mcentral

 

 

参考资料:https://my.oschina.net/renhc/blog/2236782?spm=a2c4e.11153940.blogcont652551.13.3e3f24754ybVI2

C语言模拟内存分配管理使用位视图的方法可以通过一个位数组来表示内存的使用情况。假设我们有一个大小为N的内存,我们可以用一个长度为N的位数组来表示每个内存块的占用情况。 1. 初始化位视图:开始时,将位数组中的所有位都设置为0,表示内存中的所有块都是空闲的。 2. 内存分配:当需要分配一块大小为m的内存时,我们可以遍历位数组中的连续m个位,直到找到一个连续的空闲块。将这些位设置为1,并返回内存块的起始地址。如果找不到连续的空闲块,则分配失败。 3. 内存释放:当一块内存不再使用时,我们可以根据内存块的起始地址和大小,将对应的位数组中的位设置为0,表示该内存块已经释放。 4. 内存管理算法:为了提高内存的利用率,可以使用不同的内存分配算法,如首次适应、最佳适应或最坏适应等。这些算法通过遍历位数组来找到适合的内存块进行分配。 使用位视图进行内存分配管理的优点是简单、高效。只需要维护一个位数组,不需要额外的数据结构。同时,也可以快速地判断一个连续的内存块是否空闲。然而,位视图的缺点是不能处理碎片化问题,可能会导致内存的浪费。 总而言之,使用位视图可以模拟内存分配管理,在C语言中实现内存的分配和释放操作。通过维护一个位数组,可以方便地管理内存的使用情况,并选择合适的算法来分配和释放内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值