golang的内存管理

0.1、索引

https://blog.waterflow.link/articles/1663406367769

1、内存管理

内存管理是管理计算机内存的过程,在主存和磁盘之间移动进程以提高系统的整体性能。内存管理的基本要求是提供方法来根据程序的请求动态的将部分内存分配给程序,并在不需要时释放它以供重用。

程序通过将他们的内存划分为执行特定任务的不同部分来管理他们。栈和堆就是这部分中的俩个,他们管理程序的未使用的内存并将其分配给不同类型的数据。当程序不再需要这些内存的时候就会释放他们,供后续使用。

2、经典的内存模型

[图片上传失败...(image-cbdca7-1666656179207)]

正在运行的程序将其数据保存在这些逻辑区域之中。操作系统在将逻辑加载到内存时为全局变量和静态变量分配内存,并且在程序结束之前不会释放它。这些值不会被修改。另外俩个区域,堆和栈,更多的是动态分配的变量。在这些区域中,程序根据需要去分配和释放内存。这两个区域之间的区别下面会说到。

文本段

文本段是目标文件或内存中程序的一部分,其中包含可执行指令。文本段一般放在堆或栈的下方,以防止堆栈溢出被覆盖。

初始化数据和未初始化数据段

数据段是程序虚拟地址空间的一部分,其中包含由程序员初始化的全局变量和静态变量。

栈用于静态内存分配,就像数据结构中的栈,遵循后进先出。通常函数和局部变量会在栈上分配,当数据被分配到栈上或是从栈上弹出时,实际上没有任何物理移动,只有保存在栈中的值会被修改。这使得从栈中存储和查询数据的过程非常快,因为不需要查找。 我们可以从它最上面的块中存储和查询数据。 存储在栈上的任何数据都必须是有限且静态的。 这意味着数据的大小在编译时是已知的。 堆的内存管理简单明了,由操作系统完成。 因为栈的大小是有限的,我们可能会在这里遇到堆栈溢出错误。

这里的堆和数据结构中的堆是没有关系的。堆用于动态内存分配。栈只允许在顶部进行分配和释放,而程序可以在堆中的任何位置分配或释放内存。程序必须以与其分配相反的顺序将内存返回到栈。但是程序可以以任何顺序将内存返回到堆中。这意味着堆比栈更灵活。指针、数组和大数据结构通常存储在堆中。

存储在堆上的数据必须形成一个足够大的连续块,以使用单个内存块满足请求。此属性增加了堆的复杂性。首先,执行分配操作的代码必须扫描堆,直到找到足够大的连续内存块来满足请求。其次,当内存返回堆时,必须合并相邻的已释放块,以更好地适应未来对大块内存的请求。这种增加的复杂性意味着使用堆管理内存比使用堆栈慢。

堆内存分配方案不提供自动解除分配。我们需要使用垃圾回收器来删除未使用的对象,以便有效地使用内存。

栈和堆的区别

  • 与栈相比,堆更,因为查找数据的过程涉及更多。
  • 堆比栈可以存储更多的数据。
  • 堆以动态大小存储数据;栈以静态大小存储数据。
  • 堆在应用程序的线程之间共享。
  • 堆由于其动态特性而更难管理。
  • 当我们谈论内存管理时,我们主要是在谈论管理堆内存。
  • 堆内存分配不是线程安全的,因为存储在此空间中的数据对所有线程都是可访问或可见的。

内存分配的重要性

内存是有限的。如果一个程序继续消耗内存而不释放它,它将耗尽内存并自行崩溃。因此,软件程序不能随心所欲地继续使用内存,它会导致其他程序和进程耗尽内存。由于这一点的重要性,大多数编程语言(包括 Go)都提供了自动内存管理的方法。

3、go的内存模型

Go 支持自动内存管理,例如自动内存分配和自动垃圾回收,避免了很多隐藏的 bug。

在 Go 中,每个线程都有自己的堆栈。当我们启动一个线程时,我们分配一块内存用作该线程的堆栈。当这块内存不够用时,问题就来了。为了克服这个问题,我们可以增加堆栈的默认大小,但增加堆栈的大小意味着每个线程的大小都会增加。如果我们想使用大量线程,这将非常低效。

