go语言取内存 go语言内存不断升高

(十一)golang 内存分析编写过C语言程序的肯定知道通过malloc()方法动态申请内存,其中内存分配器使用的是glibc提供的ptmalloc2 。除go语言取内存了glibc,业界比较出名的内存分配器有Google的tcmalloc和Facebook的jemalloc 。二者在避免内存碎片和性能上均比glic有比较大的优势,在多线程环境中效果更明显 。
Golang中也实现了内存分配器,原理与tcmalloc类似 , 简单的说就是维护一块大的全局内存,每个线程(Golang中为P)维护一块小的私有内存 , 私有内存不足再从全局申请 。另外,内存分配与GC(垃圾回收)关系密切,所以了解GC前有必要了解内存分配的原理 。
为了方便自主管理内存,做法便是先向系统申请一块内存,然后将内存切割成小块,通过一定的内存分配算法管理内存 。以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示go语言取内存:
预申请的内存划分为spans、bitmap、arena三部分 。其中arena即为所谓的堆区 , 应用中需要的内存从这里分配 。其中spans和bitmap是为了管理arena区而存在的 。
arena的大小为512G,为了方便管理把arena区域划分成一个个的page,每个page为8KB,一共有512GB/8KB个页;
spans区域存放span的指针,每个指针对应一个page,所以span区域的大小为(512GB/8KB)乘以指针大小8byte = 512M
bitmap区域大小也是通过arena计算出来,不过主要用于GC 。
span是用于管理arena页的关键数据结构,每个span中包含1个或多个连续页,为了满足小对象分配,span中的一页会划分更小的粒度,而对于大对象比如超过页大?。蛲ü嘁呈迪?。
根据对象大小,划分了一系列class,每个class都代表一个固定大小的对象,以及每个span的大小 。如下表所示:
上表中每列含义如下:
class: class ID,每个span结构中都有一个class ID, 表示该span可处理的对象类型
bytes/obj:该class代表对象的字节数
bytes/span:每个span占用堆的字节数,也即页数乘以页大小
objects: 每个span可分配的对象个数,也即(bytes/spans)/(bytes/obj)waste
bytes: 每个span产生的内存碎片,也即(bytes/spans)%(bytes/obj)上表可见最大的对象是32K大?。?超过32K大小的由特殊的class表示 , 该class ID为0 , 每个class只包含一个对象 。
span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大?。瑂pan将一个或多个页拆分成多个块进行管理 。src/runtime/mheap.go:mspan定义了其数据结构:
以class 10为例,span和管理的内存如下图所示:
spanclass为10,参照class表可得出npages=1,nelems=56,elemsize为144 。其中startAddr是在span初始化时就指定了某个页的地址 。allocBits指向一个位图,每位代表一个块是否被分配,本例中有两个块已经被分配 , 其allocCount也为2 。next和prev用于将多个span链接起来,这有利于管理多个span,接下来会进行说明 。
有了管理内存的基本单位span,还要有个数据结构来管理span,这个数据结构叫mcentral,各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断的加锁,Golang为每个线程分配了span的缓存,这个缓存即是cache 。src/runtime/mcache.go:mcache定义了cache的数据结构
alloc为mspan的指针数组,数组大小为class总数的2倍 。数组中每个元素代表了一种class类型的span列表,每种class类型都有两组span列表,第一组列表中所表示的对象中包含了指针,第二组列表中所表示的对象不含有指针,这么做是为了提高GC扫描性能,对于不包含指针的span列表,没必要去扫描 。根据对象是否包含指针,将对象分为noscan和scan两类,其中noscan代表没有指针,而scan则代表有指针,需要GC进行扫描 。mcache和span的对应关系如下图所示:
mchache在初始化时是没有任何span的,在使用过程中会动态的从central中获取并缓存下来,跟据使用情况,每种class的span个数也不相同 。上图所示,class 0的span数比class1的要多,说明本线程中分配的小对象要多一些 。
cache作为线程的私有资源为单个线程服务,而central则是全局资源,为多个线程服务 , 当某个线程内存不足时会向central申请,当某个线程释放内存时又会回收进central 。src/runtime/mcentral.go:mcentral定义了central数据结构:
lock: 线程间互斥锁,防止多线程读写冲突
spanclass : 每个mcentral管理着一组有相同class的span列表
nonempty: 指还有内存可用的span列表
empty: 指没有内存可用的span列表
nmalloc: 指累计分配的对象个数线程从central获取span步骤如下:
将span归还步骤如下:
从mcentral数据结构可见,每个mcentral对象只管理特定的class规格的span 。事实上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap数据结构中 。src/runtime/mheap.go:mheap定义了heap的数据结构:
lock: 互斥锁
spans: 指向spans区域,用于映射span和page的关系
bitmap:bitmap的起始地址
arena_start: arena区域首地址
arena_used: 当前arena已使用区域的最大地址
central: 每种class对应的两个mcentral
从数据结构可见,mheap管理着全部的内存,事实上Golang就是通过一个mheap类型的全局变量进行内存管理的 。mheap内存管理示意图如下:
系统预分配的内存分为spans、bitmap、arean三个区域,通过mheap管理起来 。接下来看内存分配过程 。
针对待分配对象的大小不同有不同的分配逻辑:
(0, 16B) 且不包含指针的对象: Tiny分配
(0, 16B) 包含指针的对象:正常分配
[16B, 32KB] : 正常分配
(32KB, -) : 大对象分配其中Tiny分配和大对象分配都属于内存管理的优化范畴 , 这里暂时仅关注一般的分配方法 。
以申请size为n的内存为例,分配步骤如下:
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节 。1、Golang程序启动时申请一大块内存并划分成spans、bitmap、arena区域
2、arena区域按页划分成一个个小块 。
3、span管理一个或多个页 。
4、mcentral管理多个span供线程申请使用
5、mcache作为线程私有资源,资源来源于mcentral 。
golang-指针类型 tips: *号,可以指向指针类型内存地址上的值,号,可以获取值类型的内存地址
每一个变量都有内存地址,可以通过变量来操作内存地址中的值,即内存的大小
go语言中获取变量的内存地址方法:通过符号可以获取变量的地址
定义:普通变量存储的是对应类型的值 , 这些类型就叫值类型
变量b,在内存中的地址为:0x1040a124,在这个内存地址上存储的值为:156
定义:指针类型的变量存储的是?个地址,所以?叫指针类型或引?类型
b 是值类型 , 它指向的是内存地址上的值
a是指针类型,它指向的是b的内存地址
指针类型定义,语法: var 变量名 *类型
指针类型在定义完成后,默认为空地址,即空指针(nil)
在定义好指针变量后,可以通过***** 符号可以获取指针变量指向的变量
在这里的 *a 等价于 b,通过修改 *a ,最终修改的是值类型b的值
这里a,d是值类型,b,c是指针类型
d就相当于把a内存地址上值,在内存中从新开辟了一块空间存储,d和a互不影响
b,c相当于指向了a的内存地址 , 当使用*号引用出内存地址上的变量上,修改值得,a的值也会跟着改变
Golang 怎么得到 CPU 的使用率和可用内存第一步 , 创建性能监视器对象:
PerformanceCounter _oPerformanceCounter=new PerformanceCounter("Processor","% Processor Time","_Total");
第二步,获取CPU使用情况:
float _nVal=_oPerformanceCounter.NextValue();
_nVal中就是当前CPU的使用率了,加上百分号(%)就是使用率的百分比,比如:
string _s="当前CPU使用率:"nVal.ToString("0.0")"%";
Process [] pro;
pro = Process.GetProcesses();
int total=0;
Process temp;
int i;
for(i=0;ipro.Length ;i)
{
temp =pro[i];
total=temp.PrivateMemorySizetotal ;
}
获得内存的占用大小
Go 如何查看一个变量的内存地址 理解指针问题 熟悉C语言的同学都知道 , 查看一个变量的地址在处理指针的相关问题的时候直观重要,在C中直接取地址符 即可 。那么在Go语言中如何查看一个变量的地址,我们使用unsafe.Pointer() 函数来查看一个变量的内存地址 。
举例:
type Vertex struct {
X, Y float64
}
func (v Vertex) sqrt() float64 {
return math.Sqrt(v.X * v.Xv.Y * v.Y)
}
func (vVertex) scale(f float64) { //带 号 和不带*号的区别 可以从内存地址来看出
fmt.printf("=======", unsafe.Pointer(v))//v 本身就是指针 存储的就是地址 不用取地址
v.X = x.X * f
v.Y = v.Y * f
}
func main() {
v := Vertex{3, 4}
fmt.printf("=======", unsafe.Pointer(v))
v.scale(10)
fmt.Println(v.sqrt())
}
//带 号 打印的结果 ====== -%!(EXTRA unsafe.Pointer=0xc00006e070)======%!(EXTRA unsafe.Pointer=0xc00006e070) 相同
//不带 号 打印的结果======%!(EXTRA unsafe.Pointer=0xc000094060)======%!(EXTRA unsafe.Pointer=0xc000094090) 不同
去掉*号 在scale()方法中要对 v 进行取地址操作
Golang 1.14中内存分配、清扫和内存回收 Golang的内存分配是由golang runtime完成,其内存分配方案借鉴自tcmalloc 。
主要特点就是
本文中的element指一定大小的内存块是内存分配的概念,并为出现在golang runtime源码中
本文讲述x8664架构下的内存分配
Golang 内存分配有下面几个主要结构
Tiny对象是指内存尺寸小于16B的对象,这类对象的分配使用mcache的tiny区域进行分配 。当tiny区域空间耗尽时刻 , 它会从mcache.alloc[tinySpanClass]指向的mspan中找到空闲的区域 。当然如果mcache中span空间也耗尽 , 它会触发从mcentral补充mspan到mcache的流程 。
小对象是指对象尺寸在(16B,32KB]之间的对象,这类对象的分配原则是:
1、首先根据对象尺寸将对象归为某个SpanClass上,这个SpanClass上所有的element都是一个统一的尺寸 。
2、从mcache.alloc[SpanClass]找到mspan , 看看有无空闲的element,如果有分配成功 。如果没有继续 。
3、从mcentral.allocSpan[SpanClass]的nonempty和emtpy中找到合适的mspan,返回给mcache 。如果没有找到就进入mcentral.grow()—mheap.alloc()分配新的mspan给mcentral 。
大对象指尺寸超出32KB的对象,此时直接从mheap中分配,不会走mcache和mcentral , 直接走mheap.alloc()分配一个SpanClass==0 的mspan表示这部分分配空间 。
对于程序分配常用的tiny和小对象的分配,可以通过无锁的mcache提升分配性能 。mcache不足时刻会拿mcentral的锁,然后从mcentral中充mspan 给mcache 。大对象直接从mheap 中分配 。
在x8664环境上,golang管理的有效的程序虚拟地址空间实质上只有48位 。在mheap中有一个pages pageAlloc成员用于管理golang堆内存的地址空间 。golang从os中申请地址空间给自己管理,地址空间申请下来以后,golang会将地址空间根据实际使用情况标记为free或者alloc 。如果地址空间被分配给mspan或大对象后,那么被标记为alloc , 反之就是free 。
Golang认为地址空间有以下4种状态:
Golang同时定义了下面几个地址空间操作函数:
在mheap结构中,有一个名为pages成员,它用于golang 堆使用虚拟地址空间进行管理 。其类型为pageAlloc
pageAlloc 结构表示的golang 堆的所有地址空间 。其中最重要的成员有两个:
在golang的gc流程中会将未使用的对象标记为未使用,但是这些对象所使用的地址空间并未交还给os 。地址空间的申请和释放都是以golang的page为单位(实际以chunk为单位)进行的 。sweep的最终结果只是将某个地址空间标记可被分配,并未真正释放地址空间给os,真正释放是后文的scavenge过程 。
在gc mark结束以后会使用sweep()去尝试free一个span;在mheap.alloc 申请mspan时刻,也使用sweep去清扫一下 。
清扫mspan主要涉及到下面函数
如上节所述 , sweep只是将page标记为可分配,但是并未把地址空间释放;真正的地址空间释放是scavenge过程 。
真正的scavenge是由pageAlloc.scavenge()—sysUnused()将扫描到待释放的chunk所表示的地址空间释放掉(使用sysUnused()将地址空间还给os)
golang的scavenge过程有两种:
golang内存扩容一般来说当内存空间span不足时 , 需要进行扩容 。而在扩容前需要将当前没有剩余空间的内存块相关状态解除,以便后续的垃圾回收期能够进行扫描和回收,接着在从中间部件(central)提取新的内存块放回数组中 。
需要注意由于中间部件有scan和noscan两种类型,则申请的内存空间最终获取的可能是其两倍,并由heap堆进行统一管理 。中间部件central是通过两个链表来管理其分配的所有内存块:
1、empty代表“无法使用”状态,没有剩余的空间或被移交给缓存的内存块
2、noempty代表剩余的空间,并这些内存块能够提供服务
由于golang垃圾回收器使用的累增计数器(heap.sweepgen)来表达代龄的:
从上面内容可以看到每次进行清理操作时该计数器2
再来看下mcentral的构成
当通过mcentral进行空间span获取时,第一步需要到noempty列表检查剩余空间的内存块,这里面有一点需要说明主要是垃圾回收器的扫描过程和清理过程是同时进行的,那么为go语言取内存了获取更多的可用空间,则会在将分配的内存块移交给cache部件前,先完成清理的操作 。第二步当noempty没有返回时,则需要检查下empty列表(由于empty里的内存块有可能已被标记为垃圾,这样可以直接清理,对应的空间则可直接使用了) 。第三步若是noempty和empty都没有申请到 , 这时需要堆进行申请内存的
通过上面的源码也可以看到中间部件central自身扩容操作与大对象内存分配差不多类似 。
在golang中将长度小于16bytes的对象称为微小对象(tiny),最常见的就是小字符串,一般会将这些微小对象组合起来,并用单块内存存储,这样能够有效的减少内存浪费 。
当微小对象需要分配空间span,首先缓存部件会按指定的规格(tiny size class)取出一块内存,若容量不足 , 则重新提取一块go语言取内存;前面也提到会将微小对象进行组合 , 而这些组合的微小对象是不能包含指针的,因为垃圾回收的原因 , 一般都是当前存储单元里所有的微小对象都不可达时 , 才会将该块内存进行回收 。
而当从缓冲部件cache中获取空间span时,是通过偏移位置(tinyoffset)先来判断剩余空间是否满足需求 。若是可以的话则以此计算并返回内存地址;若是空间不足,则提取新的内存块,直接返回起始地址便可; 最后在对比新旧两块内存,空间大的那块则会被保留 。
【go语言取内存 go语言内存不断升高】go语言取内存的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于go语言内存不断升高、go语言取内存的信息别忘了在本站进行查找喔 。

    推荐阅读