内容存储的方式: 变量会在栈中存储,对象在堆中存储
let a = { name: “jiusi” };
a = [1, 2, 3, 4, 5];
引用计数算法
策略:跟踪记录每个变量值被引用的次数
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候,这个值的引用次数就为1
- 如果同一个值又被赋值给另一个变量,那么引用次数+1
- 如果该变量的值被其他的值覆盖了,则引用次数-1
- 当这个值得引用次数变为0的时候,说明没有变量在使用,这个值就无法访问,回收空间,垃圾回收器会在运行的时候清理掉引用次数为0得值所占用得那部分内存空间
let test = { name: "zhangsan" };
test = [1, 2, 34, 5];
let a = new Object(); // 1 + 1 - 1 - 1 = 0
let b = a;
a = null;
b = null;
优点:
- 对比标记清除法更清晰,首先引用计数在引用值为0的时候,也就是再变成垃圾的那一刻就会被回收,即可以立即回收垃圾
- 标记清除算法,需要每隔一段时间执行一次,那么在应用程序(JS脚本)运行过程中,线程就必须展厅去执行一段时间的GC
- 标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了
缺点: - 首先需要一个计数器,因为不知道被引用数量的上限值,此计数器需要占据很大的空间
- 无法解决循环引用问题(以及JavaScript 中不恰当的闭包写法)
// 循环引用
let c = new Object(); // +1
let d = new Object(); // +1
c.d = d; // d的值: +1
d.c = c; // c的值: +1
c = null; // c的值: -1
d = null; // d的值: -1
// 此时变量值的引用次数:
// c:1+1-1 = 1
// d:1+1-1 = 1
// GC c d 不为空 内存中变量无法被释放
标记清除算法(Mark-Sweep)
- 目前在jsvascript引擎中是最常用的,到目前为止得大多数浏览器得javascript引擎都采用标记清除算法,各大浏览器厂商对此算法进行了优化加工,且不同浏览器得jsvascript引擎在运行垃圾回收得频率上有所差异
- 当变量进入执行环境时,反转某一位(通过一个二进制字符来表示标记),又或者可以维护进入环境变量和离开环境变量这样两个列表,可以自由的把变量从一个列表转移到另一个列表
- 引擎在执行 GC(使用标记清除算法)时,需要从出发点去遍历内存中所有的对象去打标记,而这个出发点有很多,我们称之为一组根对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象、文档DOM树
此算法主要分为 标记 和 清除 阶段:
- 标记阶段: 为所有的活动对象做上标记
- 清除阶段:把没有标记(即非活动对象)销毁
整体过程:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
- 然后从各个根对象开始遍历,把不是垃圾的节点改成1
- 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收
优点:实现比较简单,打标记就是打与不打,所以一位2进制数(0和1)就能完成标记
缺点:在清楚后,剩余对象在内存中的位置不会变更,也会导致空闲空间不连续,即会导致内存碎片化,并且由于剩余空间得内存不是一整块,它是由不同大小的内存组成的列表,由此便产生了内存分配的问题
假设我们新建对象分配内存时需要大小为 size,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size 的块才能为其分配
三种分配策略:
- first-fit 找到大于等于 size 的空间返回
- Best-fit 遍历所有空闲列表 返回大于等于 size 的最小分块
- Worst-fit- ,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回
注意: 这三种策略里面 Worst-fit 的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fit 和 Best-fit 来说,考虑到分配的速度和效率 First-fit 是更为明智的选择
综上所述,标记清除算法或者说策略就有两个很明显的缺点:
- 内存碎片化:空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
- 分配速度慢:因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
解决方案: 而 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
V8引擎
V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收
1. 新生代
新加入对象时: 会被存储在使用区。当使用区快被写满时,执行GC
- 新生代垃圾回收器会对使用区的活动对象进行标记,标记完成后然后活动对象将会被复制到空闲区并进行排序
- 垃圾清理阶段开始即将非活动对象占据的空间清理掉
- 最后角色互换,将原来的使用区变为空闲区,将原来的空闲区变为使用区
注意:
- 如果一个对象经过多次复制后依然存活,那么会被认定为是生命周期较长的对象,且会被移动到老生代中进行管理
- 如果复制一个对象到空闲区时,空闲区的空间占用超过25%,那么这个对象对被直接放入到老生代空间中(25%的比例设置时为了避免影响后续内存分配,因为当按照Scavenge算法回收完成后,空闲区将翻转成使用区,继续进行对象的内存分配)
Scavenge算法:
Scavenge 算法是一种基于copy的垃圾回收算法,它是最简单实用的一种模型,被广泛地使用在各类语言虚拟机中,例如JVM中的Scavenge算法就是以Scavenge算法为基础的改良版本。Scavenge算法的基本思想是把某个空间里的活跃对象复制到其他空间,把原来的空间全部清空,这就相当于是把活跃的对象从一个空间搬到新的空间。在Scavenge算法中,有两个主要的空间区域:From空间和To空间。From空间用于分配新的对象,而To空间则作为幸存者空间,用于存放从From空间复制过来的活跃对象。当From空间快被写满时,就需要执行一次垃圾清理操作,即对From空间中的对象进行标记,然后将存活的对象复制到To空间中,同时整理To空间中的对象顺序,以消除内存碎片。这种算法的实现简单且有效,能够快速回收不再使用的内存,保持系统的运行效率
标记方法:全停顿标记
- 虽然我们的 GC (垃圾回收 garbage collection)操作被放到了主进程与子进程中去处理,但最终的结果还是主进程被较长时间占用
- 因为js是单线程的,所以GC的过程和执行 js线程的只能执行一个
回收方法:并行回收
为了减少主线程阻塞,我们在进行 GC 处理时,使用辅助进程,GC过后还会有一个merge的过程
2. 老生代
- 不同于新生代,老生代中存储的内容是相对使用频繁并且短时间无需清理回收的内容。这部分我们可以使用标记整理进行处理。
- 从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象
- 清除阶段老生代垃圾回收器会直接将非活动对象进行清除。
标记方法:切片标记(增量标记)
增量就是将一次 GC 标记的过程,分成了很多小步,每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成一轮 GC 标记
- 每次标记只标记一部分内容,直到标记结束
- 对象可能会只标记一部分,整个对象并没有标记结束(解决策略: 三色标记法)
三色标记法(黑白灰)
- 白色指的是未被标记的对象
- 灰色指自身被标记,成员变量(该对象的引用对象const name = obj.name)未被标记
- 黑色指自身和成员变量皆被标记
写屏障(增量中修改引用)
这一机制用于处理在增量标记进行时修改引用的处理,可自行修改为灰色
回收方法:惰性清理
增量标记只是用于标记活动对象和非活动对象,真正的清理释放内存,则 V8 采用的是**惰性清理(Lazy Sweeping)**方案
在增量标记完成后,进行清理。当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕。
总结
分代式机制:
新生代:新、小、存活时间短的对象,采用一小块内存频率较高的快速清理
老生代:老、大、存活时间长的对象,使其很少接受检查
新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率