另一种选择是单独决定每个线程的堆栈大小。同样,在我们有很多线程的设置中,这将是低效的,因为我们需要弄清楚我们应该为每个堆栈分配多少内存。

Go 的创建者想出的不是给每个 goroutine 一个固定数量的堆栈内存,而是 Go 运行时尝试根据需要为 goroutine 提供所需的堆栈空间。这样我们在创建线程时就不需要考虑堆栈大小了。

goroutine 以2 kb的堆栈大小开始,可以根据需要增长和缩小。Go 检查它即将执行的函数所需的堆栈数量是否可用,如果不够用,则调用morestack分配一个新帧,然后才执行该函数。当该函数退出时,它的返回参数被复制回原始堆栈帧,并且任何不需要的堆栈空间都被释放。

堆栈也有上限。如果达到此限制,我们的应用程序将panic并中止。

Go 在两个地方分配内存:一个用于动态分配的全局堆和一个用于每个 goroutine 的本地堆栈。Go 与许多垃圾收集语言相比的一个主要区别是,许多对象直接分配在程序堆上。Go 更喜欢在栈上分配。栈分配代价更低,因为它只需要两条 CPU 指令:一条推入栈进行分配,另一条从栈中释放。

不幸的是,并非所有数据都可以使用栈上分配的内存。栈分配要求可以在编译时确定变量的生命周期和内存占用。如果无法确定,则在运行时动态分配到堆上。

Go 编译器使用一个称为逃逸分析的过程来查找其生命周期在编译时已知的对象,并将它们分配到栈上而不是在垃圾回收的堆内存中。基本思想是在编译时做垃圾回收的工作。编译器跨代码区域跟踪变量的范围。它使用这些数据来确定哪些变量持有一组检查,以证明它们的生命周期在运行时是完全可知的。如果变量通过了这些检查,则可以在栈上分配值。如果不是,就代表逃逸,并且必须进行堆分配。

内存是在栈上分配还是逃到堆上完全取决于你如何使用内存,而不是你如何声明变量。

可以通过下面的命令查看是否有内存逃逸,go build -gcflags '-m'

4、垃圾回收

垃圾回收是自动内存管理的一种形式。垃圾回收器尝试回收由程序分配但不再被引用的内存。

Go 的垃圾回收器是一个非分代并发、三色标记和清理垃圾回收器

分代垃圾回收器专注于最近分配的对象,因为它假设像临时变量这样的短期对象最常被回收。

Go 编译器更喜欢在栈上分配对象,短期对象通常分配在栈上而不是堆上;这意味着不需要分代GC。

Go 的垃圾回收分为两个阶段,标记阶段清除阶段。GC 使用三色算法来分析内存块的使用情况。该算法首先将仍被引用的对象标记为“活跃”,并在下一阶段(扫描)释放不活跃对象的内存。

不用回收垃圾,但是可以减少垃圾

导致垃圾回收代价高主要因素之一是堆上的对象数量。通过优化我们的代码以减少堆上长寿命对象的数量,我们可以最大限度地减少花费在 GC 上的资源,并提高我们的系统性能。

重构结构

在读取数据时,现代计算机 CPU 的内部数据寄存器可以保存和处理 64 位。这称为字长。它通常是 32 位或 64 位的。

当我们不对齐数据以适应字长时,会添加填充以正确对齐内存中的字段,以便下一个字段可以从一个字长倍数的偏移量开始。

当数据自然对齐时,现代 CPU 硬件最有效地执行对内存的读取和写入。Go 编译器使用所需的对齐来确保并排存储的内存可以使用公倍数访问。它的值等于结构中最大字段所需的内存大小。

在 Go 中创建struct时,会为其分配一个连续的内存块。Go 内存分配器不会针对数据结构对齐进行优化,因此通过重新排列结构的字段,您可以通过降低填充来降低内存使用量。

通常go中的类型对应的字节大小如下

