Golang 内存之mspan、mcache、mcentral 和 mheap 数据结构

Golang中的内存部件组成关系如下图所示

components of memory allocation
golang 内存分配组件

在学习golang 内存时,经常会涉及几个数据结构,如果不熟悉它们的话,理解起来就非常的吃力,所以本篇主要对相关的几个内存组件做下介绍。

在 Golang 中,mcachemcentralmheap 是内存管理的三大组件,mcache 管理线程在本地缓存的 mspanmcentral 管理全局的 mspan 供所有线程。

根据分配对象的大小,内部会使用不同的内存分配机制

  • <16b 会使用微小对象内存分配器,主要使用 mcache.tinyXXX 这类的字段
  • 16-32b 从P下面的 mcache 中分配
  • >32b 直接从 mheap 中分配

对于golang中的内存申请流程,大家应该都非常熟悉了,这里不再进行详细描述。

mcache

在GPM关系中,会在每个 P 下都有一个 mcache 字段,用来表示内存信息。在 GM 时代它被放在了M里,但发现存在太多问题,所以为了避免为每个M都额外创建内存,即使它处于阻塞状态,在GPM 时代将其放在了P下。这样在并发状态下,每个G只有在运行的时候才会使用到内存,而每个G都有自己的P,所以它们并发运行时并不会产生锁。

在P中,一个 mcache 可以用来缓存小对象。还包含本地分配统计信息。由于它在每个P下面都存在一个,所以是无锁的。

Continue reading

Runtime: Golang GC源码分析

在阅读此文前,需要先了解一下三色标记法以及混合写屏障这些概念。

源文件 src/runtime/mgc.go 版本 1.16.2。

基本知识

在介绍GC之前,我们需要认识有些与GC相关的基本信息,如GC的状态、模式、统计信息等。

三种状态

共有三种状态

const (
	_GCoff             = iota // GC not running; sweeping in background, write barrier disabled
	_GCmark                   // GC marking roots and workbufs: allocate black, write barrier ENABLED
	_GCmarktermination        // GC mark termination: allocate black, P's help GC, write barrier ENABLED
)
  • _GCoff GC未运行
  • _GCmark 标记中,启用写屏障
  • _GCmarktermination 标记终止,启用写屏障

三种模式

支持三种模式:

const (
    gcBackgroundMode gcMode = iota // concurrent GC and sweep
    gcForceMode                    // stop-the-world GC now, concurrent sweep
    gcForceBlockMode               // stop-the-world GC now and STW sweep (forced by user)
)
  • gcBackgroundMode 默认模式,标记与清扫过程都是并发执行的
  • gcForceMode 只在清扫阶段支持并发;
  • gcForceBlockMode GC全程需要STW。

针对每种模式,在标记阶段会采用不同的标记策略,详细见  gcBgMarkWorker() 

Continue reading

Golang中的切片与GC

今天再看 timer 源码的时候,在函数 clearDeletedTimers() 里看到一段对切片的处理代码,实现目的就是对一个切片内容进行缩容。

// src/runtime/time.go

// The caller must have locked the timers for pp.
func clearDeletedTimers(pp *p) {
	timers := pp.timers
	......
	// 对无用的切片元素赋值 nil
	for i := to; i < len(timers); i++ {
		timers[i] = nil
	}

	atomic.Xadd(&pp.deletedTimers, -cdel)
	atomic.Xadd(&pp.numTimers, -cdel)
	atomic.Xadd(&pp.adjustTimers, -cearlier)

	timers = timers[:to]
	pp.timers = timers
	updateTimer0When(pp)

	......
}

to 变量指新切片的长度, len(timers)指原来切片的长度。

这里在其进行 timers = timers[:to] 操作前,先是将 to 数组索引后的值进行了赋值 nil。按照我们平常的用法,是没有必要执行这一步的,那为什么这里要加这一步呢?其实这里与GC 有关。

在日常开发中很少注意到这个细节,虽然最终实现的结果是一样的,但如果考虑GC的话,差别可就大多了。

Continue reading