golang源码解析--内存总览

看了gc,发现没有内存的知识,光看gc,只能背个流程,其中很多涉及内存的操作,所以先来了解一波内存,然后了解gc的协程,golang的锁,再回头看gc

golang内存分配的简介

关于golang的内存分配

原理:
思想来源于Thread-CachingMalloc。核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

内存分配工作通过内存页来进行。
小的内存分配(0~32kb)被分成了70个级别,每一个都有它自己的一组完全相同大小的对象。任何可用内存页都可以拆分为一组对象,然后使用可用位图管理这些对象。

相关的数据结构

fixalloc:固定大小堆外对象的空闲列表分配器,用于管理分配器使用的存储。
mheap:malloc堆,按页(8192字节)粒度管理。
mspan:由mheap管理的一系列页面。
mcentral:收集给定大小类的所有span
mcache: 一个P的可用内存空间
mstats: 分配统计信息

golang如何分配一个小对象

分配一个小对象将沿着缓存的层次结构前进
1.将小对象的大小,四舍五入的(保证class的大小大于对象内存)分配在一个小类中,并在P的mcache中查询对应的mspan,扫描mcache中的mspan的空余位去找可用的slot。如果存在可用slot,分配内存。这个过程的完成不需要锁。
2.如果申请的mspan在mcache中没有可用插槽,请从mcentral的具有可用空间的所需大小类别的mspan列表中获取一个新的mspan。获得整个span会摊销锁定mcentral的成本。
3.如果mcentral的mspan列表为空,请从mheap获取一系列用于mspan的页面。
4.若mheap为空,或者没有足够大的页,从操作系统申请一组新的页(至少1MB),分配大量的页面可以分摊与操作系统对话的成本。

golang如何释放mspan上的内存

扫掠mspan并释放其上的对象将沿着类似的层次结构进行:
1.如果mspan是响应于分配而被回收的,那么它将被返回到mcache以满足分配。
2.非情况1,如果msapn中仍有分配的对象,则它被放在mcentral的mspan的size类的空余列表中
3.非1,2,如果所有的对象在mspan中都是空闲的,则mspan是空闲的,则会返回mheap并不再有size class的属性。这可能会与相邻的空闲mspan合并。
4.如果mspan保持空闲状态足够长的时间,则将其页面返回到操作系统。

golang如何分配和释放一个大对象

分配和释放大对象直接使用mheap,而绕过mcache和mcentral。

延迟归零机制

仅当mspan.needzero为false时,才会将mspan中的可用对象插槽清零。
如果needzero为true,则在分配对象时将其清零。
延迟调零有多种好处:
1.堆栈帧分配可以完全避免归零。
2.由于程序可能即将写入内存,因此它展现了更好的时间局部性。
3.我们不会将永远不会被重用的页面归零。

虚拟内存层

堆由一组arena组成,在64位计算机上是64MB,在32位计算机上是4MB,每个arena的起始地址也与arena大小对齐
每个arena都有一个关联的heapArena对象,该对象存储该arena的元数据:arena中所有单词的堆位图和arena中所有页面的跨度图。 它们本身是堆外分配的。
由于arena是对齐的,因此可以将地址空间视为一系列arena框架。arena映射(mheap_.arenas)从arena帧号映射到* heapArena,对于不由Go堆支持的部分地址空间,映射为nil。 arena图(map)的结构为两层数组,由“ L1”arena图和许多“ L2”arena图组成。 但是,由于arena很大,因此在许多体系结构上,arena map都由一个单独的大型L2 map组成。
arena map覆盖了整个可能的地址空间,从而允许Go堆使用地址空间的任何部分。 分配器尝试使arena保持连续,以便大跨度(以及大对象)可以跨越arena。

golang关于内存分配的基础概念

