后端实践选题方向三 | 豆包MarsCode AI 刷题

91 阅读5分钟

方向三:实践记录以及工具使用

Go语言内存分配和垃圾回收机制

Go语言的内存分配器借鉴了线程缓存分配的设计,实现了多级高速内存分配机制,其最大的特点就是多级缓存和按类分配。

1.多级缓存

多级缓存是指多级内存分配组件协同工作,共同管理内存。mcache是处理器(GMP机制中的Processor)专属的内存缓存组件,对每一个线程处理微对象和小对象的分配,它们会持有最小内存管理单元mspan。当mcache不存在可用的mspan时,会从中心缓存mcentral中获取新的内存管理单元。 mheap则是全局的内存管理中心,它会从操作系统申请内存,存储到heapArena中,然后生成对应的内存管理单元供mcentral和mcache管理。

2. 按类分配

Go语言内存分配机制的另一大特点就是按类分配,将对象根据大小划分为微对象,小对象和大对象。按照分类后的类别实施不同的分配策略。针对微对象和小对象,优先请求mcache进行内存分配,当mcache没有足够的内存空间时,会根据对象大小请求不同的mcentral来处理;针对大对象,则会直接请求mheap分配对应的内存。不同大小的对象请求不同的mcentral进行处理,虽然需要加锁,但是也分散了锁粒度,提高了内存分配效率。

Go语言内存空间状态机

Go语言内存空间状态机详解 Go语言的内存管理以高效和并发友好著称,其中的内存分配与状态管理依赖一套状态机机制,确保内存分配和释放的准确性和性能。以下将通过代码示例和详细分析,讲解Go内存空间状态机的工作原理和实现细节。

一、内存空间状态的基本概念

Go的内存空间主要涉及以下几种状态: 1. Free(空闲状态) :尚未使用的内存空间。 2. In-use(使用状态) :当前已分配并被应用程序使用的内存。 3. Scavenged(回收状态) :已经被垃圾回收标记为空闲,并将其内容归零的内存。 4. Allocated(已分配状态) :当前正在使用的内存块,通常由 mspan 或 mcache 管理。 这些状态通过状态机转移实现动态管理。状态转移的逻辑主要体现在: • 空闲内存如何转为使用状态。 • 使用内存如何归还到空闲状态。 • 回收后的内存如何被清零以避免数据泄漏。

二、核心组件和结构体

Go的内存管理中核心数据结构如下: 1. mheap mheap 是全局堆,负责从操作系统请求大块内存并管理其分配。大块内存以 arena 为单位管理。 2. mspan mspan 是内存管理的基本单元,用于分配特定大小类别的对象。每个 mspan 的状态由 mspan.state 表示,状态包括: • mSpanInUse: 使用中。 • mSpanFree: 空闲中。 • mSpanScavenged: 已回收并归零。 3. mcache mcache 是每个处理器(P)的线程本地缓存,优先用于分配微对象和小对象的内存,减少锁竞争。

三、内存状态机的转移 以下代码演示了内存状态如何通过分配、回收和清零实现状态转移: 示例代码:内存状态管理的伪实现

package main

import (
	"fmt"
	"sync"
)

type SpanState int

const (
	SpanFree SpanState = iota       // 空闲状态
	SpanInUse                      // 使用状态
	SpanScavenged                  // 回收状态
)

// mspan 结构体模拟
type mspan struct {
	state SpanState // 当前状态
	size  int       // 内存块大小
}

// 模拟 mheap 管理的 spans
var mheap struct {
	spans []*mspan
	lock  sync.Mutex
}

// 分配内存函数
func allocate(size int) *mspan {
	mheap.lock.Lock()
	defer mheap.lock.Unlock()

	// 查找空闲内存块
	for _, span := range mheap.spans {
		if span.state == SpanFree && span.size >= size {
			span.state = SpanInUse
			return span
		}
	}

	// 如果没有空闲内存块,创建新块
	newSpan := &mspan{
		state: SpanInUse,
		size:  size,
	}
	mheap.spans = append(mheap.spans, newSpan)
	return newSpan
}

// 回收内存函数
func free(span *mspan) {
	mheap.lock.Lock()
	defer mheap.lock.Unlock()

	if span.state != SpanInUse {
		panic("invalid free operation")
	}
	span.state = SpanScavenged
}

// 清零函数(模拟 scavenging)
func scavenge(span *mspan) {
	mheap.lock.Lock()
	defer mheap.lock.Unlock()

	if span.state != SpanScavenged {
		panic("only scavenged spans can be scavenged")
	}
	span.state = SpanFree
	fmt.Printf("Memory of size %d cleared and returned to free state.\n", span.size)
}

func main() {
	// 初始化 mheap
	mheap.spans = []*mspan{}

	// 分配内存
	span1 := allocate(64)
	fmt.Printf("Allocated span of size %d. State: %d\n", span1.size, span1.state)

	// 回收内存
	free(span1)
	fmt.Printf("Freed span of size %d. State: %d\n", span1.size, span1.state)

	// 清零并回归空闲
	scavenge(span1)
	fmt.Printf("Span state after scavenging: %d\n", span1.state)
}

运行结果为:

image.png

四、状态机的工作过程 以下是内存状态的转移流程: 1. 分配阶段(Free -> In-use) • 当程序需要分配内存时,内存管理器优先从空闲状态的 mspan 中寻找合适大小的块。 • 如果没有足够的空闲块,内存管理器会向操作系统请求更多内存。 2. 释放阶段(In-use -> Scavenged) • 当对象不再被引用时,垃圾回收器将标记其对应的 mspan 为已回收状态。 • 该阶段的数据仍存在,但无法被应用程序访问。 3. 清零阶段(Scavenged -> Free) • 在清零阶段,垃圾回收器将已回收的 mspan 内容清零并标记为空闲状态。 • 这一步防止敏感数据泄漏,同时优化内存重用。

五、优化与注意事项 1. 提高分配效率 • mcache 的本地缓存减少了全局锁竞争,提升了微对象和小对象的分配速度。 • 按大小类别分配 mspan,避免碎片化。 2. 控制清零频率 • 清零操作可能增加 CPU 使用率,需平衡内存安全性与性能。 • Go 的垃圾回收器动态调整清零频率以满足运行时需求。 3. 避免大对象频繁分配 • 大对象直接由 mheap 分配,频繁分配和回收可能导致锁竞争。

个人总结与体会 通过状态机机制,Go 实现了高效的内存分配和管理流程,既满足了并发性能需求,又提供了内存安全保障。通过了解这些底层原理,开发者可以更好地优化 Go 应用程序的性能,例如减少短生命周期对象的分配,利用 sync.Pool 实现对象重用等。