Go

Golang的特性五(堆内存和栈内存)----内存管理

Royal
2023-07-25 / 0 评论 / 8 阅读 / 正在检测是否收录...

一. 内存管理概述

内存管理是编程语言设计中至关重要的一环,它直接影响着程序的性能、稳定性和开发效率。Go 语言作为一门现代化的编程语言,在内存管理方面做了许多精妙的设计,使其在高效、安全和易用性之间取得了良好的平衡。
内存管理一般包含三个不同的组件,分别是用户程序(Mutator)、分配器(Allocator)和收集器(Collector),当用户程序申请内存时,它会通过内存分配器申请新内存,而分配器会负责从堆中初始化相应的内存区域。
m7j5v1lg.png

在了解内管管理之前,我们先了解下什么是堆内存?什么是栈内存?什么是堆?什么是栈?

二. 堆和栈:程序运行时的内存空间
在计算机科学中,堆(Heap) 和 栈(Stack) 是程序运行时用于存储数据的两种重要内存区域。它们有着不同的特性和用途,理解它们的区别对于编写高效、可靠的程序至关重要。

1、 栈 (Stack)
栈是一种遵循 后进先出 (LIFO) 原则的数据结构,类似于一摞盘子,最后放上去的盘子会被最先拿走。
2、堆 (Heap)
堆是一种用于动态内存分配的内存区域,程序可以在运行时从堆中申请和释放内存。

堆和栈的比较

特性
分配方式编译器自动分配手动分配 (new/malloc)
释放方式编译器自动释放手动释放 (delete/free)
效率
空间
线程安全线程私有线程共享
生命周期与函数调用绑定灵活

三. 内存分区
在 Go 语言中,程序运行时的内存空间主要分为以下几个部分:

  • 栈内存 (Stack) : 用于存储局部变量、函数参数、函数调用的返回地址等。每个 goroutine 都有自己的栈空间,栈内存的分配和释放由编译器自动完成,效率极高。
  • 堆内存 (Heap): 用于存储动态分配的数据,例如使用 new 或 make 创建的对象。堆内存是所有 goroutine 共享的,由垃圾回收器负责管理。
  • 全局区: 用于存储全局变量、常量等。
  • 代码区: 用于存储程序的二进制代码。

四. 栈内存详解
栈内存的特点

  • 高效: 栈内存的分配和释放由编译器自动完成,只需要移动栈顶指针即可,效率极高。
  • 线程私有: 每个 goroutine 都有自己的栈空间,栈内存是线程私有的,不需要加锁保护。
  • 空间有限: 栈内存的空间有限,通常只有几 MB 大小。

栈内存的分配
当一个函数被调用时,编译器会为该函数的局部变量和参数在栈上分配内存空间。函数返回时,这些内存空间会被自动释放。

栈内存的优缺点

优点

  • 分配和释放效率高。
  • 无需手动管理,由编译器自动完成。
  • 线程私有,无需加锁保护。

缺点

  • 空间有限,不适合存储大量数据。
  • 生命周期与函数调用绑定,不够灵活。

五. 堆内存详解

堆内存的特点

  • 动态分配: 堆内存的空间是动态分配的,可以根据需要申请和释放。
  • 空间较大: 堆内存的空间通常比栈内存大得多,可以存储大量数据。
  • 线程共享: 堆内存是所有 goroutine 共享的,需要加锁保护。

堆内存的分配

  • new: 用于分配值类型的内存,例如 int, float64, struct 等。
  • make: 用于分配引用类型的内存,例如 slice, map, channel 等。

堆内存的释放
堆内存的释放由垃圾回收器负责管理。垃圾回收器会定期扫描堆内存,回收不再使用的对象。

堆内存的优缺点

优点:

  • 空间较大,可以存储大量数据。
  • 生命周期灵活,不受函数调用限制。

缺点:

  • 分配和释放效率比栈内存低。
  • 需要手动管理,容易造成内存泄漏。
  • 线程共享,需要加锁保护。

六. 逃逸分析

Go 语言的编译器会进行逃逸分析,确定变量是分配在栈上还是堆上。如果一个变量在函数返回后仍然被引用,则它会被分配到堆上,否则会被分配到栈上。

逃逸分析可以帮助编译器优化代码,减少堆内存的分配,提高程序性能。

七. Go 内存管理组件

Go 语言的内存分配器包含内存管理单元、线程缓存、中心缓存和页堆几个重要组件:

  • runtime.mspan
  • runtime.mcache
  • runtime.mcentral
  • runtime.mheap

八. 内存管理思想

Go 内存管理核心思想可以分为以下几点:

  • 每次从操作系统申请一大块儿的内存,由 Go 对这块儿内存做分配,减少系统调用。
  • 内存分配借鉴了 Google 的 TCMalloc(Thead-Caching Malloc)算法。

TCMalloc 是由 Google 开发的一种内存分配器,主要用于优化多线程环境下的内存分配和释放性能。TCMalloc 是Thread-Caching Malloc 的缩写,即线程缓存分配器。

TCMalloc 比 glibc 中的 malloc 还要快很多。Go 的内存分配器就借鉴了 TCMalloc 的设计实现高速的内存分配,它的核心思想是使用多级缓存并将对象根据大小分类,按照类别实施不同的分配策略。

TCMalloc 中将内存分成三类,即小对象,小于256K的,中型对象,介于256K到1M的,大于1M的为大对象。

TCMalloc 不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,分为线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)。
m7j65khg.png

TCMalloc 的核心思想是:
(1)内存切分,减少碎片。

  • 采用了 span 机制来减少内存碎片。多个连续的内存页(8KB)组成 span,每个 span 又划分成大小固定的多个 slot。
  • slot size 有 67 种,每个 size 有两种类型,scan 和 noscan,表示分配的对象是否包含指针。

(2)分级管理,无锁并降低锁的粒度。

  • 多层次的分配 Cache,每个 P 上有一个 mcache,mcache 会为每个 size 最多缓存一个 span,用于无锁分配。
  • 全局每个 size 的 span 都有一个 mcentral,锁的粒度相对于全局的 mheap 小很多。每个 mcentral 可以看成是每个 size 的 span 的一个全局后备 cache。获取不到再上升到全局的 mheap。mheap 获取不到再向系统申请。从无锁到全局 1/(67*2)力度的锁,再到全局锁,再到系统调用。

(3)回收复用

  • 内存由 GC 进行释放。回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。
  • 只有内存闲置过多的时候,sysmon 协程会定时把 mheap 空余的内存归还给操作系统,降低整体开销。

九. 其他内存分配方法

除了上面讲的TCMalloc内存方法还有其他两种内存分配方法,一种是线性分配器,另一种是空闲链表分配器。
线性分配器
线性分配器(Bump Allocator)是一种高效的内存分配方法,但是有较大的局限性。当我们使用线性分配器时,只需要在内存中维护一个指向内存特定位置的指针,如果用户程序向分配器申请内存,分配器只需要检查剩余的空闲内存、返回分配的内存区域并修改指针在内存中的位置,即移动下图中的指针:
m7j63ldu.png

空闲链表分配器
空闲链表分配器(Free-List Allocator)可以重用已经被释放的内存,它在内部会维护一个类似链表的数据结构。当用户程序申请内存时,空闲链表分配器会依次遍历空闲的内存块,找到足够大的内存,然后申请新的资源并修改链表:
m7j63x5e.png

0

评论

博主关闭了当前页面的评论