佰汇康网站建设,汕头站扩建效果图,平面设计短期培训班,百度账号购买网站Go语言成为高生产力语言的原因之一自己管理内存#xff1a;Go抛弃了C/C中的开发者管理内存的方式#xff0c;实现了主动申请与主动释放管理#xff0c;增加了逃逸分析和GC#xff0c;将开发者从内存管理中释放出来#xff0c;让开发者有更多的精力去关注软件设计#xff… Go语言成为高生产力语言的原因之一自己管理内存Go抛弃了C/C中的开发者管理内存的方式实现了主动申请与主动释放管理增加了逃逸分析和GC将开发者从内存管理中释放出来让开发者有更多的精力去关注软件设计而不是底层的内存问题。 我们无须精通复杂的内存管理但掌握内存的管理可以让你写出更高质量的代码和高效快速还定位问题这是要求具备本质思维从问题表象逐层深入本质的过程。 首先声明这篇原文并非完全原创是对网上相关文章进行思考和总结 https://zhuanlan.zhihu.com/p/76802887 http://goog-perftools.sourceforge.net/doc/tcmalloc.html 学习的本质是知识搬迁通过不断的探索、实践和思考总结来增强认知能力。 一、内存和内存管理 1、内存计算机的存储结构
计算机系统中有几类存储设备cache、内存、外存。从上至下的访问速度越来越慢。 2、虚拟内存和物理内存
具体可以了解《Linux内存管理》https://guisu.blog.csdn.net/article/details/6152921
1物理内存 真实存在的插在主板内存槽上的内存条的容量的大小. 物理内存是由若干个存储单元组成的每个存储单元有一个编号这种编号可唯一标识一个存储单元称为内存地址或物理地址。我们可以把内存看成一个从0字节一直到内存最大容量逐字节编号的存储单元数组即每个存储单元与内存地址的编号相对应。2 虚拟内存(Virtual memory)(也叫虚拟存储器) 虚拟内存地址就是每个进程可以直接寻址的地址空间不受其他进程干扰。每个指令或数据单元都在这个虚拟空间中拥有确定的地址。 虚拟内存就是进程中的目标代码数据等虚拟地址组成的虚拟空间 虚拟内存不考虑物理内存的大小和信息存放的实际位置只规定进程中相互关联信息的相对位置。每个进程都拥有自己的虚拟内存且虚拟内存的大小由处理机的地址结构和寻址方式决定。如直接寻址如果cpu的有效地址长度为16位则其寻址范围0 -64k。32位机器可以直接寻址4G空间意思是每个应用程序都有4G内存空间可用。
虚拟内存一般分为以下4大块1栈空间特点是内存地址连续先进后出里面放了局部变量、函数形参、自动变量。编译器自动分配和释放进行管理。2堆空间特点是内存地址是不连续一般是链表结构先进后出用户自己管理申请malloc分配calloc释放realloc 。3数据段数据段里面又分三块 第一块是bss保存未初始化的全局变量 第二块是rodata保存了常量 第三块 是.data静态数据区保存了初始化的全局变量还有static修饰的变量。4代码段存放了源代码。
一个可执行程序在存储(没有调入内存时主要分为代码段数据段未初始化数据段三部分。
可执行程序在运行时又多出了两个区域栈段Stack和堆段(Heap)。 3、内存管理
操作系统有内存管理、linux有内存管理、jvm也有内存管理等GO也有内存管理。
操作系统内存管理主要包括物理内存管理和虚拟内存管理具体可以了解《操作系统内存管理》https://guisu.blog.csdn.net/article/details/5713164。
程序中的数据和变量都会被分配到程序所在的虚拟内存中内存空间包含两个重要区域栈区Stack和堆区Heap。函数调用的参数、返回值以及局部变量大都会被分配到栈上这部分内存会由编译器进行管理
当我们说应用程序内存管理的时候主要是指堆内存的管理因为栈的内存管理不需要程序去操心。 不同编程语言使用不同的方法管理堆区的内存C 等编程语言会由工程师主动申请和释放内存Go 以及 Java 等编程语言会由工程师和编译器共同管理堆中的对象由内存分配器分配并由垃圾收集器回收。
Java的jvm内存管理运行JVM虚机通过参数配置jvm内存大小系统将分配给它一块内存区域运行数据区这一内存区域由JVM自己来管理。JVM内存可以划分为5大块Java栈、程序计数寄存器PC寄存器、本地方法栈Native Method Stack、Java堆、方法区。
GO内存管理GO应用程序的内存一般也会分成堆区和栈区程序在运行期间可以主动从堆区申请内存空间这些内存由内存分配器分配并由垃圾收集器负责回收。
二、堆内存如何分配 在一个最简单的内存管理中堆内存最初会是一个完整的大块即未分配任何内存。
内存申请当发现内存申请的时候堆内存就会从未分配内存分割出一个小内存块(block)然后用链表把所有内存块连接起来。 内存释放释放内存实质是把使用的内存块从链表中取出来然后标记为未使用当分配内存块的时候可以从未使用内存块中优先查找大小相近的内存块如果找不到再从未分配的内存中分配内存。
要掌握内存的分配过程先了解有哪些内存分配器很分配方法
编程语言的内存分配器一般包含两种分配方法一种是线性分配器Sequential AllocatorBump Allocator另一种是空闲链表分配器Free-List Allocator这两种分配方法有着不同的实现机制和特性。
1、线性分配器
线性分配Bump Allocator是一种高效的内存分配方法但是有较大的局限性。当我们使用线性分配器时只需要在内存中维护一个指向内存特定位置的指针如果用户程序向分配器申请内存分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置即移动下图中的指针 虽然线性分配器实现为它带来了较快的执行速度以及较低的实现复杂度但是线性分配器无法在内存被释放时重用内存。如下图所示如果已经分配的内存被回收线性分配器无法重新利用红色的内存 因为线性分配器具有上述特性所以需要与合适的垃圾回收算法配合使用例如标记压缩Mark-Compact、复制回收Copying GC和分代回收Generational GC等算法它们可以通过拷贝的方式整理存活对象的碎片将空闲内存定期合并这样就能利用线性分配器的效率提升内存分配器的性能了。
因为线性分配器需要与具有拷贝特性的垃圾回收算法配合所以 C 和 C 等需要直接对外暴露指针的语言就无法使用该策略。
2、空闲链表分配器 空闲链表分配器Free-List Allocator可以重用已经被释放的内存它在内部会维护一个类似链表的数据结构。当用户程序申请内存时空闲链表分配器会依次遍历空闲的内存块找到足够大的内存然后申请新的资源并修改链表
因为不同的内存块通过指针构成了链表所以使用这种方式的分配器可以重新利用回收的资源但是因为分配内存时需要遍历链表所以它的时间复杂度是 ()。 空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择最常见的是以下四种
首次适应First-Fit— 从链表头开始遍历选择第一个大小大于申请内存的内存块循环首次适应Next-Fit— 从上次遍历的结束位置开始遍历选择第一个大小大于申请内存的内存块最优适应Best-Fit— 从链表头遍历整个链表选择最合适的内存块隔离适应Segregated-Fit— 将内存分割成多个链表每个链表中的内存块大小相同申请内存时先找到满足条件的链表再从链表中选择合适的内存块
Go 语言使用的内存分配策略与第四种策略有些相似我们通过下图了解该策略的原理
如上图所示该策略会将内存分割成由 4、8、16、32 字节的内存块组成的链表当我们向内存分配器申请 8 字节的内存时它会在上图中找到满足条件的空闲内存块并返回。隔离适应的分配策略减少了需要遍历的内存块数量提高了内存分配的效率。
三、分级分配TCMalloc 1、TCMalloc分配器概述 TCMalloc线程缓存分配Thread-Caching Malloc就是一个内存分配器管理堆内存主要影响malloc和free用于降低频繁分配、释放内存造成的性能损耗并且有效地控制内存碎片。 在Linux操作系统中其实有不少的内存管理库比如glibc的ptmallocFreeBSD的jemalloc。glibc中的内存分配器是ptmalloc2TCMalloc号称要比它快。一次malloc和free操作ptmalloc需要300ns而tcmalloc只要50ns。 Front-end 它是一个内存缓存提供了快速分配和重分配内存给应用的功能。它主要有2部分组成Per-thread cache 和 Per-CPU cache。 Middle-end 职责是给Front-end提供缓存。也就是说当Front-end缓存内存不够用时从Middle-end申请内存。它主要是 Central free list 这部分内容。 Back-end 这一块是负责从操作系统获取内存并给Middle-end提供缓存使用。它主要涉及 Page Heap 内容。 Go 语言的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配它的核心理念是使用多级缓存将对象根据大小分类并按照类别实施不同的分配策略。 随着Go的迭代Go的内存管理与TCMalloc不一致地方在不断扩大但其主要思想、原理和概念都是和TCMalloc一致的。 同一进程下的所有线程共享相同的内存空间它们申请内存时需要加锁如果不加锁就存在同一块内存被2个线程同时访问的问题。 TCMalloc的做法是什么呢为每个线程预分配一块缓存线程申请小内存时可以从缓存分配内存这样有3个好处 减少系统调用速度快为线程预分配缓存需要进行1次系统调用后续线程申请小内存时直接从缓存分配都是在用户态执行的没有了系统调用缩短了内存总体的分配和释放时间这是快速分配内存的主要原因。 降低了锁竞争 对于小对象多个线程同时申请小内存从各自的缓存分配访问的是不同的地址空间从而无需加锁把内存并发访问的粒度进一步降低了。对于大对象TCMalloc尝试使用粒度较好和有效的自旋锁。 节省内存分配N个8字节对象使用大约8N * 1.01字节的空间。而ptmalloc2中每个对象都使用了一个四字节的头。 要使用TCMalloc只要将tcmalloc通过“-ltcmalloc”链接器标志接入你的应用即可。 也可以通过LD_PRELOAD动态加载$ LD_PRELOAD”/usr/lib/libtcmalloc.so”。 例如Mysql要使用TCMalloc可以把TCMalloc动态库加到mysqld_safe中启动 也可也静态编译要依次编译libunwindTCMalloc然后编译mysqlconfigure中加入–with-mysqld-ldflags-ltcmalloc选项。 2、TCMalloc基本原理
TCMalloc将整个虚拟内存空间划分为n个同等大小的Page。将n个连续的page连接在一起组成一个Span。 PageHeap向OS申请内存申请的span可能只有一个page也可能有n个page。
ThreadCache内存不够用会向CentralCache申请CentralCache内存不够用时会向PageHeap申请PageHeap不够用就会向OS操作系统申请。 TCMalloc的几个重要概念
Page 操作系统对内存管理以页为单位默认大小是8KBTCMalloc也是这样只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等而是倍数关系。《TCMalloc解密》里称x64下Page大小是8KB。
Span 一组连续的Page被称为Span比如可以有2个页大小的Span也可以有16页大小的Span多个这样的span就用链表来管理。Span比Page高一个层级是为了方便管理一定大小的内存区域TCMolloc以span为单位向系统申请内存。申请内存分裂span回收内存合并span。 ThreadCache线程缓存 ThreadCache是每个线程各自的Cache一个Cache包含多个空闲内存块链表size classes每个链表size-class连接的都是内存块object同一个链表上内存块object的大小是相同的也可以说按内存块大小给内存块分了个类这样可以根据申请的内存大小快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache所以ThreadCache访问是无锁的。
size class每一个size class都对应着不同空闲内存块的大小 例如8字节、16字节等。共有(1B~256KB)分为85个类别。
CentralCache中心缓存 CentralCache是所有线程共享的缓存也是保存的空闲内存块链表链表数量与ThreadCache中链表数量相同 当ThreadCache的内存块不足时可以从CentralCache获取内存块 当ThreadCache内存块过多时可以放回CentralCache。 由于CentralCache是共享的所以它的访问是要加锁自旋锁的。
PageHeap页堆 PageHeap是对堆内存的抽象PageHeap存的也是若干链表链表保存的是Span。当CentralCache的内存不足时会从PageHeap获取空闲的内存Span然后把1个Span拆成若干内存块添加到对应大小的链表中并分配内存当CentralCache的内存过多时会把空闲的内存块放回PageHeap中。 3、对象大小类别和内存分配回收流程
TCMalloc对象的大小将对象分成小对象、中对象、大对象三种
小对象大小0~256KB中对象大小257~1MB大对象大小1MB
小对象的分配流程ThreadCache - CentralCache - HeapPage
分配 当一个线程申请内存的时候将要分配的内存大小映射到对应的size class 1ThreadCache获取(无需加锁)查看ThreadCache中size class对应的FreeList。若ThreadCache的FreeList有空闲对象则返回一个空闲对象分配结束 2CentralCache获取若ThreadCache没有空闲对象的时候向CentralCache中对应的class size获取对象 CentralCache是线程共享的所以需要自旋锁若有可用对象将分配的class size放到ThreadCache的FreeList中返回对象分配结束 3PageHeap申请如果CentralCache也没有可用的对象向PageHeap申请一个span将span拆分成class size放到CentralCache的freeList中。 大部分时候ThreadCache缓存都是足够的不需要去访问CentralCache和HeapPage无系统调用配合无锁分配分配效率是非常高的。 回收 根据申请内存地址计算页号通过页号找到对应的span通过span知道对应的size class若没超过ThreadCache的阈值2MB)则使用垃圾回收机制移动到CentralCache 中对象分配流程中对象大小(256KB, 1MB])
直接在PageHeap中选择适当的大小即可128 Page的Span所保存的最大内存就是1MB。
分配 在PageHeap中的span list顺序选择一个非空链表M(n个page)然后按照内存大小将M分成2类一种是满足大小的k个page,返回对象分配结束。另外一种的n-k的page会继续放在n-kpage的span list中。 若PageHeap没有合适的空闲块时就按照大对象内存分配进行分配。 回收 根据申请内存地址计算页号通过页号找到对应的span寻找到对应的span大小进行回收 大对象分配流程
从large span set选择合适数量的页面组成span用来存储数据。
分配 在PageHeap中的span set选取最新的span进行分配(n个page)也是分成2类一种是满足大小的k个page,返回对象分配结束。另外一种的n-k的若n-k128,将剩下的page放在span set中其他会继续放在n-k个page的span list中。 回收 根据申请内存地址计算页号通过页号找到对应的span寻找到对应的span大小进行回收若没有对应的大小则继续放在span set中 因为程序中的绝大多数对象的大小都在 32KB 以下而申请的内存大小影响 Go 语言运行时分配内存的过程和开销所以分别处理大对象和小对象有利于提高内存分配器的性能。 五、go具体内存管理组件 Go内存管理的许多概念在TCMalloc中已经有了含义是相同的只是名字有一些变化。Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件这几种最重要组件对应的数据结构分别是 runtime.mspan、runtime.mcache、runtime.mcentral 和 runtime.mheap。 所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局每一个处理器都会分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配它们会持有内存管理单元 runtime.mspan。 Page
与TCMalloc中的Page相同x64架构下1个Page的大小是8KB。上图的最下方1个浅蓝色的长方形代表1个Page。
Span
Span与TCMalloc中的Span相同Span是内存管理的基本单位代码中为mspan一组连续的Page组成1个Span所以上图一组连续的浅蓝色长方形代表的是一组Page组成的1个Span另外1个淡紫色长方形为1个Span。
mcache
mcache与TCMalloc中的ThreadCache类似mcache保存的是各种大小的Span并按Span class分类小对象直接从mcache分配内存它起到了缓存的作用并且可以无锁访问。但是mcache与ThreadCache也有不同点TCMalloc中是每个线程1个ThreadCacheGo中是每个P拥有1个mcache。因为在Go程序中当前最多有GOMAXPROCS个线程在运行所以最多需要GOMAXPROCS个mcache就可以保证各线程对mcache的无锁访问线程的运行又是与P绑定的把mcache交给P刚刚好。
mcentral
mcentral与TCMalloc中的CentralCache类似是所有线程共享的缓存需要加锁访问。它按Span级别对Span分类然后串联成链表当mcache的某个级别Span的内存被分配光时它会向mcentral申请1个当前级别的Span。
但是mcentral与CentralCache也有不同点CentralCache是每个级别的Span有1个链表mcache是每个级别的Span有2个链表这和mcache申请内存有关稍后我们再解释。
mheap
mheap与TCMalloc中的PageHeap类似它是堆内存的抽象把从OS申请出的内存页组织成Span并保存起来。当mcentral的Span不够用时会向mheap申请内存而mheap的Span不够用时会向OS申请内存。mheap向OS的内存申请是按页来的然后把申请来的内存页生成Span组织起来同样也是需要加锁访问的。
但是mheap与PageHeap也有不同点mheap把Span组织成了树结构而不是链表并且还是2棵树然后把Span分配到heapArena进行管理它包含地址映射和span是否包含指针等位图这样做的主要原因是为了更高效的利用内存分配、回收和再利用。 object size代码里简称size指申请内存的对象大小。size class代码里简称class它是size的级别相当于把size归类到一定大小的区间段比如size[1,8]属于size class 1size(8,16]属于size class 2。span class指span的级别但span class的大小与span的大小并没有正比关系。span class主要用来和size class做对应1个size class对应2个span class2个span class的span大小相同只是功能不同1个用来存放包含指针的对象一个用来存放不包含指针的对象不包含指针对象的Span就无需GC扫描了。num of page代码里简称npage代表Page的数量其实就是Span包含的页数用来分配内存。1、Go内存分配 Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑运行时根据对象的大小将对象分成微对象、小对象和大对象三种
类别大小微对象(0, 16B)小对象[16B, 32KB]大对象(32KB, ∞)
微对象 (0, 16B) — 先使用微型分配器再依次尝试线程缓存mcache、中心缓存mcentral和堆mheap分配内存小对象 [16B, 32KB] — 依次尝试使用线程缓存mcache、中心缓存mcentral和堆mheap分配内存大对象 (32KB, ∞) — 直接在堆mheap上分配内存
2、微对象内存分配
Go语言运行时将小于 16 字节的对象划分为微对象它会使用线程缓存上的微分配器提高微对象分配的性能我们主要使用它来分配较小的字符串以及逃逸的临时变量。微分配器可以将多个较小的内存分配请求合入同一个内存块中只有当内存块中的所有对象都需要被回收时整片内存才可能被回收。
微分配器管理的对象不可以是指针类型管理多个对象的内存块大小 maxTinySize 是可以调整的在默认情况下内存块的大小为 16 字节。maxTinySize 的值越大组合多个对象的可能性就越高内存浪费也就越严重maxTinySize 越小内存浪费就会越少不过无论如何调整8 的倍数都是一个很好的选择。 如上图所示微分配器已经在 16 字节的内存块中分配了 12 字节的对象如果下一个待分配的对象小于 4 字节它会直接使用上述内存块的剩余部分减少内存碎片不过该内存块只有所有对象都被标记为垃圾时才会回收。
线程缓存 runtime.mcache 中的 tiny 字段指向了 maxTinySize 大小的块如果当前块中还包含大小合适的空闲内存运行时会通过基地址和偏移量获取并返回这块内存
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size maxSmallSize {if noscan size maxTinySize {off : c.tinyoffsetif offsize maxTinySize c.tiny ! 0 {x unsafe.Pointer(c.tiny off)c.tinyoffset off sizec.local_tinyallocsreleasem(mp)return x}...}...}...
}当内存块中不包含空闲的内存时下面的这段代码会先从线程缓存找到跨度类对应的内存管理单元 runtime.mspan调用 runtime.nextFreeFast 获取空闲的内存当不存在空闲内存时我们会调用 runtime.mcache.nextFree 从中心缓存或者页堆中获取可分配的内存块
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size maxSmallSize {if noscan size maxTinySize {...span : c.alloc[tinySpanClass]v : nextFreeFast(span)if v 0 {v, _, _ c.nextFree(tinySpanClass)}x unsafe.Pointer(v)(*[2]uint64)(x)[0] 0(*[2]uint64)(x)[1] 0if size c.tinyoffset || c.tiny 0 {c.tiny uintptr(x)c.tinyoffset size}size maxTinySize}...}...return x
}获取新的空闲内存块之后上述代码会清空空闲内存中的数据、更新构成微对象分配器的几个字段 tiny 和 tinyoffset 并返回新的空闲内存。
3、小对象内存分配
小对象是指大小为 16 字节到 32,768 字节的对象以及所有小于 16 字节的指针类型的对象小对象的分配可以被分成以下的三个步骤
确定分配对象的大小以及跨度类runtime.spanClass 从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间调用 runtime.memclrNoHeapPointers清空空闲内存中的所有数据
确定待分配的对象大小以及跨度类需要使用预先计算好的 size_to_class8、size_to_class128 以及 class_to_size 字典这些字典能够帮助我们快速获取对应的值并构建runtime.spanClass
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size maxSmallSize {...} else {var sizeclass uint8if size smallSizeMax-8 {sizeclass size_to_class8[(sizesmallSizeDiv-1)/smallSizeDiv]} else {sizeclass size_to_class128[(size-smallSizeMaxlargeSizeDiv-1)/largeSizeDiv]}size uintptr(class_to_size[sizeclass])spc : makeSpanClass(sizeclass, noscan)span : c.alloc[spc]v : nextFreeFast(span)if v 0 {v, span, _ c.nextFree(spc)}x unsafe.Pointer(v)if needzero span.needzero ! 0 {memclrNoHeapPointers(unsafe.Pointer(v), size)}}} else {...}...return x
}在上述代码片段中我们会重点分析两个方法的实现原理它们分别是 runtime.nextFreeFast 和 runtime.mcache.nextFree这两个方法会帮助我们获取空闲的内存空间。runtime.nextFreeFast 会利用内存管理单元中的 allocCache 字段快速找到该字段为 1 的位数我们在上面介绍过 1 表示该位对应的内存空间是空闲的
func nextFreeFast(s *mspan) gclinkptr {theBit : sys.Ctz64(s.allocCache)if theBit 64 {result : s.freeindex uintptr(theBit)if result s.nelems {freeidx : result 1if freeidx%64 0 freeidx ! s.nelems {return 0}s.allocCache uint(theBit 1)s.freeindex freeidxs.allocCountreturn gclinkptr(result*s.elemsize s.base())}}return 0
}找到了空闲的对象后我们就可以更新内存管理单元的 allocCache、freeindex 等字段并返回该片内存如果我们没有找到空闲的内存运行时会通过 runtime.mcache.nextFree 找到新的内存管理单元
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {s c.alloc[spc]freeIndex : s.nextFreeIndex()if freeIndex s.nelems {c.refill(spc)s c.alloc[spc]freeIndex s.nextFreeIndex()}v gclinkptr(freeIndex*s.elemsize s.base())s.allocCountreturn
}在上述方法中如果我们在线程缓存中没有找到可用的内存管理单元会通过前面介绍的 runtime.mcache.refill 使用中心缓存中的内存管理单元替换已经不存在可用对象的结构体该方法会调用新结构体的 runtime.mspan.nextFreeIndex 获取空闲的内存并返回。
4、大对象
运行时对于大于 32KB 的大对象会单独处理我们不会从线程缓存或者中心缓存中获取内存管理单元而是直接调用 runtime.mcache.allocLarge 分配大片内存
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size maxSmallSize {...} else {var s *mspanspan c.allocLarge(size, needzero, noscan)span.freeindex 1span.allocCount 1x unsafe.Pointer(span.base())size span.elemsize}publicationBarrier()mp.mallocing 0releasem(mp)return x
}runtime.mcache.allocLarge 会计算分配该对象所需要的页数它按照 8KB 的倍数在堆上申请内存
func (c *mcache) allocLarge(size uintptr, needzero bool, noscan bool) *mspan {npages : size _PageShiftif size_PageMask ! 0 {npages}...s : mheap_.alloc(npages, spc, needzero)mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)s.limit s.base() sizeheapBitsForAddr(s.base()).initSpan(s)return s
}申请内存时会创建一个跨度类为 0 的 runtime.spanClass 并调用 runtime.mheap.alloc 分配一个管理对应内存的管理单元。
五、Go变量的内存位置 1、Go变量的位置由编译器决定 我们在写C、PHP、Java的时候可以很容易的知道所写的变量所在的位置带new、malloc等字段的那一定是在堆上分配了至于后续GC怎么处理有没有引用继续关联堆有没与释放程序是否存在内存泄露…这都是后续处理的问题了变量的存储位置是肯定是在堆上了。 但是在用Go的时候要注意new、make等等关键字都不好使Go变量的位置不是由写程序的程序员来决定的而是Go自行处理所以可能你的变量是new出来的但是最终也不一定分配到堆上很可能是分配在栈上。 Go把变量的位置在哪儿这件事对程序员“隐藏”了Go自行处理因为Go认为变量的存储位置会对程序的性能有一定影响而Go是计划打造对性能有极致要求的程序因而自己管了。 Go是这么管的 首先栈stack上的效率肯定是比堆要高的这算是常识 Go在编译期会对每一个函数变量做判断如果不能够判断此函数中的变量在返回之后是否仍被引用到就给把变量扔堆heap上否则就扔栈stack上。但是注意如果变量非常大还是会扔到堆heap上。
2、栈内存空间 每个goroutine都有自己的栈栈的初始大小是2KB100万的goroutine会占用2G但goroutine的栈会在2KB不够用时自动扩容当扩容为4KB的时候百万goroutine会占用4GB。 栈区的内存一般由编译器自动分配和释放其中存储着函数的入参以及局部变量这些参数会随着函数的创建而创建函数的返回而消亡一般不会在程序中长期存在这种线性的内存分配策略有着极高地效率但是用户也往往不能控制栈内存的分配这部分工作基本都是由编译器完成的。 Go 语言使用用户态线程 Goroutine 作为执行上下文它的额外开销和默认栈大小都比线程小很多然而 Goroutine 的栈内存空间和栈结构也在早期几个版本中发生过一些变化
v1.0 ~ v1.1 — 最小栈内存空间为 4KBv1.2 — 将最小栈内存提升到了 8KBv1.3 — 使用连续栈替换之前版本的分段栈8v1.4 — 将最小栈内存降低到了 2KB
Goroutine 的初始栈内存在最初的几个版本中多次修改从 4KB 提升到 8KB 是临时的解决方案其目的是为了减轻分段栈中的栈分裂对程序的性能影响在 v1.3 版本引入连续栈之后Goroutine 的初始栈大小降低到了 2KB进一步减少了 Goroutine 占用的内存空间。
3、内存逃逸机制 在 C 语言和 C 这类需要手动管理内存的编程语言中将对象或者结构体分配到栈上或者堆上是由工程师自主决定的这也为工程师的工作带来的挑战如果工程师能够精准地为每一个变量分配合理的空间那么整个程序的运行效率和内存使用效率一定是最高的但是手动分配内存会导致如下的两个问题
不需要分配到堆上的对象分配到了堆上 — 浪费内存空间需要分配到堆上的对象分配到了栈上 — 悬挂指针、影响内存安全
与悬挂指针相比浪费内存空间反而是小问题。在 C 语言中栈上的变量被函数作为返回值返回给调用方是一个常见的错误在如下所示的代码中栈上的变量 i 被错误返回
int *dangling_pointer() {int i 2;return i;
}当 dangling_pointer 函数返回后它的本地变量会被编译器回收调用方获取的是危险的悬挂指针我们不确定当前指针指向的值是否合法时这种问题在大型项目中是比较难以发现和定位的。 应用程序的每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等这些内存会由编译器在栈中进行分配每一个函数都会分配一个栈桢在函数运行结束后进行销毁但是有些变量我们想在函数运行结束后仍然使用它那么就需要把这个变量在堆上分配,这种从栈上逃逸到堆上的现象就成为内存逃逸。 在编译器优化中逃逸分析是用来决定指针动态作用域的方法。Go 语言的编译器使用逃逸分析决定哪些变量应该在栈上分配哪些变量应该在堆上分配其中包括使用 new、make 和字面量等方法隐式分配的内存Go 语言的逃逸分析遵循以下两个不变性
指向栈对象的指针不能存在于堆中指向栈对象的指针不能在栈对象回收后存活在栈上分配的地址一般由系统申请和释放不会有额外性能的开销比如函数的入参、局部变量、返回值等。在堆上分配的内存如果要回收掉需要进行GC那么GC一定会带来额外的性能开销。编程语言不断优化GC算法主要目的都是为了减少GC带来的额外性能开销,变量一旦逃逸会导致性能开销变大。
逃逸机制 编译器会根据变量是否被外部引用来决定是否逃逸: 1.如果函数外部没有引用则优先放到栈中; 2.如果函数外部存在引用则必定放到堆中; 3.如果栈上放不下则必定放到堆上; 4、逃逸分析的工具
我们是否有办法知道我们写的Go程序中变量的位置呢 答案是有的Go向开发者提供了变量逃逸分析的工具
go build -gcflags -m -l main.go
这里的main.go也可以是某个具体的二进制应用程序
下面对如下代码进行逃逸分析
import (fmt
)func main(){a: 3b : 5ret : add(a, b)fmt.Println(ret)
}func add(x,y int)int {sum : x yreturn sum
}
分析结果:
./main.go:11:16: main ... argument does not escape ./main.go:11:16: ret escapes to heap
5、总结 1.栈上分配内存比在堆中分配内存效率更高 2.栈上分配的内存不需要GC处理,而堆需要 3.逃逸分析目的是决定内分配地址是栈还是堆 4.逃逸分析在编译阶段完成 因为无论变量的大小只要是指针变量都会在堆上分配所以对于小变量我们还是使用传值效率而不是传指针更高一点