现在我们可以使用break断点到runtime.newobject函数的调用上:
(dlv) break runtime.newobject Breakpoint 2 set at 0x40d426 for runtime.newobject() c:/software/go/src/runtime/malloc.go:1164输入continue跳到断点的位置:
(dlv) continue > runtime.newobject() c:/software/go/src/runtime/malloc.go:1164 (hits goroutine(1):1 total:1) (PC: 0x40d426) Warning: debugging optimized function 1159: } 1160: 1161: // implementation of new builtin 1162: // compiler (both frontend and SSA backend) knows the signature 1163: // of this function =>1164: func newobject(typ *_type) unsafe.Pointer { 1165: return mallocgc(typ.size, typ, true) 1166: } 1167: 1168: //go:linkname reflect_unsafe_New reflect.unsafe_New 1169: func reflect_unsafe_New(typ *_type) unsafe.Pointer {print命令来查看typ的数据:
(dlv) print typ *runtime._type {size: 16, ptrdata: 8, hash: 875453117, tflag: tflagUncommon|tflagExtraStar|tflagNamed (7), align: 8, fieldAlign: 8, kind: 25, equal: runtime.strequal, gcdata: *1, str: 5418, ptrToThis: 37472}可以看到这里打印的size是16bytes,因为我们A结构体里面就一个string类型的field。
进入到mallocgc方法后,通过args和locals命令查看函数的参数和局部变量:
(dlv) args size = (unreadable could not find loclist entry at 0x8b40 for address 0x40ca73) typ = (*runtime._type)(0x4d59a0) needzero = true ~r3 = (unreadable empty OP stack) (dlv) locals (no locals) 各个对象入口我们根据汇编可以判断,所有的函数入口都是runtime.mallocgc,但是下面两个对象需要注意一下:
int64对象runtime.convT64
func convT64(val uint64) (x unsafe.Pointer) { if val < uint64(len(staticuint64s)) { x = unsafe.Pointer(&staticuint64s[val]) } else { x = mallocgc(8, uint64Type, false) *(*uint64)(x) = val } return }这段代码表示如果一个int64类型的值小于256,直接十三姨的是缓存值,那么这个值不会进行内存分配。
string对象runtime.convTstring
func convTstring(val string) (x unsafe.Pointer) { if val == "" { x = unsafe.Pointer(&zeroVal[0]) } else { x = mallocgc(unsafe.Sizeof(val), stringType, true) *(*string)(x) = val } return }由这段代码显示,如果是创建一个为”“的string对象,那么会直接返回一个固定的地址值,不会进行内存分配。
分析 分配器的组件内存分配是由内存分配器完成,分配器由3种组件构成:runtime.mspan、runtime.mcache、runtime.mcentral、runtime.mheap。
runtime.mspan
type mspan struct { // 上一个节点 next *mspan // 下一个节点 prev *mspan // span集合 list *mSpanList // span开始的地址值 startAddr uintptr // span管理的页数 npages uintptr // Object n starts at address n*elemsize + (start << pageShift). // 空闲节点的索引 freeindex uintptr // span中存放的对象数量 nelems uintptr // 用于快速查找内存中未被使用的内存 allocCache uint64 // 用于计算mspan管理了多少内存 elemsize uintptr // span的结束地址值 limit uintptr ... }runtime.mspan是内存管理器里面的最小粒度单元,所有的对象都是被管理在mspan下面。
mspan是一个链表,有上下指针;
npages代表mspan管理的堆页的数量;
freeindex是空闲对象的索引;
nelems代表这个mspan中可以存放多少对象,等于(npages * pageSize)/elemsize;
allocCache用于快速的查找未被使用的内存地址;
elemsize表示一个对象会占用多个个bytes,等于class_to_size[sizeclass],需要注意的是sizeclass每次获取的时候会sizeclass方法,将sizeclass>>1;
limit表示span结束的地址值,等于startAddr+ npages*pageSize;
实例图如下:
图中alloc是一个拥有137个元素的mspan数组,mspan数组管理数个page大小的内存,每个page是8k,page的数量由spanclass规格决定。
runtime.mcache
type mcache struct { ... // 申请小对象的起始地址 tiny uintptr // 从起始地址tiny开始的偏移量 tinyoffset uintptr // tiny对象分配的数量 local_tinyallocs uintptr // number of tiny allocs not counted in other stats // mspan对象集合,numSpanClasses=134 alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass ... }runtime.mcache是绑在并发模型GPM的P上,在分配微对象和小对象的时候会先去runtime.mcache中获取,每一个处理器都会被分配一个线程缓存runtime.mcache,因此从runtime.mcache进行分配时无需加锁。
在runtime.mcache中有一个alloc数组,是runtime.mspan的集合,runtime.mspan是 Go 语言内存管理的基本单元。对于[16B,32KB]的对象会使用这部分span进行内存分配,所以所有在这区间大小的对象都会从alloc这个数组里寻找,下面会分析到。