这一部分内容源自http://www.sohu.com/a/300983903_657921
在这里插入图片描述
arena区域就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成 8KB大小的页,一些页组合起来称为 mspan。
bitmap区域标识 arena区域哪些地址保存了对象,并用 4bit标志位表示对象是否包含指针、 GC标记信息。 bitmap中一个 byte大小的内存对应 arena区域中4个指针大小(指针大小为 8B )的内存,所以 bitmap区域的大小是 512GB/(4x8B)=16GB。
spans区域存放 mspan(也就是一些 arena分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以 spans区域的大小就是 512GB/8KBx8B=512MB。除以8KB是计算 arena区域的页数,而最后乘以8是计算 spans区域所有指针的大小。创建 mspan的时候,按页填充对应的 spans区域,在回收 object时,根据地址很容易就能找到它所属的 mspan。
总结
Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。
Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。
极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。
概念:
mspan:Go中内存管理的基本单元,是由一片连续的 8KB的页组成的大块内存。注意,这里的页和操作系统本身的页并不是一回事,它一般是操作系统页大小的几倍。一句话概括: mspan是一个包含起始地址、 mspan规格、页的数量等内容的双端链表。
内存管理组件

内存分配由内存分配器完成。分配器由3种组件构成: mcache, mcentral, mheap。
mcache

mcache:每个工作线程都会绑定一个mcache,本地缓存可用的 mspan资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源。
mcentral:为所有 mcache提供切分好的 mspan资源。每个 central保存一种特定大小的全局 mspan列表,包括已分配出去的和未分配出去的。 每个 mcentral对应一种 mspan,而 mspan的种类导致它分割的 object大小不同。当工作线程的 mcache中没有合适(也就是特定大小的)的 mspan时就会从 mcentral获取。
mheap:代表Go程序持有的所有堆空间,Go程序使用一个 mheap的全局对象 _mheap来管理堆内存。
当 mcentral没有空闲的 mspan时,会向 mheap申请。而 mheap没有资源时,会向操作系统申请新内存。 mheap主要用于大对象的内存分配,以及管理未切割的 mspan,用于给 mcentral切割成小对象。
同时我们也看到, mheap中含有所有规格的 mcentral,所以,当一个 mcache从 mcentral申请 mspan时,只需要在独立的 mcentral中使用锁,并不会影响申请其他规格的 mspan。

源码分析

golang内存分配的过程

mallocgc

//分配一个大小为字节的对象。
//从per-P缓存的空闲列表中分配小对象。
//从堆直接分配大对象(> 32 kB)。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
	//目前gc还处于stop the world阶段,禁止申请
	if gcphase == _GCmarktermination {
		throw("mallocgc called with gcphase == _GCmarktermination")
	}
	//分配空指针
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
	//如果debug.sbrk != 0则不会释放内存 
	if debug.sbrk != 0 {
		align := uintptr(16)
		if typ != nil {
			align = uintptr(typ.align)
		}
		//分配持久内存
		return persistentalloc(size, align, &memstats.other_sys)
	}

	// assistG 是这次分配内存的G,如果GC当前非活跃,则为空
	var assistG *g
	if gcBlackenEnabled != 0 {
	    // gc处于标记状态
		// 从当前G分撇内存
		assistG = getg()
		if assistG.m.curg != nil {
			assistG = assistG.m.curg
		}
		// 从当前g获取内存
		assistG.gcAssistBytes -= int64(size)

		if assistG.gcAssistBytes < 0 {
			//这个G没有足够内存。 协助GC进行更正,然后再分配。 这必须在禁用抢占之前发生。
			gcAssistAlloc(assistG)
		}
	}

	// Set mp.mallocing to keep from being preempted by GC.
	// 抢占锁防止GC使用
	mp := acquirem()
	if mp.mallocing != 0 {
		throw("malloc deadlock")
	}
	if mp.gsignal == getg() {
		throw("malloc during signal")
	}
	mp.mallocing = 1

	shouldhelpgc := false
	dataSize := size
	c := gomcache()
	var x unsafe.Pointer
	noscan := typ == nil || typ.kind&kindNoPointers != 0
	if size <= maxSmallSize {
		if noscan && size < maxTinySize {
	     //针对微小分配器的分配
	     //微小的内存分配将多个微小内存分配的请求分配到一个内存块上。当所有的子对象都不可达,释放该内存块。
		//所有子对象不能有指针,用于限制可能浪费的内存

        //用于合并的存储块的大小是可调的
		//当前设置是16bytes,这个的标准是与最坏情况下浪费内存的两倍有关(当仅有一个子对象可达,其余对象都不可达)
		//8bytes将没有内存浪费,但是对于组合请求的概率将大大减少
		//32bytes会让更多的请求结合在一个内存块,但是会造成4倍的内存浪费
		//无论块大小多少,最好的案例都是8的倍数

       //从微小分配器分配的内容都不能被显示释放,所以我们要确保当释放内存的时候它的大小大于maxTinySize(默认16bytes)
      //SetFinalizer针对由微小分配器分配的对象有特殊处理方式,
	  //可以为内存块的内部字节设置终结器  
      // 微小分配器主要针对小字符串和独立的转义变量
	// 在json基础上,分配器将内存消耗的数量减少了12%,并将hepsize减少了20%
            // 对其微小指针 
			if size&7 == 0 {
				off = round(off, 8)
			} else if size&3 == 0 {
				off = round(off, 4)
			} else if size&1 == 0 {
				off = round(off, 2)
			}
			if off+size <= maxTinySize && c.tiny != 0 {
				// The object fits into existing tiny block.
				//已有的微小块有适合对象的存在,则直接分配完毕
				x = unsafe.Pointer(c.tiny + off)
				c.tinyoffset = off + size
				c.local_tinyallocs++
				mp.mallocing = 0
				releasem(mp)
				return x
			}
			// 分配一个新的maxTinySize内存块
			span := c.alloc[tinySpanClass]
			v := nextFreeFast(span)
			if v == 0 {
				v, _, shouldhelpgc = c.nextFree(tinySpanClass)
			}
			x = unsafe.Pointer(v)
			(*[2]uint64)(x)[0] = 0
			(*[2]uint64)(x)[1] = 0
			// See if we need to replace the existing tiny block with the new one
			// based on amount of remaining free space.
			if size < c.tinyoffset || c.tiny == 0 {
				c.tiny = uintptr(x)
				c.tinyoffset = size
			}
			size = maxTinySize
		} else {
		     //非微小对象的情况,内存消耗大于16字节
			//先确定对象属于的类,直接申请
			var sizeclass uint8
			if size <= smallSizeMax-8 {
				sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
			} else {
				sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]
			}
			size = uintptr(class_to_size[sizeclass])
			spc := makeSpanClass(sizeclass, noscan)
			span := c.alloc[spc]
			v := nextFreeFast(span)
			if v == 0 {
				v, span, shouldhelpgc = c.nextFree(spc)
			}
			x = unsafe.Pointer(v)
			if needzero && span.needzero != 0 {
				memclrNoHeapPointers(unsafe.Pointer(v), size)
			}
		}
	} else {
	//申请大对象,大于32kb
		var s *mspan
		shouldhelpgc = true
		systemstack(func() {
			s = largeAlloc(size, needzero, noscan)
		})
		s.freeindex = 1
		s.allocCount = 1
		x = unsafe.Pointer(s.base())
		size = s.elemsize
	}

	var scanSize uintptr
	if !noscan {
	     //若申请的是defer+arg的块,我们已经针对此获取了足够大的内存,将"asked for"的大小
		//削减至defer头的大小,为的是GC bitmap可以将arg块记录为什么也没有(如同将其记录为一个内存块由于size对齐而未使用的部分)
		//defer arg部分将被当做scanstak的一部分被回收
		if typ == deferType {
			dataSize = unsafe.Sizeof(_defer{})
		}
		heapBitsSetType(uintptr(x), size, dataSize, typ)
		if dataSize > typ.size {
			// Array allocation. If there are any
			// pointers, GC has to scan to the last
			// element.
			if typ.ptrdata != 0 {
				scanSize = dataSize - typ.size + typ.ptrdata
			}
		} else {
			scanSize = typ.ptrdata
		}
		c.local_scan += scanSize
	}

	//将申请的内存在被垃圾收集器可观察前初始化为类型安全的内存,并设置堆位的存储
	//目的是防止垃圾收集器跟随指向x的指针看到未初始化的或陈旧的堆栈
	publicationBarrier()

	//GC期间,新申请内存为黑色
	//所有slot为空,则不需要清扫
	//这可能与gc产生竞争,所以自动标记该位若可能存在竞争
	if gcphase != _GCoff {
		gcmarknewobject(uintptr(x), size, scanSize)
	}

	if raceenabled {
		racemalloc(x, size)
	}

	if msanenabled {
		msanmalloc(x, size)
	}

	mp.mallocing = 0
	releasem(mp)

	if debug.allocfreetrace != 0 {
		tracealloc(x, size, typ)
	}

	if rate := MemProfileRate; rate > 0 {
		if rate != 1 && int32(size) < c.next_sample {
			c.next_sample -= int32(size)
		} else {
			mp := acquirem()
			profilealloc(mp, x, size)
			releasem(mp)
		}
	}

	if assistG != nil {
		// Account for internal fragmentation in the assist
		// debt now that we know it.
		assistG.gcAssistBytes -= int64(size - dataSize)
	}
   //若申请大内存,则判断是否需要进行内存回收也就是gc操作
	if shouldhelpgc {
		if t := (gcTrigger{kind: gcTriggerHeap}); t.test() {
			gcStart(t)
		}
	}

	return x
}
发布了212 篇原创文章 · 获赞 33 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/hello_bravo_/article/details/103817330
今日推荐