Go的内存管理之道 | 青训营笔记

41 阅读6分钟

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

前言

内存管理在程序中是很重要的存在,Go和Java一样都是自动内存管理机制,因此,可以让作为编程人员的我们更加专注于业务本身,我个人认为这点是优点,因此对于Go的内存管理很有学习的必要!

知识点内容

1.性能优化

内存管理也好,编译器优化也好,最终的目的就是对程序性能的优化。

性能优化的目的有两点:一个是提升用户体验,再一个就是让资源高效利用。在优化时主要从三个层面去考虑优化:业务层优化:针对特定场景,具体问题去具体分析,容易获得较大性能收益;语言运行时优化:用来解决更通用的性能问题,需要考虑更多场景;通过数据驱动优化,可以通过自动化性能分析工具——pprof去查看分析当前程序的运行情况,即我们要依靠数据而非猜测,而首要的优化就是去优化最大瓶颈。

2.内存管理及优化

Go依靠自动内存管理(垃圾回收),从而实现自动管理程序内存,这里的设计思想和Java很像,都有自己的内存管理机制,也是我觉得一门现代编程语言所应该具备的,我个人认为手动管理内存对编程人员的专业素养要求很高。

2-1.垃圾回收机制

GO的垃圾回收机制主要有两个,也是和Java一样。一个是追踪垃圾回收(可达性分析法),另一个是引用计数法。

2-1-1.追踪垃圾回收(被回收的条件:不可达对象)

可作为标记根对象(GC roots)的有: 静态变量、全局变量、常量、线程栈等。

过程实现:①Copying GC: 将存活对象从一块内存空间复制到另外一块内存空间,原先的空间可以直接进行对象分配;②Mark-sweep GC: 将死亡对象所在内存块标记为可分配,使用 free list 管理可分配的空间;③Mark-compact GC: 将存活对象复制到同一块内存区域的开头。

2-1-2.引用计数(对象存活的条件:当且仅当引用数大于0)

过程实现:①给对象中添加一个引用计数器:②每当有一个地方引用它,计数器就加1;③当引用失效,计数器就减1;④任何时候计数器为0的对象就是不可能再被使用的。 我认为这个回收方法最大的一个缺点就是无法回收环形数据,如果现在两个对象互相依赖引用,那么它们的引用计数器始终大于0。

2-2.垃圾回收算法

分代GC 根据对象GC次数的不同将对象分代,一般分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法

2-3.Go内存管理

2-3-1.分块(为对象在 heap 上分配内存)

调用系统调用mmap()向操作系统申请一大块内存,例如4MB; 先将内存划分成大块,例如8KB,称作mspan; 再将大块继续划分成特定大小的小块,用于对象分配; noscan mspan:分配不包含指针的对象——GC不需要扫描; scan mspan:分配包含指针的对象——GC需要扫描; 对象分配:根据对象的大小,选择最合适的块返回。

2-3-2.缓存

Go内存管理构成了多级缓存机制,从操作系统分配得的内存被内存管理回收后,也不会立刻归还给操作系统,而是在Go runtime内部先缓存起来,从而避免频繁向操作系统申请内存。

这里有个关键:线上分配内存是高频操作以及分配中小对象占比非常高。因此,优化小对象分配是关键。

2-3-3.字节跳动的优化方案(balanced GC)

策略是将多次小对象的分配合并成一次大对象的分配。因此,当大对象中哪怕只有一个小对象存活时,Go runtime也会认为整个大对象存活。为此,balanced GC会根据GC策略,将大对象中存活的对象移动到另外的大对象中,从而压缩并清理大对象的内存空间,原先的大对象空间由于不再有存活对象,可以全部释放。

3.编译器优化思路

大思路上是面向后端长期执行的任务,用适当增加编译时间换取更高性能的代码。主要有以下两种方案:

3-1.函数内联

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

我们需要根据调用和被调用函数的规模来决定是否内联,函数内联在大多数情况下是正向优化,即多内联,会提升性能,因为这样做消除了调用开销,缺点就是系统开销增大了,因此需要仔细分析是否需要内联。

3-2.逃逸分析

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

从对象分配处出发,沿着控制流,观察数据流。若发现指针p在当前作用域s:作为参数传递给其他函数;传递给全局变量;传递给其他的 go routine;传递给已逃逸的指针指向的对象;则指针p逃逸出s,反之则没有逃逸出s。

而未逃逸出当前函数的指针指向的对象也可以在栈上分配。

小结

不得不说,今天的课程相比前几天轻松了很多,主要体现在Go的内存管理机制上很多东西都和Java一致,因此学习起来很快。主要的新知识点体现在编译器那部分,因为我以前做Java程序优化从来没考虑过编译器层面也可以优化,更多得是考虑项目可以部署更多的机器、数据库可以读写分离以及使用redis缓存热点数据,还有代码层面上的优化。所以,对编译器进行优化给我提供了一个新思路。

参考文档

青训营学习资料