Go语言内存详解 | 青训营笔记

59 阅读5分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天

重点内容

  1. 自动内存管理
  2. Go内存管理及优化
  3. 编译器和静态分析
  4. Go编译器优化

详细知识点

自动内存管理

· 动态内存

程序在运行时根据需求动态分配的内存:malloc()

· 自动内存管理

避免手动内存管理,专注于实现业务逻辑 

保证内存使用的**正确**性和**安全性**:double-free problem、use-after-free problem

· 三个任务

为新对象分配空间 找到存活的对象 回收死亡对象的内存空间

相关概念:
· Mutator:业务线程,分配新对象,修改对象指向关系

· Collector:GC线程,找到存活对象,回收死亡对象的内存空间

· Serial GC:只有一个collector

· Parallel GC:支持多个collectors同时回收的GC算法

· Concurrent GC:mutator(s)和collector(s)可以同时执行

三种GC,Concurrent GC必须感知对象指向关系的变化,否则会导致存活对象被回收

评价GC算法:

· 安全性(Safety):不能回收存活的对象 基本要求

· 吞吐率(Throughput):1- GC时间/程序执行总时间 花在GC上的时间越短越好

· 暂停时间(Pause time):stop the world (STW) 业务是否感知

· 内存开销(Space overhead) GC元数据开销

追踪垃圾回收

回收目标:指针指向关系不可达的对象

过程:

· 标记根对象(静态变量、全局变量、常量、线程栈等)

· 标记:找到可达对象(从根对象出发,找到所有可达对象)

· 清理:所有不可达对象

· Copying GC:将存活对象复制到另外的内存空间,原空间就可分配了 

· Mark-sweap GC:将死亡对象的内存标记为“可分配” 

· Mark-compact GC:移动并整理存活对象,也就是让他们的地址连续,分配其他空间时就可以从后面开始分配了

分代GC

简单来说, 经历过GC的次数决定对象的年龄,年轻代的存货少,用copying gc,老年代的对象趋于一直活着,反复复制开销大,采用mark-sweep

引用计数

看到这个就联想到C++的智能指针,原理一样

· 每个对象有一个与之关联的引用数目 · 对象存活条件:当且仅当引用数大于0

缺点:

· 维护引用计数开销大:通过原子操作保证对引用计数操作的原子性和可见性 · 无法回收环形数据结构 -- weak reference · 内存开销 · 回收内存时依然可能引发暂停

Go内存管理及优化

分块

目标:为对象在heap上分配内存

提前将内存分块

系统调用mmap()向OS申请一大块内存,例如4MB 先将内存划分为大块,例如8KB,称为mspan 再将大块继续划分为特定大小的小块,用于对象分配 noscan mspan:分配不包含指针的对象--GC不需要扫描 scan mspan:分配包含指针的对象--GC需要扫描

缓存

虚拟内存一类的?这里就讲了个大概

内存管理优化

Balanced GC

本质:将多个小对象的分配合并成一次大对象的分配

方案:移动GAB中存活的对象 当GAB总大小超过一定阈值时,将GAB中存活的对象复制到另外分配的GAB中 原先的GAB可以释放,避免内存泄漏 本质:用copying GC的算法管理小对象

编译器和静态分析

结构

image-20230120194950469

静态分析

不执行程序代码,推导程序行为,分析程序的性质

两种分析

控制流(Control flow): 程序执行的流程 数据流(Data flow): 数据在控制流上的传递

过程内分析和过程间分析

过程内(Intra-procedural analysis):仅在函数内进行分析 过程间(Inter-procedural analysis):考虑过程调用时参数传递和返回值的数据流和控制流

过程间分析要得知调用接口的时哪个类型的变量

Go编译器优化

WHY: 用户无感知,重新编译即可获得性能收益 通用性优化

思路: 用编译时间换取更高效的机器码

函数内联

内联:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

优点: 消除调用开销 将过程间分析转化为过程内分析

缺点: 函数体变大,icache不友好 编译生成的Go镜像变大

Beast mode

Go受到很多限制:interface,defer等,限制了内联。 内联策略保守

Beast mode:调整内联策略,使更多函数被内联 降低函数调用的开销 增加其他优化的机会

逃逸分析

分析代码中指针的动态作用域:指针在何处可以被访问

若发现指针p在当前作用域s: · 作为参数传给其他函数 · 传递给全局变量 · 传递给其他goroutine · 传递给已经逃逸的指针指向的对象 则p指向的对象逃逸出s,反之则没有逃逸出s

Beast mode 让更多函数被内联,扩展了函数边界,更多对象不逃逸

优化:未逃逸的对象可在栈上分配 对象在栈上的分配和回收很快,只要移动sp指针就行 减少在heap上的分配,降低GC负担