GC 全称 Garbage Collection ,即:垃圾回收。javascript
写过计算机程序的都知道,程序中无时无刻伴不随着变量的引用,赋值,运算等操做。因而乎存在着某些变量在使用事后,程序不会再用到它们,可是他们依然占据着必定的内存空间。内存中这样不会被程序再次使用的数据即可称之为‘垃圾’。java
简而言之,回收就是将上面讲到的 程序运行时产生的‘垃圾’ 释放掉,以便于程序再次使用这块内存区域。算法
使人惊叹的是,目前全部的GC算法只不过是上述3种算法的组合或应用,也就是说从1963年赋值算法诞生时,到目前为止没有诞生新的GC算法!浏览器
首先咱们明确个知识点,GC在内存中销毁移动的基本单位是对象。
bash
对象由头(head)和 域(field)构成。
函数
对象中保存对象自己信息的部分称之为‘头’。head中的信息用户没法访问和修改,包含下列信息。优化
field 对于咱们而言比较熟悉,在JavaScript使用属性操做符即可以直接访问的部分被称之为域。域包含两种数据类型。ui
指的是修改CG对象之间的引用关系。更准确的来说它就是 ‘应用程序’自己,也就是咱们写的代码。
this
堆指的是用于动态(也就是执行程序时)存放对象的内存空间。当 mutator 申请存放对象时, 所需的内存空间就会从这个堆中被分配给 mutator。spa
也就是分配到内存空间中的对象中那些可以经过 mutator 引用的对象称之为 ‘活动对象’,反之为‘非活动对象’即:垃圾。
在CG世界中,根指的是对象指针的起点部分。也就是**mutator **(应用程序) 中的全局变量,经过递归这些根(全局变量)能够遍历到的对象就是活动对象,反之就是非活动对象(垃圾)。
如该算法的名称所描述,它的执行能够分为两个阶段,即:标记 和 清除。标记就是经过上面提到的根(全局对象)能遍历到的内存中的对象标记为活动对象,标记完成以后接下来就清除内存中没有被标记的对象,也就是非活动对象。经过这两个阶节阶段实现了内存空间的重复利用。
标记过程实际上能够经过伪代码来更加具体展示:
/*标记函数*/
mark_phase(){
// 遍历全局对象,即:根
for(r : $roots)
mark(*r)
}
/* 标记遍历到的对象,而后继续遍历该对象的引用对象。 标记过程采用的是深度优先算法 */
mark(obj){
// 遍历到已经设置为 true 的对象则再也不处理
if(obj.mark == FALSE)
obj.mark = TRUE
// 标记后继续遍历当前对象的引用对象,直至全部的活动对象都被遍历到
for(child : children(obj))
mark(*child)
}
复制代码
清除阶段的遍历过程是从堆的首地址开始,一个个的遍历对象的标志位。
伪代码以下所示:
sweep_phase(){
// $heap_start 指针指堆中的第一个对象的指针
sweeping = $heap_start
// 遍历堆时的边界控制,不能超出堆的大小
while(sweeping < $heap_end)
if(sweeping.mark == TRUE)
// 遇到标记为 true 的对象,则取消标志位,准备下一次GC
sweeping.mark = FALSE
else
// 这里就是关键的回收处理了,首先$free_list 就是‘空闲链表’,程序须要的内存就是从其中分配得到。
// 可能有小伙伴看不懂这里,就详细解释一下:
/*
$free_list 是指向‘空闲链表’的指针,将它赋值给要回收对象的 next 域,那么接下来这个要被回收
的对象就变成了‘空闲链表’的头,即:header,也就是这个对象被加入到了空闲链表中,接下来就会被
当作空闲空间来分配给应用程序。最后再将头指针赋给 $free_list 变量,也就是 $free_list 回指向
空闲链表,而后继续遍历……
*/
sweeping.next = $free_list
$free_list = sweeping
// size 保存着被回收对象的大小,这步操做实际上就是移动至堆中的下一个对象,进行回收操做。
sweeping += sweeping.size
}
复制代码
经过下图能够形象的看出完成一次 GC 回收事后堆的状态:
GC 被本质上来讲就是一种研究如何释放没法被引用对象的技术。那么,能够想到,若是让对象本身记录一下,它有没有被程序引用。这就是引用计数法。
new_obj()
函数和
update_ptr()
函数。
伪代码以下:
new_obj(size){
// 从‘空闲链表’中为程序分配空间
obj = pickup_chunk(size, $free_list)
// 分配空间失败
if(obj == NULL)
allocation_fail()
else
// 为分配成功的对象设置计数器,并初始化
obj.ref_cnt = 1
return obj
}
复制代码
伪代码以下:
update_ptr(ptr, obj){
// obj 对象的计数器增量操做
inc_ref_cnt(obj)
// ptr 指针原来执行的对象计数器减量操做
dec_ref_cnt(*ptr)
// ptr 指正指向 obj
*ptr = obj
}
// 该函数就是让指针 ptr 要指向 obj 对象,实际中的代码相似于:
/*
var a = {};
var b = {};
a 对象计数器自增,b 对象计数器自减,
b=a;
*/
复制代码
inc_ref_cnt() 函数伪代码实现:
// 这里很简单 ,就是计数器自增
inc_ref_cnt(obj){
obj.ref_cnt++
}
复制代码
dec_ref_cnt() 函数伪代码实现:
dec_ref_cnt(obj){
// obj 对象计数器自减
obj.ref_cnt--
// 若是计数器等于 0 ,也就是说 obj 对象变为垃圾了
if(obj.ref_cnt == 0)
// 那么被 obj 所引用的对象也应该作减量操做
for(child : children(obj))
dec_ref_cnt(*child)
// 将 obj 对象链接至空闲链表
reclaim(obj)
}
复制代码
循环引用,形成内存没法被回收
// 注意:例子仅用于描述场景,不符合真实环境状况
funciton Person(name,lover) {
this.name = name;
this.lover= lover;
}
let jxy = new Person("jxy");
let xxx = new Person("xxx")
jxy.lover = xxx;
xxx.lover = jxy;
xxx = null;
jxy = null;
// 当GC采用引用计数法管理内存的时候,在上面例子中虽然变量都被赋值为空,可是两个对象自己确相互
// 引用,这样就致使了内存没法被有效回收,即:内存泄露
复制代码
复制算法顾名思义,就是将堆中的全部活动对象复制到另一个空间,而后原来的空间所有回收掉。这样的好处就是防止出现内存的碎片化,易于随后为程序分配新的空间。能够形象的理解为下图:
咱们把原来的活动对象空间称之为 **From **空间,将要复制到的新空间称之为 To 空间。当 **From **空间被彻底占满时,GC 会将活动对象复制到 To 空间。复制完成后,该算法会把 From 空间和 To 空间互换,本次 GC 也就结束了。GC 复制算法概要以下图所示:
copying(){
// 用 $free 变量记录 To 空间的开始位置
$free = $to_start
// 遍历全部的根对象,使用 copy 方法将他们复制到 To 空间
for(r : $roots)
// 返回的 *r 是对象被复制到 To 空间后新的指针。
*r = copy(*r)
// 复制完成以后,交换 From 和 To 空间
swap($from_start, $to_start)
}
复制代码
**copy()**函数伪代码:
/* copy 函数将做为参数给出的对象复制,再递归复制其子对象 */
copy(obj){
// obj 对象的 tag 域用于标记是不是一个已经被赋值过的对象
if(obj.tag != COPIED)
// 使用 copy_data 方法具体来拷贝 obj 对象,同时传入To 空间地址,以及 obj d对象的大小
copy_data($free, obj, obj.size)
// 拷贝完成以后,标记一下,该对象已经被赋值过了. $free 变量指向新的 obj
obj.tag = COPIED
// 旧的 obj 对象的 forwarding 域保存新的 obj 对象的指针,用于后面将其赋给程序中原始的指向
obj.forwarding = $free
// $free 跳过已经被复制的 obj 的空间,指向 To 空间的空闲位置,方便下一次复制使用
$free += obj.size
// 递归复制 obj 对象的引用对象
for(child : children(obj.forwarding))
*child = copy(*child)
// 当拷贝完成以后返回新对象的指针
return obj.forwarding
}
复制代码
前面写了那么多,那么到底咱们经常使用的浏览器使用的是那种回收算法呢?我想这多是小伙伴们最关心的了。那么以咱们最喜好的 Chrome 为例,它使用的是多种回收算法的组合优化,而非某种单一算法。V8 的 GC 算法统称为分代垃圾回收算法,也就是经过记录对象的引用次数,将超过必定引用次数的对象划分为老年对象,剩下的称之为**新生代对象,而后分别对他们采用不一样到的垃圾回收算法。**那这样划分到底有什么优点呢,咱们知道程序中生成的大多数对象其实都是产生以后随即丢弃。如下面代码为例,函数内部生成了对象,在该函数执行完毕,出栈以后,包括函数自己以及它内部的变量都会马上成为垃圾:
// 该函数的执行上下文环境非全局做用域
function foo() {
var a = {c:1};
var c = {c:2};
}
复制代码
那么对于这种新生代对象来讲,回收就会变得很频繁,若是使用 GC 标记清除算法,那么就意味着每次清除过程须要处理不少的对象,会浪费大量的的时间。因而若是对新生代对象采用 GC 复制算法的只须要将活动对象复制出来,而后将整个 From 清空便可,无需再去遍历须要清除的对象,达到优化的目的。而针对老年对象则不一样,它们都有多个引用,也就意味着它们成为非活动对象的几率较小,也就能够理解为老年对象不会轻易变成垃圾。再进一步也就是老对象产生的垃圾不多,若是采用复制算法的话得不偿失,大量的老年对象被复制来复制去也会增长负担,因此针对老年对象采用的是标记清除法,须要清除的老年对象只是少数,这样标记清除算法会更有优点**。**还有随着程序的执行新生代的对象会变成老年对象,这个具体过程比较复杂,小的能力有限,这里也就一笔带过了。既然对象分为新生带对像和老年对象,那么它们在堆中是如何分布的呢,请看下图: