96SEO 2026-02-19 23:35 13
申请与主动释放管理增加了逃逸分析和GC将开发者从内存管理中释放出来让开发者有更多的精力去关注软件设计而不是底层的内存问题。

我们无须精通复杂的内存管理但掌握内存的管理可以让你写出更高质量的代码和高效快速还定位问题这是要求具备本质思维从问题表象逐层深入本质的过程。
https://zhuanlan.zhihu.com/p/76802887
http://goog-perftools.sourceforge.net/doc/tcmalloc.html
学习的本质是知识搬迁通过不断的探索、实践和思考总结来增强认知能力。
计算机系统中有几类存储设备cache、内存、外存。
从上至下的访问速度越来越慢。
具体可以了解《Linux内存管理》https://guisu.blog.csdn.net/article/details/6152921
物理内存是由若干个存储单元组成的每个存储单元有一个编号这种编号可唯一标识一个存储单元称为内存地址或物理地址。
我们可以把内存看成一个从0字节一直到内存最大容量逐字节编号的存储单元数组即每个存储单元与内存地址的编号相对应。
2
虚拟内存地址就是每个进程可以直接寻址的地址空间不受其他进程干扰。
每个指令或数据单元都在这个虚拟空间中拥有确定的地址。
虚拟内存不考虑物理内存的大小和信息存放的实际位置只规定进程中相互关联信息的相对位置。
每个进程都拥有自己的虚拟内存且虚拟内存的大小由处理机的地址结构和寻址方式决定。
如直接寻址如果cpu的有效地址长度为16位则其寻址范围0
-64k。
32位机器可以直接寻址4G空间意思是每个应用程序都有4G内存空间可用。
虚拟内存一般分为以下4大块1栈空间特点是内存地址连续先进后出里面放了局部变量、函数形参、自动变量。
编译器自动分配和释放进行管理。
2堆空间特点是内存地址是不连续一般是链表结构先进后出用户自己管理申请malloc分配calloc释放realloc
是.data静态数据区保存了初始化的全局变量还有static修饰的变量。
4代码段存放了源代码。
一个可执行程序在存储(没有调入内存时主要分为代码段数据段未初始化数据段三部分。
可执行程序在运行时又多出了两个区域栈段Stack和堆段(Heap)。
操作系统有内存管理、linux有内存管理、jvm也有内存管理等GO也有内存管理。
操作系统内存管理主要包括物理内存管理和虚拟内存管理具体可以了解《操作系统内存管理》https://guisu.blog.csdn.net/article/details/5713164。
程序中的数据和变量都会被分配到程序所在的虚拟内存中内存空间包含两个重要区域栈区Stack和堆区Heap。
函数调用的参数、返回值以及局部变量大都会被分配到栈上这部分内存会由编译器进行管理
当我们说应用程序内存管理的时候主要是指堆内存的管理因为栈的内存管理不需要程序去操心。
等编程语言会由工程师和编译器共同管理堆中的对象由内存分配器分配并由垃圾收集器回收。
Java的jvm内存管理运行JVM虚机通过参数配置jvm内存大小系统将分配给它一块内存区域运行数据区这一内存区域由JVM自己来管理。
JVM内存可以划分为5大块Java栈、程序计数寄存器PC寄存器、本地方法栈Native
GO内存管理GO应用程序的内存一般也会分成堆区和栈区程序在运行期间可以主动从堆区申请内存空间这些内存由内存分配器分配并由垃圾收集器负责回收。
在一个最简单的内存管理中堆内存最初会是一个完整的大块即未分配任何内存。
内存申请当发现内存申请的时候堆内存就会从未分配内存分割出一个小内存块(block)然后用链表把所有内存块连接起来。
内存释放释放内存实质是把使用的内存块从链表中取出来然后标记为未使用当分配内存块的时候可以从未使用内存块中优先查找大小相近的内存块如果找不到再从未分配的内存中分配内存。
编程语言的内存分配器一般包含两种分配方法一种是线性分配器Sequential
Allocator是一种高效的内存分配方法但是有较大的局限性。
当我们使用线性分配器时只需要在内存中维护一个指向内存特定位置的指针如果用户程序向分配器申请内存分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置即移动下图中的指针
虽然线性分配器实现为它带来了较快的执行速度以及较低的实现复杂度但是线性分配器无法在内存被释放时重用内存。
如下图所示如果已经分配的内存被回收线性分配器无法重新利用红色的内存
因为线性分配器具有上述特性所以需要与合适的垃圾回收算法配合使用例如标记压缩Mark-Compact、复制回收Copying
GC等算法它们可以通过拷贝的方式整理存活对象的碎片将空闲内存定期合并这样就能利用线性分配器的效率提升内存分配器的性能了。
Allocator可以重用已经被释放的内存它在内部会维护一个类似链表的数据结构。
当用户程序申请内存时空闲链表分配器会依次遍历空闲的内存块找到足够大的内存然后申请新的资源并修改链表
因为不同的内存块通过指针构成了链表所以使用这种方式的分配器可以重新利用回收的资源但是因为分配内存时需要遍历链表所以它的时间复杂度是
空闲链表分配器可以选择不同的策略在链表中的内存块中进行选择最常见的是以下四种
从链表头开始遍历选择第一个大小大于申请内存的内存块循环首次适应Next-Fit—
从上次遍历的结束位置开始遍历选择第一个大小大于申请内存的内存块最优适应Best-Fit—
从链表头遍历整个链表选择最合适的内存块隔离适应Segregated-Fit—
将内存分割成多个链表每个链表中的内存块大小相同申请内存时先找到满足条件的链表再从链表中选择合适的内存块
语言使用的内存分配策略与第四种策略有些相似我们通过下图了解该策略的原理
字节的内存时它会在上图中找到满足条件的空闲内存块并返回。
隔离适应的分配策略减少了需要遍历的内存块数量提高了内存分配的效率。
Malloc就是一个内存分配器管理堆内存主要影响malloc和free用于降低频繁分配、释放内存造成的性能损耗并且有效地控制内存碎片。
在Linux操作系统中其实有不少的内存管理库比如glibc的ptmallocFreeBSD的jemalloc。
glibc中的内存分配器是ptmalloc2TCMalloc号称要比它快。
一次malloc和free操作ptmalloc需要300ns而tcmalloc只要50ns。
它是一个内存缓存提供了快速分配和重分配内存给应用的功能。
它主要有2部分组成Per-thread
职责是给Front-end提供缓存。
也就是说当Front-end缓存内存不够用时从Middle-end申请内存。
它主要是
这一块是负责从操作系统获取内存并给Middle-end提供缓存使用。
它主要涉及
的设计实现高速的内存分配它的核心理念是使用多级缓存将对象根据大小分类并按照类别实施不同的分配策略。
随着Go的迭代Go的内存管理与TCMalloc不一致地方在不断扩大但其主要思想、原理和概念都是和TCMalloc一致的。
同一进程下的所有线程共享相同的内存空间它们申请内存时需要加锁如果不加锁就存在同一块内存被2个线程同时访问的问题。
TCMalloc的做法是什么呢为每个线程预分配一块缓存线程申请小内存时可以从缓存分配内存这样有3个好处
减少系统调用速度快为线程预分配缓存需要进行1次系统调用后续线程申请小内存时直接从缓存分配都是在用户态执行的没有了系统调用缩短了内存总体的分配和释放时间这是快速分配内存的主要原因。
对于小对象多个线程同时申请小内存从各自的缓存分配访问的是不同的地址空间从而无需加锁把内存并发访问的粒度进一步降低了。
对于大对象TCMalloc尝试使用粒度较好和有效的自旋锁。
1.01字节的空间。
而ptmalloc2中每个对象都使用了一个四字节的头。
要使用TCMalloc只要将tcmalloc通过“-ltcmalloc”链接器标志接入你的应用即可。
LD_PRELOAD”/usr/lib/libtcmalloc.so”。
例如Mysql要使用TCMalloc可以把TCMalloc动态库加到mysqld_safe中启动
也可也静态编译要依次编译libunwindTCMalloc然后编译mysqlconfigure中加入–with-mysqld-ldflags-ltcmalloc选项。
TCMalloc将整个虚拟内存空间划分为n个同等大小的Page。
将n个连续的page连接在一起组成一个Span。
PageHeap向OS申请内存申请的span可能只有一个page也可能有n个page。
ThreadCache内存不够用会向CentralCache申请CentralCache内存不够用时会向PageHeap申请PageHeap不够用就会向OS操作系统申请。
操作系统对内存管理以页为单位默认大小是8KBTCMalloc也是这样只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等而是倍数关系。
《TCMalloc解密》里称x64下Page大小是8KB。
一组连续的Page被称为Span比如可以有2个页大小的Span也可以有16页大小的Span多个这样的span就用链表来管理。
Span比Page高一个层级是为了方便管理一定大小的内存区域TCMolloc以span为单位向系统申请内存。
申请内存分裂span回收内存合并span。
ThreadCache是每个线程各自的Cache一个Cache包含多个空闲内存块链表size
classes每个链表size-class连接的都是内存块object同一个链表上内存块object的大小是相同的也可以说按内存块大小给内存块分了个类这样可以根据申请的内存大小快速从合适的链表选择空闲内存块。
由于每个线程有自己的ThreadCache所以ThreadCache访问是无锁的。
例如8字节、16字节等。
共有(1B~256KB)分为85个类别。
CentralCache是所有线程共享的缓存也是保存的空闲内存块链表链表数量与ThreadCache中链表数量相同
当ThreadCache的内存块不足时可以从CentralCache获取内存块
当ThreadCache内存块过多时可以放回CentralCache。
由于CentralCache是共享的所以它的访问是要加锁自旋锁的。
PageHeap是对堆内存的抽象PageHeap存的也是若干链表链表保存的是Span。
当CentralCache的内存不足时会从PageHeap获取空闲的内存Span然后把1个Span拆成若干内存块添加到对应大小的链表中并分配内存当CentralCache的内存过多时会把空闲的内存块放回PageHeap中。
TCMalloc对象的大小将对象分成小对象、中对象、大对象三种
小对象大小0~256KB中对象大小257~1MB大对象大小1MB
当一个线程申请内存的时候将要分配的内存大小映射到对应的size
1ThreadCache获取(无需加锁)查看ThreadCache中size
class对应的FreeList。
若ThreadCache的FreeList有空闲对象则返回一个空闲对象分配结束
2CentralCache获取若ThreadCache没有空闲对象的时候向CentralCache中对应的class
CentralCache是线程共享的所以需要自旋锁若有可用对象将分配的class
size放到ThreadCache的FreeList中返回对象分配结束
3PageHeap申请如果CentralCache也没有可用的对象向PageHeap申请一个span将span拆分成class
大部分时候ThreadCache缓存都是足够的不需要去访问CentralCache和HeapPage无系统调用配合无锁分配分配效率是非常高的。
根据申请内存地址计算页号通过页号找到对应的span通过span知道对应的size
class若没超过ThreadCache的阈值2MB)则使用垃圾回收机制移动到CentralCache
list顺序选择一个非空链表M(n个page)然后按照内存大小将M分成2类一种是满足大小的k个page,返回对象分配结束。
另外一种的n-k的page会继续放在n-kpage的span
若PageHeap没有合适的空闲块时就按照大对象内存分配进行分配。
根据申请内存地址计算页号通过页号找到对应的span寻找到对应的span大小进行回收
set选取最新的span进行分配(n个page)也是分成2类一种是满足大小的k个page,返回对象分配结束。
另外一种的n-k的若n-k128,将剩下的page放在span
根据申请内存地址计算页号通过页号找到对应的span寻找到对应的span大小进行回收若没有对应的大小则继续放在span
语言运行时分配内存的过程和开销所以分别处理大对象和小对象有利于提高内存分配器的性能。
Go内存管理的许多概念在TCMalloc中已经有了含义是相同的只是名字有一些变化。
Go
语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件这几种最重要组件对应的数据结构分别是
runtime.mspan、runtime.mcache、runtime.mcentral
语言程序都会在启动时初始化如上图所示的内存布局每一个处理器都会分配一个线程缓存
与TCMalloc中的Page相同x64架构下1个Page的大小是8KB。
上图的最下方1个浅蓝色的长方形代表1个Page。
Span与TCMalloc中的Span相同Span是内存管理的基本单位代码中为mspan一组连续的Page组成1个Span所以上图一组连续的浅蓝色长方形代表的是一组Page组成的1个Span另外1个淡紫色长方形为1个Span。
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与TCMalloc中的CentralCache类似是所有线程共享的缓存需要加锁访问。
它按Span级别对Span分类然后串联成链表当mcache的某个级别Span的内存被分配光时它会向mcentral申请1个当前级别的Span。
但是mcentral与CentralCache也有不同点CentralCache是每个级别的Span有1个链表mcache是每个级别的Span有2个链表这和mcache申请内存有关稍后我们再解释。
mheap与TCMalloc中的PageHeap类似它是堆内存的抽象把从OS申请出的内存页组织成Span并保存起来。
当mcentral的Span不够用时会向mheap申请内存而mheap的Span不够用时会向OS申请内存。
mheap向OS的内存申请是按页来的然后把申请来的内存页生成Span组织起来同样也是需要加锁访问的。
但是mheap与PageHeap也有不同点mheap把Span组织成了树结构而不是链表并且还是2棵树然后把Span分配到heapArena进行管理它包含地址映射和span是否包含指针等位图这样做的主要原因是为了更高效的利用内存分配、回收和再利用。
class代码里简称class它是size的级别相当于把size归类到一定大小的区间段比如size[1,8]属于size
class的span大小相同只是功能不同1个用来存放包含指针的对象一个用来存放不包含指针的对象不包含指针对象的Span就无需GC扫描了。
num
page代码里简称npage代表Page的数量其实就是Span包含的页数用来分配内存。
1、Go内存分配
语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑运行时根据对象的大小将对象分成微对象、小对象和大对象三种
先使用微型分配器再依次尝试线程缓存mcache、中心缓存mcentral和堆mheap分配内存小对象
依次尝试使用线程缓存mcache、中心缓存mcentral和堆mheap分配内存大对象
字节的对象划分为微对象它会使用线程缓存上的微分配器提高微对象分配的性能我们主要使用它来分配较小的字符串以及逃逸的临时变量。
微分配器可以将多个较小的内存分配请求合入同一个内存块中只有当内存块中的所有对象都需要被回收时整片内存才可能被回收。
的值越大组合多个对象的可能性就越高内存浪费也就越严重maxTinySize
字节它会直接使用上述内存块的剩余部分减少内存碎片不过该内存块只有所有对象都被标记为垃圾时才会回收。
大小的块如果当前块中还包含大小合适的空闲内存运行时会通过基地址和偏移量获取并返回这块内存
sizec.local_tinyallocsreleasem(mp)return
}当内存块中不包含空闲的内存时下面的这段代码会先从线程缓存找到跨度类对应的内存管理单元
unsafe.Pointer(v)(*[2]uint64)(x)[0]
}获取新的空闲内存块之后上述代码会清空空闲内存中的数据、更新构成微对象分配器的几个字段
确定分配对象的大小以及跨度类runtime.spanClass
从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间调用
runtime.memclrNoHeapPointers清空空闲内存中的所有数据
size_to_class8、size_to_class128
字典这些字典能够帮助我们快速获取对应的值并构建runtime.spanClass
size_to_class8[(sizesmallSizeDiv-1)/smallSizeDiv]}
size_to_class128[(size-smallSizeMaxlargeSizeDiv-1)/largeSizeDiv]}size
uintptr(class_to_size[sizeclass])spc
{memclrNoHeapPointers(unsafe.Pointer(v),
runtime.mcache.nextFree这两个方法会帮助我们获取空闲的内存空间。
runtime.nextFreeFast
}在上述方法中如果我们在线程缓存中没有找到可用的内存管理单元会通过前面介绍的
使用中心缓存中的内存管理单元替换已经不存在可用对象的结构体该方法会调用新结构体的
的大对象会单独处理我们不会从线程缓存或者中心缓存中获取内存管理单元而是直接调用
unsafe.Pointer(span.base())size
span.elemsize}publicationBarrier()mp.mallocing
needzero)mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)s.limit
sizeheapBitsForAddr(s.base()).initSpan(s)return
我们在写C、PHP、Java的时候可以很容易的知道所写的变量所在的位置带new、malloc等字段的那一定是在堆上分配了至于后续GC怎么处理有没有引用继续关联堆有没与释放程序是否存在内存泄露…这都是后续处理的问题了变量的存储位置是肯定是在堆上了。
但是在用Go的时候要注意new、make等等关键字都不好使Go变量的位置不是由写程序的程序员来决定的而是Go自行处理所以可能你的变量是new出来的但是最终也不一定分配到堆上很可能是分配在栈上。
Go把变量的位置在哪儿这件事对程序员“隐藏”了Go自行处理因为Go认为变量的存储位置会对程序的性能有一定影响而Go是计划打造对性能有极致要求的程序因而自己管了。
Go在编译期会对每一个函数变量做判断如果不能够判断此函数中的变量在返回之后是否仍被引用到就给把变量扔堆heap上否则就扔栈stack上。
但是注意如果变量非常大还是会扔到堆heap上。
每个goroutine都有自己的栈栈的初始大小是2KB100万的goroutine会占用2G但goroutine的栈会在2KB不够用时自动扩容当扩容为4KB的时候百万goroutine会占用4GB。
栈区的内存一般由编译器自动分配和释放其中存储着函数的入参以及局部变量这些参数会随着函数的创建而创建函数的返回而消亡一般不会在程序中长期存在这种线性的内存分配策略有着极高地效率但是用户也往往不能控制栈内存的分配这部分工作基本都是由编译器完成的。
是临时的解决方案其目的是为了减轻分段栈中的栈分裂对程序的性能影响在
这类需要手动管理内存的编程语言中将对象或者结构体分配到栈上或者堆上是由工程师自主决定的这也为工程师的工作带来的挑战如果工程师能够精准地为每一个变量分配合理的空间那么整个程序的运行效率和内存使用效率一定是最高的但是手动分配内存会导致如下的两个问题
语言中栈上的变量被函数作为返回值返回给调用方是一个常见的错误在如下所示的代码中栈上的变量
函数返回后它的本地变量会被编译器回收调用方获取的是危险的悬挂指针我们不确定当前指针指向的值是否合法时这种问题在大型项目中是比较难以发现和定位的。
应用程序的每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等这些内存会由编译器在栈中进行分配每一个函数都会分配一个栈桢在函数运行结束后进行销毁但是有些变量我们想在函数运行结束后仍然使用它那么就需要把这个变量在堆上分配,这种从栈上逃逸到堆上的现象就成为内存逃逸。
语言的编译器使用逃逸分析决定哪些变量应该在栈上分配哪些变量应该在堆上分配其中包括使用
指向栈对象的指针不能存在于堆中指向栈对象的指针不能在栈对象回收后存活在栈上分配的地址一般由系统申请和释放不会有额外性能的开销比如函数的入参、局部变量、返回值等。
在堆上分配的内存如果要回收掉需要进行GC那么GC一定会带来额外的性能开销。
编程语言不断优化GC算法主要目的都是为了减少GC带来的额外性能开销,变量一旦逃逸会导致性能开销变大。
因为无论变量的大小只要是指针变量都会在堆上分配所以对于小变量我们还是使用传值效率而不是传指针更高一点
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback