java篇:
java由于其比较特殊的内存分配结构, 对待gc也是非常认真, 提炼出了自己的方法。 (具体的清除方法会根据不同的区域,老年代,young代,堆区的其他地方 会采用不同的分代收集的方法)
可达性分析:
1. 可达性
从GCroot(引用节点)节点出发进行遍历,只要从gcroot节点可达,则证明是非垃圾, 对于从gcroot节点达不到的,会标明可回收(但不会立刻回收), 会放入专门用于gc的队列中。
其中只有满足下列条件之一的才可作为gcroot的节点: 虚拟机栈(栈帧中的本地变量表)中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI(Native方法)引用的对象。
以上,含有循环引用的节点不能作为gcroot节点的可达点,所以能被直接忽略。
2.gc的队列处理
在java的根类 Object中, 存在方法 finalize() 。 该方法会在进入gc队列中调用,来判断是否真的需要被回收。
可以通过重写finalize()方法,来与gcroot节点搭建可达性来避免一次(仅能一次)被回收的命运。(如果finalize执行时间过长会被强行中断,以防finalize中有死循环之类的问题)
在队列中扫描,如果finalize()没有结果,或已经执行过一次finalize(),则会被直接回收。释放出对应的空间。
php篇:
php主要以引用计数法为核心的gc手段, 但由于 无法解决循环引用的问题(例如 a和b两个对象 互相调用就会变成环状调用,导致内存泄漏) 。 所以在5.3中重新调整了gc的方法,增加了部分逻辑来针对上述问题。
定义一个变量在底层会有引用记录(refcount), 每增加一个引用,refcount++;
1. 深度优先遍历扫描所有的存储节点, 并且扫描过后的节点refcount--(学名为模拟删除), 并被标记(防止多次--)。 (如果refcount本身为0则直接标记回收)
2.第二次遍历节点,
2.1 if refcount = 0则标记为可回收,放入对应的回收队列中。
2.2 refcount > 0 则 refcount++(进行还原操作), 标记并放入新的缓冲区,待缓冲区满了之后进行3操作,去二次确认是否回收 (待缓冲区慢是怕多次调用gc算法影响性能)
3. 遍历refcount > 0 的缓冲区队列, 针对其含有的所有引用进行refcount--;
if 减少后的refcount == 0 则说明 包含循环引用, 标记并准备回收。
if 减少后的refcount > 0, 则进行还原操作, 认为暂时不可回收。
eg: 数组a, a[0] = &a, a[1] = &a, 数组a循环调用自身(此时a的refcount为2)。 利用3所述, 对数组a的引用a[0],a[1]分别进行 refcount-1, 此时发现a的refcount 减少到0, 则标记循环引用, 进行清除操作;
eg2: 对象a和b循环引用, a和b的refcount都为1, 对a的引用b做 refcount-1操作 , b中引用的a refcount-1 此时a和b的refcount都为0, 故为循环引用。。进行清除
4. php脚本执行完成后会回收对应的所有变量空间
go篇:
go的gc从最初版本就不断在改变。
最初版本的引用计数(如上所说,无法解决循环引用问题)
1.3版本 修改为标记-清除。 - 有点像gcroot,从root节点扫描查找, 扫描不到的统一清除。 最大的问题是stw(stop the world), 使用标记-清除时 程序必须暂停。 当程序量不断扩大时, 此gc会引起系统暂停很久。
1.5版本推出的三色法
1. 默认所有节点都是白色, 从root节点扫描查找,将所有可达的标记为灰色, 放入灰色队列中
2. 遍历灰色队列的每个节点, 将自身标记为黑色,并将所有的白色可达性节点标记为灰色, (广度优先遍历的思想)
3. 调用2 直到灰色队列为空( 此时引用的都为黑色, 非引用的都为白色)
4. 清除所有白色的节点,释放空间
5. 将所有黑色节点还原回白色
只需要在执行4和5的时候,暂停程序即可, 节省了停止程序的时间。 但面临的问题就是 在进行步骤2时, 尤其是黑色节点的引用在不断变化,会导致执行误清理和漏清理。
1.8版本改进,在三色法的基础上加入了写屏障(wirte barrier), 在引用发生变化&&自身是黑色节点, 则将新的引用变为灰色加入灰色队列。 减少了误清理, 但还是会有漏清理的情况。