/**
var a bool          // 1字节
var b int16         // 2字节
var c int32         // 4字节
var d int64         // 8字节
var e int32         // 4字节
var f int64         // 8字节
var g int           // 8字节
var h string        // 16字节
var i float32       // 4字节
var j float64       // 8字节
var k interface{}   // 16字节
var l time.Time     // 24字节,结构体字节数不稳定
var m time.Timer    // 80字节,结构体字节不稳定
var n time.Duration // 8字节
var o []byte        // 24字节
**/

例如,下面 User

type User1 struct {
    Age    uint8 // 1字节
    Hunger int64 // 8字节
    Happy  bool  // 1字节
}

可以看到10个字节就能保存这些属性,但是我们可以看下实际占用了多少字节:

go run struct.go
Size of main.User1 struct: 24 bytes

我们可以修改下User的结构

type User1 struct {
    Hunger int64 // 8字节
    Age    uint8 // 1字节
    Happy  bool  // 1字节
}

看下结果,减少了8个字节的长度

go run struct.go
Size of main.User1 struct: 16 bytes

诀窍就是根据字段的大小降序排列这些字段,后面的Age和Happy因为没有超过一个机器字,非配了8个字节。所以总共分配了16字节。

完整的代码如下:

package main

import (
    "fmt"
    "unsafe"
)

/**
var a bool          // 1字节
var b int16         // 2字节
var c int32         // 4字节
var d int64         // 8字节
var e int32         // 4字节
var f int64         // 8字节
var g int           // 8字节
var h string        // 16字节
var i float32       // 4字节
var j float64       // 8字节
var k interface{}   // 16字节
var l time.Time     // 24字节,结构体字节数不稳定
var m time.Timer    // 80字节,结构体字节不稳定
var n time.Duration // 8字节
var o []byte        // 24字节
var p uint8         // 1字节
**/

type User1 struct {
    Age    uint8 // 1字节
    Hunger int64 // 8字节
    Happy  bool  // 1字节
}

type User2 struct {
    Hunger int64 // 8字节
    Age    uint8 // 1字节
    Happy  bool  // 1字节
}

var user1 User1
var user2 User2

func main() {
    fmt.Printf("Size of %T struct: %d bytes\n", user1, unsafe.Sizeof(user1))

    fmt.Printf("Size of %T struct: %d bytes\n", user2, unsafe.Sizeof(user2))
}

减少长生命周期对象的数量

与其让对象存在于堆上,不如将它们创建为值而不是按需引用。例如,如果我们需要用户请求中每个项目的一些数据,而不是预先计算并将其存储在一个长期存在的映射中,我们可以基于每个请求计算它以减少堆上的对象数量。

删除指针内的指针

如果我们有一个对象的引用,并且对象本身包含更多的指针,那么这些都被认为是堆上的单个对象,即使它们可能是嵌套的。通过将这些嵌套值更改为非指针,我们可以减少要扫描的对象的数量。

避免不必要的字符串/字节数组分配

由于字符串/字节数组在底层被视为指针,因此每个数组都是堆上的一个对象。如果可能,尝试将它们表示为其他非指针值,例如整数/浮点数、时间。

原文:

https://medium.com/@ali.can/memory-optimization-in-go-23a56544ccc0

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 一、tcmalloc介绍<参考资源> go的内存管理和tcmalloc(thread-caching malloc...
    帘外五更风阅读 454评论 0 0
  • 一、知识准备 GMP运行时调度模型 go原生支持并发,不需要像Java那样需要显示地开启一个线程,也不像Pytho...
    HannahLi_9f1c阅读 685评论 0 0
  • 几个关键数据结构 mspan 由mheap管理的页面,记录了所分配的块大小和起始地址等 mcache 与P(可看做...
    SuperGopher阅读 417评论 0 0
  • 程序运行时,会将对象写入内存。在某些情况下,当这些对象不再需要的时候,它们应该被移除。这个过程称为内存管理。本文旨...
    Go语言由浅入深阅读 421评论 0 1
  • 1. Go 内存的划分 强烈推荐参考链接 在讲Go的堆栈之前,先温习一下堆栈基础知识。 什么是堆栈?在计算机中堆栈...
    将军红阅读 76评论 0 1