0.1、索引

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

1、内存管理

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

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

2、经典的内存模型

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

文本段

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

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

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

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

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

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

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

栈和堆的区别

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

内存分配的重要性

内存是有限的。如果一个程序继续消耗内存而不释放它,它将耗尽内存并自行崩溃。因此,软件程序不能随心所欲地继续使用内存,它会导致其他程序和进程耗尽内存。由于这一点的重要性,大多数编程语言(包括 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

golang的内存管理的更多相关文章

  1. Golang 的内存管理(上篇)

    Golang 的内存管理基于 tcmalloc,可以说起点挺高的.但是 Golang 在实现的时候还做了很多优化,我们下面通过源码来看一下 Golang 的内存管理实现.下面的源码分析基于 go1.8 ...

  2. Golang内存管理

    Golang 内存管理 原文链接[http://legendtkl.com/2017/04/02/golang-alloc/] Golang 的内存管理基于 tcmalloc,可以说起点挺高的.但是 ...

  3. Go语言内存管理(一)内存分配

    Go语言内存管理(一)内存分配 golang作为一种"高级语言",也提供了自己的内存管理机制.这样一方面可以简化编码的流程,降低因内存使用导致出现问题的频率(C语言使用者尤其是初学 ...

  4. Qt的内存管理机制

    当我们在使用Qt时不可避免得需要接触到内存的分配和使用,即使是在使用Python,Golang这种带有自动垃圾回收器(GC)的语言时我们仍然需要对Qt的内存管理机制有所了解,以更加清楚的认识Qt对象的 ...

  5. 【转】Go 内存管理

    1. 前言 编写过C语言程序的肯定知道通过malloc()方法动态申请内存,其中内存分配器使用的是glibc提供的ptmalloc2.除了glibc,业界比较出名的内存分配器有Google的tcmal ...

  6. 深入理解golang:内存分配原理

    一.Linux系统内存 在说明golang内存分配之前,先了解下Linux系统内存相关的基础知识,有助于理解golang内存分配原理. 1.1 虚拟内存技术 在早期内存管理中,如果程序太大,超过了空闲 ...

  7. 可视化Go内存管理

    小结: 1. Go不需要VM,Go应用程序二进制文件中嵌入了一个小型运行时(Go runtime),可以处理诸如垃圾收集(GC),调度和并发之类的语言功能 Go does not need a VM ...

  8. 2万字|30张图带你领略glibc内存管理精髓(因为OOM导致了上千万损失)

    前言 大家好,我是雨乐. 5年前,在上家公司的时候,因为进程OOM造成了上千万的损失,当时用了一个月的时间来分析glibc源码,最终将问题彻底解决. 最近在逛知乎的时候,发现不少人有对malloc/f ...

  9. Go内存管理一文足矣

    最早学习C.C++语言时,它们都是把内存的管理全部交给开发者,这种方式最灵活但是也最容易出问题,对人员要求极高:后来出现的一些高级语言像Java.JavaScript.C#.Go,都有语言自身解决了内 ...

随机推荐

  1. 万答#15,都有哪些情况可能导致MGR服务无法启动

    欢迎来到 GreatSQL社区分享的MySQL技术文章,如有疑问或想学习的内容,可以在下方评论区留言,看到后会进行解答 本文转载自微信公众号 "老叶茶馆" 欢迎大家关注! 1.都有 ...

  2. [GYCTF2020]Ezsqli-1|SQL注入

    1.打开界面之后在输入框进行输入测试,分别输入1.2.3.'等字符,结果如下: 2.看到bool(false)这里我想到了bool注入,因为之前做过这道题:https://www.cnblogs.co ...

  3. 「vijos-bashu」lxhgww的奇思妙想(长链剖分)

    倍增离线,预处理出爹和孙子们.查询\(O(1)\) #include <cstdio> #include <cstring> #include <numeric> ...

  4. CF280D k-Maximum Subsequence Sum(线段树)

    在做这题时我一开始把\(tag\)写入了结构体 #include <iostream> #include <cstdio> #include <cstring> # ...

  5. POJ3903Stock Exchange (LIS)

    学了下BIT,炸了... #include <iostream> #include <cstdio> #include <cstring> #include < ...

  6. 【Java】学习路径58-TCP聊天-双向发送实现

    这一章内容比较复杂(乱) 重点在于解决利用TCP协议实现双向传输. 其余的细节(比如end)等,不需要太在意. 但是我也把折腾经历写出来了,如果大家和我遇到了类似的问题,下文可以提供一个参考. 目标: ...

  7. 【java】学习路线2-构造、Scanner包导入、字符串操作、数组、引用类型

    请先查看前置知识: [JAVA]基础1-字符串.堆.栈.静态与引用类型 https://www.cnblogs.com/remyuu/p/15990274.html import java.util. ...

  8. CF-1623C

    Problem - 1623C - Codeforces 题意: 给出一个序列,从第三个数字开始,你可以让他减少3*d,然后让它的前两个数字,分别加2*d,和d,找出序列中的最小值的最大值. 题解: ...

  9. c#中容易被忽视的foreach

    有句俗语:百姓日用而不知.我们c#程序员很喜欢,也非常习惯地用foreach.今天呢,我就带大家一起探索foreach,走,开始我们的旅程. 一.for语句用的好好的,为什么要提供一个foreach? ...

  10. 【面试题】JS改变this指向的三种方法

    一.this指向 点击打开视频讲解更加详细 this随处可见,一般谁调用,this就指向谁.this在不同环境下,不同作用下,表现的也不同. 以下几种情况,this都是指向window 1.全局作用下 ...