垃圾回收又称为 GC(Garbage Collecation)。编写 JavaScript 程序时,开发者不须要手工跟踪内存的使用状况,只要按照标准写 JavaScript 代码,JavaScript 程序运行所需内存的分配以及无用内存的回收彻底是自动管理。JavaScript 中自动垃圾回收机制的原理为:javascript
找出那些再也不使用的变量,而后释放其占用的内存。
垃圾收集器会按照固定的时间间隔(或预约的收集时间)周期性地执行此操做。html
局部变量只在函数执行的过程当中存在。
在函数执行过程当中,会为局部变量在栈内存(或 堆内存)上分配相应的空间来存储它们的值。在函数中使用这些变量,直至函数执行结束,此时能够释放局部变量的内存供未来须要时使用。
以上状况下,较容易判断变量是否有存在的必要,更复杂的状况须要更精细的变量追踪策略。
JavaScript 中的垃圾收集器必须跟踪每一个变量是否有用,须要为再也不有用的变量打上标记,用于未来回收其占用的内存。标识无用变量的策略一般有两个:标记清除 和 引用计数 。java
上述过程当中,JavaScript 中变量分为 基本类型值 和 引用类型值:python
标记清除(mark-and-sweep) 是 JavaScript 中最经常使用的垃圾回收方式。其执行机制以下:git
逻辑上,永远不能释放进入环境的变量所占用的内存,由于执行流进入相应的环境时,可能会用到它们。
标记变量的方式有不少种,可使用标记位的形式记录变量进入环境,也可单独为“进入环境”和“离开环境”添加变量列表来记录变化。github
标记清除采用的收集策略为:算法
2008年以前,IE、Firefox、Opera、Chrome 和 Safari 的 JavaScript实现使用的均为 标记清除式的垃圾回收策略,区别可能在垃圾收集的时间间隔。segmentfault
引用计数(reference counting) 是另外一种垃圾收集策略。引用计数的本质是 跟踪记录每一个值被引用的次数。其执行机制以下:数组
垃圾收集器下次运行时,会释放那些引用次数为零的值所占用的内存。
引用计数存在一个致命的问题: 循环引用。循环引用是指,对象 A 中包含一个指向对象 B 的指针,而对象 B 中也包含一个指向对象 A 的引用。下面的代码就是标准的循环引用的例子:浏览器
function cycleRefernce() { var objectA = new Object(); var objectB = new Object(); objectA.someOtherObject = objectB; objectB.anotherObject = objectA; } 复制代码
上述例子中 objectA 和 objectB 经过各自属性相互引用。按照引用计数的策略,两个对象的引用次数均为 2。若采用标记清除策略,函数执行完毕,对象离开做用域就不存在相互引用。但采用引用计数后,函数执行完,两个对象的引用次数永不为0,会一直存尊内存中,若屡次调用,致使大量内存得不到回收。
IE8浏览器 以前中有一部分对象并非原生的 JavaScript 对象,多是使用 C++ 以 COM 对象的形式实现的(BOM, DOM)。而 COM 对象的垃圾收集机制采用的是 引用计数策略。即便 IE 的 JavaScript 引擎是使用标记清除策略实现的,但 JavaScript 访问 COM 对象仍然是基于 引用计数策略的。在这种状况下,只要在 IE 中涉及 COM 对象,就可能存在循环引用的问题。
为避免出现循环引用,最好在不使用这些对象时,手动断开 原生 JavaScript 对象 与 DOM 元素之间的链接。IE中的循环引用与手动断开的操做以下所示:
var element = document.getElementById("some_element"); var myObject = new Object(); myObject.element = element; element.someObject = myObject; // 以上 存在循环引用 // ...... // 如下 手工断开链接 myObject.element = null; element.someObject =null; 复制代码
将变量设置成 null 便可切断变量与它以前引用的值之间的链接。下次垃圾收集器运行时,会删除这些值并回收它们占用的内存。
为解决上述问题,IE9及以上版本把 BOM 和 DOM 对象都转换成了真正的 JavaScript 对象,避免了两种垃圾回收算法并存引发的问题。
垃圾收集器是周期运行的,肯定 垃圾收集的时间间隔 是个重要的问题。
IE7以前的垃圾收集器是根据内存分配量运行的,即 256 个变量、4096 个对象(数组)字面量或 64 KB 的字符串。达到这些临界值的任何一个,垃圾收集器就会运行。因此就致使若是一个脚本含有不少变量,在整个生命周期中一直保有前面临界值大小的变量,就会频繁触发垃圾回收,会存在严重的性能问题。
IE7 重写了垃圾收集例程。新的工做方式为:触发垃圾收集的变量分配、字面量和数组元素的临界值被调整为 动态修正。初始值与以前版本相同,但若是垃圾收集例程回收的内存低于 15%,则临界值加倍。若回收内存分配量超过 85%,则临界值重置回默认值。
在JavaScript脚本中,绝大多数对象的生存期很短,只有部分对象的生存期较长。因此,V8 中的垃圾回收主要使用的是 分代回收 (Generational collection)机制。
V8 引擎将保存对象的 堆 (heap) 进行了分代:
大周期进行的并不频繁。一次大周期一般是在移动足够多的对象至老生区后才会发生。
因为垃圾清理发生的比较频繁,清理的过程必须很快。V8 中的清理过程使用的是 Scavenge 算法,按照 经典的 Cheney 算法 实现的。Scavenge 算法的主要过程是:
算法的伪代码描述以下:
def scavenge(): swap(fromSpace, toSpace) allocationPtr = toSpace.bottom scanPtr = toSpace.bottom for i = 0..len(roots): root = roots[i] if inFromSpace(root): rootCopy = copyObject(&allocationPtr, root) setForwardingAddress(root, rootCopy) roots[i] = rootCopy while scanPtr < allocationPtr: obj = object at scanPtr scanPtr += size(obj) n = sizeInWords(obj) for i = 0..n: if isPointer(obj[i]) and not inOldSpace(obj[i]): fromNeighbor = obj[i] if hasForwardingAddress(fromNeighbor): toNeighbor = getForwardingAddress(fromNeighbor) else: toNeighbor = copyObject(&allocationPtr, fromNeighbor) setForwardingAddress(fromNeighbor, toNeighbor) obj[i] = toNeighbor def copyObject(*allocationPtr, object): copy = *allocationPtr *allocationPtr += size(object) memcpy(copy, object, size(object)) return copy 复制代码
若是新生区有某个对象,只有一个指向它的指针,刚好该指针在老生区的对象中,在垃圾回收以前咱们如何得知新生区的该对象是活跃的呢?
为解决此问题,V8 在写缓冲区有一个列表,其中记录了全部老生区对象指向新生区的状况。新生区对象诞生时不会有指向它的指针,当老生区的对象出现指向新生区对象的指针时,便记录跨区指向,记录行为老是发生在写操做中。
由于新生区的内存通常都不大,因此使用 Scavenge 算法进行垃圾回收效果比较好。老生区通常占用内存较大,所以采用的是 标记-清除(Mark-Sweep)算法 与 标记-紧缩(Mark-Compact)算法。
两种算法都包括两个阶段:标记阶段,清除或紧缩阶段。
在标记阶段,堆上全部的活跃对象都会被发现而且标记。
标记算法的核心是 深度优先搜索,具体过程为:
- 在标记的初期,位图是空的,全部对象也都是白的。
- 从根可达的对象会被染色为灰色,并被放入标记用的一个单独分配的双端队列。
- 标记阶段的每次循环,GC会将一个对象从双端队列中取出,染色为黑,而后将它的邻接对象染色为灰,并把邻接对象放入双端队列。
- 这一过程在双端队列为空且全部对象都变黑时结束。
- 特别大的对象,如长数组,可能会在处理时分片,以防溢出双端队列。若是双端队列溢出了,则对象仍然会被染为灰色,但不会再被放入队列(这样他们的邻接对象就没有机会再染色了)。
- 所以当双端队列为空时,GC仍然须要扫描一次,确保全部的灰对象都成为了黑对象。对于未被染黑的灰对象,GC会将其再次放入队列,再度处理。
标记算法结束后,全部的活跃对象都被染成黑色,全部的死对象还是白的。下一步就能够清除或者紧缩了。
标记算法执行后,能够选择清除 或是紧缩,这两个算法均可以收回内存,并且二者都做用于页级(V8 中的内存页是 1MB 的连续内存块)
清除算法扫描连续存放的死对象,将其变为空闲空间,并将其添加到空闲内存链表中。清除算法只须要遍历页的位图,搜索连续的白对象。[每一页都包含数个空闲内存链表,其分别表明小内存区(<256字)、中内存区(<2048字)、大内存区(<16384字)和超大内存区(其它更大的内存)]
紧缩算法会尝试将对象从碎片页(包含大量小空闲内存的页)中迁移整合在一块儿,来释放内存。这些对象会被迁移到另外的页上,所以也可能会新分配一些页。而迁出后的碎片页就返还给操做系统。
对目标碎片页中的每一个活跃对象,在空闲内存链表中分配一块其它页的区域,将该对象复制至新页,并在碎片页中的该对象上写上转发地址。
迁出过程当中,对象中的旧地址会被记录下来,这样在迁出结束后V8会遍历它所记录的地址,将其更新为新的地址。因为标记过程当中也记录了不一样页之间的指针,此时也会更新这些指针的指向。
对于一个堆很大,活跃对象有不少的脚本时,标记-清除 与 标记-紧缩 的效率可能会很慢,为减小垃圾回收引发的停顿,引入了 增量标记(Incremental marking) 和 惰性清理(lazy sweeping)。
增量标记容许堆的标记(前面的标记阶段)发生在几回5-10毫秒的小停顿中。增量标记在堆的大小达到必定的阈值时启用,启用以后每当必定量的内存分配后,脚本的执行就会停顿并进行一次增量标记。就像普通的标记同样,增量标记也是一个深度优先搜索,并一样采用白灰黑机制来分类对象。
增量标记与普通标记的区别是,添加了从黑对象到白对象的指针,为此须要再次启用写屏障中,在记录 老->新 的同时,记录 黑->白。在进行清除时,一旦在写屏障中发现这样的指针,黑对象会被从新染色为灰对象,从新放回到双端队列中。
惰性清理是指在标记完成后,并不急着释放空间,无需一次清理全部的页,垃圾回收器会视状况逐一清理,直到全部页都清理完成。
余下的涉及垃圾回收原理的部分留着后面继续整理。(平行标记 与 并发标记)
此文章最初记录在本人2019.02.21的博客 JavaScript垃圾回收机制中,如今转出来供你们一块儿学习交流,若有不许确的地方请你们批评指正。