本人在面试候选人的时候,即便一个刚毕业的前端,问他 javascript 中内存的分配,都能答出来栈内存、堆内存。可是再追问一下,堆内存到底是怎么分配的,80% 的面试者都回答不上来了。javascript
都知道,js对象是存放在堆内存中的,那么具体是放在哪里呢?前端
V8 引擎会把内存中的 堆内存
分为两块不一样的区域,一块称之为老生代(old generation),另外一块是新生代(young generation)。java
即便同处 新生代 中的对象中,它们的等级也不一样,又进一步分为 初级(nursery)代 等级和 中级(intermediate)代 等级。程序员
能够类比红警这类游戏中,刚出生的美国大兵是一级兵,经历过一场激烈的战役以后,幸存的大兵会被提高到二级兵,再经历一场战役,会被提高到三级兵。面试
js 的初级代、中级代也是如此。编程
在 js 中,当一个对象第一次分配内存时,会被分配到 新生代 中的 初级(nursery)代,至关因而最弱鸡的一级兵。浏览器
这个对象,若是在第一轮的垃圾回收中幸存下来。那么,咱们把它的等级到 中级(intermediate)代,也就是晋升成为了 二级兵。多线程
若是再通过下一次垃圾回收,这个对象幸存下来,这时候咱们就会把这个对象,从中级(intermediate)代移动到老生代,也就是晋升成为了三级兵。并发
为啥 V8 要这么作呢?编程语言
在垃圾回收中有一个重要的概念:“代际假说”(The Generational Hypothesis)。就是说,大部分的 js 对象,都是炮灰,一轮垃圾回收后,基本上都不会幸存,在内存中存在的时间很短。换句话说,从垃圾回收的角度来看,不少对象一经分配内存空间随即就变成了不可访问的。如同下图所示:
既然,短命的js对象,和命久的 js对象有如此的差距,V8 中就把他们区分开,采用不一样的垃圾回收策略。
当 javascript
主线程在正常的执行的时候,占用的内存空间会不断的增加。增加就会触发一个极限,触发极限的时候,垃圾回收就被触发了。对于新生代和老生代, V8 分别有两种垃圾回收器去处理。
V8 有两个垃圾回收器,一个是主垃圾回收器(Full Mark-Compact),一个是副垃圾回收器( Scavenge )。这两个垃圾回收器,是相互独立的。
主垃圾回收器主要负责老生区中的垃圾回收(也会负责一部分的新生代heap),副垃圾回收器重新生代中回收垃圾。
From-Space
/ To-Space
对于副垃圾回收器来讲,有两块内存空间比较相关: From-space
和 To-space
。
不要感到困惑,From-space
和 To-space
的概念,和 初级代
和 中级代
的概念,不是同一个概念。
From-space
和To-space
能够理解成,真实可操做的内存空间;初级代
和中级代
表示一个对象的等级。From-Space
和To-Space
永远是一个是空的,一个是使用中的。即便同处在From-Space
中的对象,有的对象多是初级代(一等兵),有的对象多是中级代(二级兵)。
接下来,介绍一下,副垃圾回收器的过程。
这一步,是为了判断这轮GC中哪些对象须要被回收。
如何判断呢?就是看这个对象能不能被找到。
打标首先从根部开始查找,也就是顶层的执行栈、全局的对象开始查找,而后查找对象的引用,而后是对象引用的引用,一层层递归的找。
若是一个对象能够被访问到,则认为这个对象是活的,不该该被回收;不然,就会被回收。
接下来,咱们开始一轮垃圾回收的过程。
第 2 步,V8 从 from-space
中,把那些不会被垃圾清理掉的对象,移动到 to-space
。这意味着,只有小部分的对象不会被垃圾清理掉,成功撤离到 to-space
。 剩在 from-space
中的大部分对象,当成炮灰销毁。
不要和 js 代码中的拷贝对象指针搞混了,这里是更底层的二进制数据。拷贝的过程,就是拷贝那一块内存中的二进制数据的过程。
这一步也称为 撤离步骤 evacuation step,这步结束后,内存中的状况以下图:
上图右侧,撤离到 to-space
中的对象,成功的在第一轮的垃圾回收中活了下来,给他们的小方块打上一个标记(也就是每一块的小圆圈),标志着它们从 初级代
晋升到了 中级代
。
接下来,第3步被称为更新引用指针。
咱们发现,js 中对象的引用指针,仍是引用到了旧的from-space
空间上,咱们须要更新这些引用到 to-space
空间上。
结束后,以下图所示:
接下来,咱们把 to-space
和 from-space
交换位置。to-space
移动到左侧,成为了 下一轮的 from-space
,from-space
移动到右侧,成为了下一轮的 to-space
第一轮垃圾回收以后, js 继续执行,会有一些新分配的 初级代
对象,被推入到了 from-space
空间中, 安置在上一轮的幸存老兵 中级代
对象后面,以下图红色箭头所示:
第二轮垃圾回收的过程,和第一轮相似,就不赘述了。
二轮的垃圾回收的关键点是:from-space
中的幸存老兵 中级代
会拷贝最右侧的 old generation
, 晋升为 老生代
。刚加入的新兵 初级代
会被拷贝到 to-space
, 也打上一个标志,晋升成为 中级代
, 以下图所示:
现今,V8 在新生代垃圾回收中使用并发清理。
什么? 并发?
对,你没有听错。虽然 javascript
是单线程的语言,这仅仅意味着 javascript
的程序员写的代码大部分是在 单线程上面跑的。可是 javascript
语言的宿主环境,好比说 V8 引擎,它是 Javascript
的执行环境,它能够新建出不少线程出来,用来辅助 javascript
主线程的工做。咱们把这些其余的辅助的线程称为 辅助线程(helper)
, javascript 执行的线程是 主线程(main thread)
。
把幸存对象撤离到 to-space
的工做,是 主线程 和 辅助线程一块儿并发执行,是为了d最大限度的减小 GC 的时间。
每一个辅助线程 和 主线程,会把活的对象都移动到 To-Space
。在每一次尝试将活的对象移动到 To-Space
的时,必须确保原子操做。
不一样的辅助线程,都有可能经过不一样的路径找到相同的对象,并尝试将这个对象移动到 To-Space
;不管哪一个辅助线程成功移动对象到 To-Space
,都必须更新这个对象的指针。
由于代际假说的理论,只有小部分的 js 对象是会幸存下来的,因此在副垃圾回收器中,只会撤离一小部分的对象,拷贝到to-space
的空间中,其余大部分对象都通通销毁。
from-space
和 to-space
只有一个在用,空间开销很大,典型的用空间换时间。
辅助线程 并发的帮助撤离
上文中提到,新生代的对象,若是连续二轮GC幸存,会被晋升到老生代。
接下来,咱们来看一下,老生代的对象是如何被 主垃圾回收器所处理的。
老生代的垃圾回收会经历下面几个过程:打标 ( marking ) 、 清扫 (sweeping) **、压缩 (compacting)
打标的过程, 在上文副垃圾回收器中已经讲过了
打标以后,V8 知道有哪些对象是不会被访问到,也就是须要被回收的了。这些被回收的对象所占用的位置,人走茶凉,就空了下来,成为了一个空闲的位置。
V8 会管理这些空闲的位置,以便下次有新到对象来了,能够把新到对象安置在空间位置中。
V8 把这些空闲的位置,扫到一张叫 FreeList
的表中来记录,这个过程被称为清扫,清扫的过程可让一个辅助线程在后台静默的去作掉。
若是你了解计算机操做系统,必定了解 碎片
的概念。
压缩的意思是,咱们想把 内存中的数据,挤一挤,靠得紧凑一点,把他们中间的间隙——也就是碎片
,合并成一个大一些的连续空间。这样下次来一个比较大的对象时,能够有充足的空间来存放。
在主垃圾回收器中,一样存在多辅助线程来提高效率。
首先, 辅助线程
开始并发的去打标(marking)
接下来, 当 辅助线程(helper)
的工做作完了,主线程
就会暂停执行,转而进行最后的打标记工做(finalize marking)和开始清理工做 ( sweeping tasks )。
最后的打标记工做是主线程会快速的从根部从新检查一下,看有 辅助线程
是否有遗漏的,确保全部的对象都正确的扫过了
若是检查完毕OK,主线程和一部分辅助线程
齐心合力一块儿作 合并碎片(Compact)
、更新(update)
的操做。
另一部分辅助线程,会去并发的执行 清扫
工做,并不会影响并行内存页的整理工做和 JavaScript 的执行。
当这些工做都作完了,主线程会从新开始执行代码。
对于 JavaScript 程序员来讲,咱们是没有办法直接操做垃圾回收器的。
为了解决这个问题, V8 提出了空闲时间的概念。咱们的页面跑在浏览器内,浏览器以每秒60帧的速度去执行一些动画,浏览器大约有16.6毫秒的时间去渲染动画的每一帧。
若是这些渲染的工做,提早完成了,那么浏览器在下一帧以前的空闲时间去触发垃圾回收器。
V8 的垃圾回收器项目自立项以来已经走过了漫长的道路。向现有的垃圾回收器添加并行、并发和增量垃圾回收技术通过了不少年的努力,而且也已经取得了一些成效。
将大量的移动对象的任务转移到后台进行,大大减小了主线程暂停的时间,改善了页面卡顿,让动画,滚动和用户交互更加流畅。Scavenger 回收器将新生代的垃圾回收时间减小了大约 20% - 50%,空闲时垃圾回收器在 Gmail 网页应用空闲的时候将 JavaScript 堆内存减小了 45%。并发标记清理能够减小大型 WebGL 游戏的主线程暂停时间,最多能够减小 50%。
大部分 JavaScript 开发人员并不须要考虑垃圾回收,可是了解一些垃圾回收的内部原理,能够帮助你了解内存的使用状况,以及采起合适的编范式。好比:从 V8 堆内存的分代结构和垃圾回收器的角度来看,建立生命周期较短的对象的成本是很是低的,可是对于生命周期较长的对象来讲成本是比较高的。这些模式是适用于不少动态编程语言的,而不只仅是 JavaScript。