这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
今日课程内容
今天主要学习了go语言的内存管理方式以及字节是如何优化的,以及编译器优化。
自动内存管理的相关概念
自动内存管理的概念
-
自动内存管理:也叫垃圾回收(GC,garbage collection),是指由程序语言在运行时系统管理动态内存 这么做的好处是:保证内存使用的正确性和安全性,可以专注于业务,避免了手动释放内存。 有Serial GC、Parallel GC和Concurrent GC三种。
-
Mutator: 业务线程
-
Collector: GC 线程
-
Serial GC:只有一个collector,在运行时会暂停。
-
Parallel GC:支持多个collector同时进行GC,也会暂停,性能要比Serial GC高。
-
Concurrent GC:支持 mutator(s) 和 collector(s)同时进行。 Concurrent GC
常见的算法
基本上都是基于分代假说,即分为年轻代和老年代(主要是根据经历GC的次数)。老年代是指经历了多次GC但仍然存活。然后根据分代的不同,制定不同的GC策略,从而减小内存的开销。对年轻代多采用标记复制的方法(因为存活的很少),而对于老年代,多采用标记删除(让老年代呆在原理,清除不保留的)。
追踪垃圾回收
对于不可达的对象,会被清理掉。判断对象是否可达的条件: 首先标记所谓的root对象,通常可以被当作GC root的有全局变量、静态变量、常量、线程栈等(存活时间比较长),然后从这些对象出发,标记所有可达的对象,标记完成后,清理掉所有不可达的对象。
引用计数
认为每个对象都有一个与之关联的数目。在该方法中,当引用数大于0时,就认为该对象是存活的,在进行GC时不清除这个对象。
优点:将GC分摊到程序运行中,同时也不需要去区分哪些是全局变量 缺点:开销大,同时每个对象还要专门开辟一个空间记录它本身的引用数。
此外,之前看java面经的时候,还提到在java中引用计数法有一个缺陷,就是如果两个对象互相引用,会导致两者的引用数始终大于0,进而导致这两个对象始终存活,无法被清除掉。不知道在goland中是不是也有这种问题。
Go语言的内存管理以及优化
内存管理
主要是提前分块和缓存。 在Go中,会首先向OS申请一块内存空间,然后先将这块内存分成大块,再将大块进一步划分成特定大小的小块,用这些小块进行对象分配。在分配时,根据对象的大小,选择适当的块。
Go在分配内存时,做了很多级的缓存,从而加快了速度。从 OS 分配得的内存被内存管理回收后,不会立刻归还给 OS,而是在所谓的Go runtime内先缓存起来,这样就避免了频繁向OS进行申请。
优化
目前存在的问题:内存分配路径长、耗时长。
针对此问题,字节提出了名为balanced GC的优化方案。 (这里还不是太懂)。
编译器优化
编译器的相关概念
静态分析
在不执行程序的情况下,推导程序的行为,分析程序的性质 通常会在控制流分析和数据流分析。
- 控制流分析:分析程序执行的流程,往往会画出控制流程图,
- 数据流分析:分析数据在控制流上的传递
经过分析得出程序的性质后,根据这些性质进行优化。
- 过程内分析:仅在函数内部进行分析
- 过程间分析:同时考虑跨函数的数据流和控制流
编译器优化
通常是一个通用性的优化。 目的: 本章学习的编译优化面对的场景是后端长期执行任务。因此是用编译时间换取更高效的机器码。
函数内联
- 定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定
- 优点:消除调用开销;将过程间分析的问题转换为过程内分析,帮助其他分析
- 缺点:每个函数体变大;编译生成的 Go 镜像文件变大 然后通过benchmark进行了对比,发现性能得到了显著提升。
逃逸分析
定义:分析代码中指针的动态作用域,即指针在何处可以被访问(判断p可不可以在作用域s外访问)
- 过程(思路):
从对象分配处出发,沿控制流,观察数据流。如果发现指针 p 在当前作用域 s内,作为参数传递给其他函数或全局变量或其他的 goroutine或已逃逸的指针指向的对象,那么就认为指针 p 逃逸出作用域 s,反之则没有逃逸出 s。
判断后,对没有逃逸的对象,就可以直接在栈上进行分配,进而降低负载。
总结
这节课主要学习了内存管理的概念,GC的方法以及go中进行内存管理的方式。之后学习了编译器优化的概念以及函数内联和逃逸分析这两种方法。这节课学的知识,有些是自己以前看过的(分代理论,编译原理等),更多的是自己之前没怎么接触过的,经过这节课的学习,我受益匪浅。