Go GC 知识点概要
- 存在Gc
- 1.5之前采用标记清除,之后采用三色标记加写屏障
- 并行Gc
- gc不分代
- 无vm
内存模型
- code area 方法区
- static area 静态变量区域
- heap 堆区域
- stack 栈区域
Go 进程管理模型
- 一个进程对应多个OS线程(与cpu数相同的活跃线程数)
- 线程模型是MPG模型:一个线程对应多个协程(goroutine)
- 对于所有的goroutine来说,heap可以看作是共享的区域
- 每个goroutine有自己的stack
一般程序的内存管理
stack会随着线程执行code area的stack frame,自动pop、push、remove;
stack里的变量、调用完毕,就随着出栈,自动销毁;
但是heap不会,heap内的对象,通常是用stack内或者其它heap对象内的指针变量,对heap内的对象进行操作;
这个也叫做对象引用;
垃圾回收,garbage collect,回收的就是heap的空间;
Gc方式
引用计数
过程
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。
优点
实现简单
缺点
无法解决循环引用的问题。
①当出现左边绿色的情况时,假设外部对A的引用消除,此时因为A引用计数从1减为0,A将被清除。从而对B的引用也消除,B的计数减为0,GC将正确回收对象{A, B}
②然而若出现右边橙色状况,假设外部对E的引用消除,外部对于对象集{C,D,E}不再有引用,但他们之间出现循环引用现象,计数始终保持为1,导致{C,D,E}无法被回收。
参考
标记/清除
过程
- 标记 :就是遍历gcRoot,找到存活的对象,并给他们打上标记。
我理解就是沿着入口方法的调用链路遍历堆内的引用对象,然后打上标记。
- 清除:在标记之后执行,将之前未标记上的对象给清除,然后将之前标记上的对象的标记置空。
- 未标记即未被引用所以要删除
触发时机
通俗的讲就是程序发现内存不够的时候,gc线程就会触发将当前应用程序暂停,然后进行遍历打上标记,然后清除未打上标记的对象再清除之前的标记,最后程序恢复运行。
优点
占用空间比较小,但是效率比较低,而且会导致之前的排列杂乱无章,而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
复制
过程
复制算法就是将内存一分为二,分为空闲区和活动区,当活动区的内存达到上限时,则将活动区的所有存活对象按照原来的顺序复制到空闲区,然后把活动区和空闲区相互置换,同时也把之前的活动区的不活跃对象(通过标记法寻找)清除。
优点
在于效率很高,但是效率高的代价就是占用两倍的内存。而且当对象存活率非常高的时候,这种开销是不可忽视的。
参考
标记/整理(标记/压缩)
过程
- 标记:就是遍历gcRoot,找到存活的对象,并给他们打上标记。
- 整理:移动所有之前标记的存活对象,且按照内存地址值排列,然后将具有内存地址值之后的所有内存清空。
解释
此算法标记过程同上(标记/清除算法)一样,而整理过程和复制算法一样,但是没有内存划分这么一说,少了不必要的内存开销。记/整理算法唯一的缺点就是效率也不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。从效率上来说,标记/整理算法要低于复制算法
分代回收
Java 中采用分代回收,每一代采用以上不同的垃圾回收算法。
- 新生代: 存活时间较短,复制效率高,虽然用了较多内存但是因为大部分存活时间较短所以会释放很多。
- 复制算法
- 老年代:存活时间均较长,复制的话也释放不了多少,反而造成拷贝开销。
- 就地回收(标记清除/整理)
Go的Gc
要点
- 三色标记: gray black white三色标记
- 黑色:对象在这次GC中已标记,且这个对象包含的子对象也已标记
- 灰色:对象在这次GC中已标记, 但这个对象包含的子对象未标记
- 白色:对象在这次GC中未标记
- 写屏障 write barrier
- gc-root可达性分析
- 并发的标记/扫描
- STW(start/stop the world)(开启/暂停所有用户协程)
参考
- 原文
- 翻译版本
Go的栈内存管理
Go由于协程的数量可以无限多,需要的栈内存也很多,所以,设计了stack cache pool的机制;
栈内存用来存储函数内变量;在golang函数、协程退出后,其占用的栈内存也会一同被释放。
栈内存管理的核心思想和堆内存很像;在分配时,首先查找线程内stackcache是否有足够的空间,如果有足够的空间,则进行分配,避免了线程间竞争,提高了效率;若线程内stackcache内存不足,则会向全局stackpool中申请一批stack,按照规格进行切分后,放入到线程的stackcache中,然后再次进行分配。
GC触发条件
自动垃圾回收的触发条件有两个:
- 超过内存大小阈值
- 达到定时时间
阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。比如一次回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高。 如果一直达不到内存大小的阈值呢?这个时候GC就会被定时时间触发,比如一直达不到10M,那就定时(默认2min触发一次)触发一次GC保证资源的回收。
注 - 其他 Java - ZGC
ZGC
JDK11之后Java可以采用的垃圾回收器,同时将原来的分代回收改为分页回收。
疑问:为什么分代回收改为分页回收会提升效率?
开启要求
- 64位机器
- 通过高位区间标记是否为垃圾,一般为44位之上或者42之上,节省空间。
- 传统方式为扫描之后存储在单独开辟的空间里。
- 通过高位区间标记是否为垃圾,一般为44位之上或者42之上,节省空间。
- JDK11之后的版本。
阶段
- 标记阶段
- 初始标记阶段- Stop The World(暂停所有执行线程) 主要针对GC Roots(主线程入口的局部变量/静态变量等)
- 并发标记 - Start The World
- 再标记阶段 - Stop The World - 解决漏标问题(从GC Roots开始再次遍历一遍,由于之前并发标记过所有记录应该是有缓存,因此再次标记速度会很快)
- 再标记解决漏标问题及速度问题需要再思考?
- 漏标问题是由于并发标记阶段可能会发生对象引用关系的变更。
- 并发转移阶段
- 转移准备
- 这个阶段主要做了什么?
- 如图所示,我理解应该是寻找合适的空间
- 这个阶段主要做了什么?
- 初始化转移
- Stop The World 我理解主要针对GC Roots进行复制转移
- 并发转移
- 遍历GC Roots进行全量转移
- 会存在调用寻址变更问题(引入转发表)
- 通过第二次ZGC对上次ZGC中记录进转发表中的数据进行修复,清空转发表。
- 遍历GC Roots进行全量转移
- 转移准备
如下图一次ZGC全流程示意图
参考
java 版垃圾回收 - 各类算法
GcRoot
搞懂Go垃圾回收
golang gc| go语言gc详解
zhuanlan.zhihu.com/p/115143370
总结描述
Go主进程启动,呼起线程池,创建与线程池数量对应的MP
垃圾回收时,垃圾回收协程呼起和P数量对应的回收线程,扫描前STW(stop the world),开启写屏障(记录扫描过程中引用关系发生改变的对象),标记(start the world) 关闭写屏障,执行清除。