V8 的内存管理模式
一个运行的程序通常是通过在内存中分配一部分空间来表示的。这部分空间被称为驻留集(Resident Set)。 V8 的内存管理模式有点类似于Java 虚拟机(JVM),它会将内存进行分段:
- 代码 Code:实际被执行的代码
 - 栈 Stack:包括所有的携带指针引用堆上对象的值类型(原始类型,例如整型和布尔),以及定义程序控制流的指针。
 - 堆 Heap:用于保存引用类型(包括对象、字符串和闭包)的内存段
 

V8 的垃圾回收机制
分代式垃圾回收
V8 的垃圾回收策略主要基于「分代式垃圾回收机制」,基于这个机制,V8 把内存分为「新生代(New Space)」和 「老生代 (Old Space)」。
新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。
设置老生代内存空间的最大值选项 --max-old-space-size,设置新生代内存空间的大小选项 --max-new-space-size

新生代中的垃圾回收
在新生代中,主要通过 Scavenge 算法进行垃圾回收。
Scavenge
在 Scavenge 算法中,它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另外一个处于闲置状态。处于使用状态的 semispace 称为 From 空间,处于闲置状态的 semispace 称为 To 空间。当我们分配对象时,先是从 From 空间中分配。当开始进行垃圾回收时,会检查 From 空间中存活的对象,这些存活的对象会被复制到 To 空间中,而非存活的对象占用的空间会被释放。完成复制后,From 空间和 To 空间角色互换。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个 semispace 空间之间进行复制。

在新生代中的对象怎样才能到老生代中?
在新生代存活周期长的对象会被移动到老生代中,主要符合两个条件中的一个:
1. 对象是否经历过 Scavenge 回收。
对象从 From 空间中复制到 To 空间时,会检查它的内存地址来判断这个对象是否已经经历过一次 Scavenge 回收,如果已经经历过了,则将该对象从 From 空间中复制到老生代空间中。
2. To 空间的内存占比超过 25%限制。
当对象从 From 空间复制到 To 空间时,如果 To 空间已经使用超过 25%,则这个对象直接复制到老生代中。这么做的原因在于这次 Scavenge 回收完成后,这个 To 空间会变成 From 空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
老生代中的垃圾回收
对于老生代的对象,由于存活对象占比较大比重,使用 Scavenge 算法显然不科学。一来复制的对象太多会导致效率问题,二来需要浪费多一倍的空间。所以,V8 在老生代中主要采用「Mark-Sweep」算法与「Mark-Compact」算法相结合的方式进行垃圾回收。
Mark-Sweep
Mark-Sweep 是标记清除的意思,分为标记和清除两个阶段。在标记阶段遍历堆中的所有对象,并标记存活的对象,在随后的清除阶段中,只清除标记之外的对象。

但是 Mark-Sweep 有一个很严重的问题,就是进行一次标记清除回收之后,内存会变得碎片化。如果需要分配一个大对象,这时候就无法完成分配了。这时候就该 Mark-Compact 出场了。
Mark-Compact
Mark-Compact 是标记整理的意思,是在 Mark-Sweep 基础上演变而来。Mark-Compact 在标记存活对象之后,在整理过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。

Incremental Marking
鉴于 Node 单线程的特性,V8 每次垃圾回收的时候,都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复应用逻辑,被称为「全停顿」。在分代垃圾回收中,一次小垃圾回收只收集新生代,且存活对象也相对较少,即使全停顿也没有多大的影响。但是在老生代中,存活对象较多,垃圾回收的标记、清理、整理都需要长时间的停顿,这样会严重影响到系统的性能。所以「增量标记 (Incrememtal Marking)」被提出来。它从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记,拆分为许多小「步进」,每做完一「步进」就让 JavaScript 应用逻辑执行一小会,垃圾回收与应用逻辑这样交替执行直到标记阶段完成。
导致内存泄漏的原因
- 全局变量的使用
 - 定时器或者回调函数
 - 代码中存在大量未使用的对象的引用
 - 闭包
 
全局变量的使用
正常来说,javascript 允许使用未声明的变量,例如在函数中未用 var 关键字声明的变量,默认会被定义为全局变量,例如
function foo(arg) {  bar = '全局变量';}// 相当于function foo(arg) {  window.bar = '全局变量';}
          如果用 this.bar 创建,但是在全局下访问这个 foo 函数也会导致 bar 成为一个全局变量
function foo() {  this.variable = '全局转变';}// 在全局的环境下调用foo, this指向windowfoo();
          防止这种书写错误的方法就是在文件声明 use strict  关键字
定时器或者回调函数
例如我们经常使用 setInterval 实现类似下面的方法
var someResource = getData();setInterval(function() {  var node = document.getElementById('Node');  if(node) {      node.innerHTML = JSON.stringify(someResource));  }}, 1000);
          上面的例子中,setInterval 函数保持这对 node 和 someResource 的引用,由于处理函数没法没回收,所以他们的引用也没法被回收,如果 someResource 存储着大量数据,必然会占用内存,无法回收内存,唯一处理就是把移除这些回调
var itv = setInterval(function() {    var node = document.getElementById('Node');    if(node) {        node.innerHTML = JSON.stringify(someResource));        clearInterval(itv);    }}, 1000);
          值得庆幸的是,现在很多浏览器的垃圾回收算法都可以帮我们避免这些问题!
dom 引用
var elements = {  button: document.getElementById('button'),  image: document.getElementById('image'),  text: document.getElementById('text'),};function doStuff() {  image.src = 'http://some.url/image';  button.click();  console.log(text.innerHTML);  // Much more logic}function removeButton() {  // The button is a direct child of body.  document.body.removeChild(document.getElementById('button'));  // At this point, we still have a reference to #button in the global  // elements dictionary. In other words, the button element is still in  // memory and cannot be collected by the GC.}
          闭包
javascript 的一个特性是闭包,通过闭包可以在某一块内存中保持着对各自的环境变量的引用,导致内存无法被 gc 回收,下面是一个典型的例子
var theThing = null;var replaceThing = function () {  var originalThing = theThing;  var unused = function () {    if (originalThing) console.log('hi');  };  theThing = {    longStr: new Array(1000000).join('*'),    someMethod: function () {      console.log(someMessage);    },  };};setInterval(replaceThing, 1000);
          上面例子中,每秒执行 replaceThing 函数,这个函数里面声明 originalThing 指向 theThing 对象,每次执行 theThing 赋予一个巨大的数组,相当于 originalThing 也引用这个有巨大的数组的对象,在 theThings 里面有一个someMethod 方法,当闭包创建是,他们使用同一个父作用域,所以 unused 也共享同一个作用域,而 unused 方法保持着对 originalThing 这巨大对象的引用,导致 gc 无法回收,由于每次执行都开辟新的内存保持着对这巨大数组的引用,导致内存一直上涨,最后导致内存泄漏