这是我参与「第五届青训营」伴学笔记创作活动的第 4 天
自动内存管理
与Java一样,Goland也有垃圾回收机制,即自动内存管理,程序在运行时会根据需求动态分配的内存,自动管理内存的调度。
自动内存管理可以让程序员专注于业务的实现,也能够保证内存使用的正确性和安全性
相关概念
- Grabage collction: 垃圾回收
- Mutator: 业务线程
- Collector: GC 线程
- Concurrent GC: 并发 GC
衡量一个垃圾回收算法好坏的指标
- Parallel GC: 并行 GC
- 安全性(Safety):指垃圾回收器不应回收存活的对象;
- 吞吐率(Throughput):指垃圾回收器花在 GC 上的时间占程序执行总时间的比率;
- 暂停时间(Pause time):指垃圾回收导致业务线程挂起的时间(stop the world/STW)
- 内存开销(Space overhead):指垃圾回收器元数据占用的内存开销;
常见的垃圾回收算法
引用计数算法
每个对象都有一个与之关联的引用数目
对象存活的条件:当且仅当引用数大于 0,当小于0时,对象会被回收
优点
内存管理的操作被平摊到程序运行中:指针传递的过程中进行引用计数的增减
不需要了解 runtime 的细节:因为不需要标记 GC roots,因此不需要知道哪里是全局变量、线程栈等
缺点
开销大,因为对象可能会被多线程访问,对引用计数的修改需要原子操作保证原子性和可见性
无法回收环形数据结构,假设有一个互相指向的环形链表,各自的引用数量都不为0,所以无法被回收,占用了系统资源
每个对象都引入额外存储空间存储引用计数
虽然引用计数的操作被平摊到程序运行过程中,但是回收大的数据结构依然可能引发暂停,导致用户的体验变差
追踪垃圾回收
过程
标记根对象 : 静态变量、全局变量、常量、线程栈等
标记:找到所有可达对象
清理:回收所有不可达对象占据的内存空间
清理分为三个阶段
1.将存活对象复制到另外的内存空间(Copying GC),原先的空间可以直接进行对象分配
2.将死亡对象的内存标记为"可分配"(Mark-sweep GC),使用 free list 管理可分配的空间
3.移动并整理存活对象(Mark-compact GC)
分代GC
将对象分为新生代和和老年代
新生代的对象创建的数量多,被回收的数量也多,系统需要频繁地去管理,根据这个特性,可以对新生代的对象采用复制算法
即将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
老年代的对象不会立刻被回收,经过了多次的新生代的回收算法后还存活,较为稳定,不需要频繁整理
因此对老年代使用: 标记 - 清除 或者 标记 - 整理 算法
标记-清除算法: 将存活的对象进行标记,然后清理掉未被标记的对象。
标记和清除过程效率都不高;
会产生大量不连续的内存碎片,导致无法给大对象分配内存
标记-整理算法: 让所有存活的对象都向一端移动,然后直接清理掉另一端的内存。
第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象。
第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。