JS垃圾回收机制笔记

直到不久以前,对于JS的垃圾回收机制,还停留在‘所分配的内存再也不须要’的阶段。问题来了,浏览器是怎么肯定‘所分配的内存再也不须要’了呢?javascript

  • 内存简介
  • 垃圾回收简介

内存简介

MDN:像C语言这样的高级语言通常都有底层的内存管理接口,好比 malloc()和free()。另外一方面,JavaScript建立变量(对象,字符串等)时分配内存,而且在再也不使用它们时“自动”释放。 后一个过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其余高级语言)开发者感受他们能够不关心内存管理。 这是错误的。java

内存生命周期

  1. 分配你所须要的内存
  2. 使用分配到的内存(读、写)
  3. 不须要时将其释放\归还

JavaScript内存分配

为了避免让程序员费心分配内存,JavaScript 在定义变量时就完成了内存分配。node

值的初始化

var n = 123; // 给数值变量分配内存
var s = "azerty"; // 给字符串分配内存

var o = {
  a: 1,
  b: null
}; // 给对象及其包含的值分配内存

// 给数组及其包含的值分配内存(就像对象同样)
var a = [1, null, "abra"]; 

function f(a){
  return a + 2;
} // 给函数(可调用的对象)分配内存

// 函数表达式也能分配一个对象
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);
复制代码

经过函数调用分配内存

有些函数调用结果是分配对象内存:程序员

var d = new Date(); // 分配一个 Date 对象

var e = document.createElement('div'); // 分配一个 DOM 元素
复制代码

有些方法分配新变量或者新对象

var s = "azerty";
var s2 = s.substr(0, 3); // s2 是一个新的字符串
// 由于字符串是不变量,
// JavaScript 可能决定不分配内存,
// 只是存储了 [0-3] 的范围。

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// 新数组有四个元素,是 a 链接 a2 的结果
复制代码

使用值

使用值的过程其实是对分配内存进行读取与写入的操做。读取与写入多是写入一个变量或者一个对象的属性值,甚至传递函数的参数。算法

当内存再也不须要时释放

MDN:大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经再也不须要了”。它每每要求开发人员来肯定在程序中哪一块内存再也不须要而且释放它。chrome

高级语言解释器嵌入了“垃圾回收器”,它的主要工做是跟踪内存的分配和使用,以便当分配的内存再也不使用时,自动释放它。这只能是一个近似的过程,由于要知道是否仍然须要某块内存是没法断定的(没法经过某种算法解决)数组


垃圾回收机制策略简介

引用概念

垃圾回收算法主要依赖于引用的概念。浏览器

在内存管理的环境中,一个对象若是有访问另外一个对象的权限(隐式或者显式),叫作一个对象引用另外一个对象。例如,一个Javascript对象具备对它原型的引用(隐式引用)和对它属性的引用(显式引用)。app

“对象”的概念不只特指 JavaScript 对象,还包括函数做用域(或者全局词法做用域)。函数

引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否再也不须要”简化定义为“对象有没有其余对象引用到它”。若是没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

var o = { 
  a: {
    b:2
  }
}; 
// 两个对象被建立,一个做为另外一个的属性被引用,另外一个被分配给变量o
// 很显然,没有一个能够被垃圾收集


var o2 = o; // o2变量是第二个对“这个对象”的引用

o = 1;      // 如今,“这个对象”的原始引用o被o2替换了

var oa = o2.a; // 引用“这个对象”的a属性
// 如今,“这个对象”有两个引用了,一个是o2,一个是oa

o2 = "yo"; // 最初的对象如今已是零引用了
           // 他能够被垃圾回收了
           // 然而它的属性a的对象还在被oa引用,因此还不能回收

oa = null; // a属性的那个对象如今也是零引用了
           // 它能够被垃圾回收了
复制代码

引用计数缺陷

该算法有个限制:没法处理循环引用。在下面的例子中,两个对象被建立,并互相引用,造成了一个循环。它们被调用以后会离开函数做用域,因此它们已经没有用了,能够被回收了。然而,引用计数算法考虑到它们互相都有至少一次引用,因此它们不会被回收。

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();
复制代码

标记-清除算法

这个算法把“对象是否再也不须要”简化定义为“对象是否能够得到”。

此算法能够分为两个阶段,一个是标记阶段(mark),一个是清除阶段(sweep)。

  1. 标记阶段,垃圾回收器会从根对象开始遍历。每个能够从根对象访问到的对象都会被添加一个标识,因而这个对象就被标识为可到达对象。
  2. 清除阶段,垃圾回收器会对堆内存从头至尾进行线性遍历,若是发现有对象没有被标识为可到达对象,那么就将此对象占用的内存回收,而且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操做。

简单看看下面两张图片

  • 在标记阶段,从根对象1能够访问到B,从B又能够访问到E,那么B和E都是可到达对象,一样的道理,F、G、J和K都是可到达对象。
  • 在回收阶段,全部未标记为可到达的对象都会被垃圾回收器回收。

这个算法比前一个要好,由于“有零引用的对象”老是不可得到的,可是相反却不必定,参考“循环引用”。

从2012年起,全部现代浏览器都使用了标记-清除垃圾回收算法。全部对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并无改进标记-清除算法自己和它对“对象是否再也不须要”的简化定义。

什么时候开始垃圾回收

一般来讲,在使用标记清除算法时,未引用对象并不会被当即回收。取而代之的作法是,垃圾对象将一直累计到内存耗尽为止。当内存耗尽时,程序将会被挂起,垃圾回收开始执行。

标记-清楚算法缺陷

  • 那些没法从根对象查询到的对象都将被清除
  • 垃圾收集后有可能会形成大量的内存碎片,像上面的图片所示,垃圾收集后内存中存在三个内存碎片,假设一个方格表明1个单位的内存,若是有一个对象须要占用3个内存单位的话,那么就会致使Mutator一直处于暂停状态,而Collector一直在尝试进行垃圾收集,直到Out of Memory。

ChromeV8垃圾回收算法分代回收(Generation GC)

这个和 Java 回收策略思想是一致的。目的是经过区分「临时」与「持久」对象;多回收「临时对象区」(young generation),少回收「持久对象区」(tenured generation),减小每次需遍历的对象,从而减小每次GC的耗时。Chrome 浏览器所使用的 V8 引擎就是采用的分代回收策略。

「临时」与「持久」对象也被叫作做「新生代」与「老生代」对象

V8分代回收

V8内存限制

在node中javascript能使用的内存是有限制的.

  1. 64位系统下约为1.4GB。
  2. 32位系统下约为0.7GB。

对应到分代内存中,默认状况下。

  1. 32位系统新生代内存大小为16MB,老生代内存大小为700MB。
  2. 64位系统下,新生代内存大小为32MB,老生代内存大小为1.4GB。

新生代平均分红两块相等的内存空间,叫作semispace,每块内存大小8MB(32位)或16MB(64位)。

这个限制在node启动的时候能够经过传递--max-old-space-size 和 --max-new-space-size来调整,如:

node --max-old-space-size=1700 app.js //单位为MB
node --max-new-space-size=1024 app.js //单位为MB
复制代码

上述参数在V8初始化时生效,一旦生效就不能再动态改变。

V8为何会有内存限制

  • 表面上的缘由是V8最初是做为浏览器的JavaScript引擎而设计,不太可能遇到大量内存的场景。
  • 而深层次的缘由则是因为V8的垃圾回收机制的限制。因为V8须要保证JavaScript应用逻辑与垃圾回收器所看到的不同,V8在执行垃圾回收时会阻塞JavaScript应用逻辑,直到垃圾回收结束再从新执行JavaScript应用逻辑,这种行为被称为“全停顿”(stop-the-world)。
  • 若V8的堆内存为1.5GB,V8作一次小的垃圾回收须要50ms以上,作一次非增量式的垃圾回收甚至要1秒以上。
  • 这样浏览器将在1s内失去对用户的响应,形成假死现象。若是有动画效果的话,动画的展示也将显著受到影响。

V8新生代算法(Scavenge)

新生代中的对象主要经过Scavenge算法进行垃圾回收。在Scavenge的具体实现中,主要采用Cheney算法。

  • Cheney算法是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,这两个空间中只有一个处于使用中,一个处于闲置状态。
  • 处于使用状态的空间称为From空间,处于闲置的空间称为To空间。
  • 分配对象时,先是在From空间中进行分配,当开始垃圾回收时,会检查From空间中的存活对象,并将这些存活对象复制到To空间中,而非存活对象占用的空间被释放。
  • 完成复制后,From空间和To空间的角色互换。
  • 简而言之,垃圾回收过程当中,就是经过将存活对象在两个空间中进行复制。
    Scavenge算法的缺点是只能使用堆内存中的一半,但因为它只复制存活的对象,对于生命周期短的场景存活对象只占少部分,因此在时间效率上有着优异的表现。

晋升

以上所说的是在纯Scavenge算法中,可是在分代式垃圾回收的前提下,From空间中存活的对象在复制到To空间以前须要进行检查,在必定条件下,须要将存活周期较长的对象移动到老生代中,这个过程称为对象晋升。

对象晋升的条件有两个,一种是对象是否经历过Scacenge回收:

另一种状况是当To空间的使用应超过25%时,则这个对象直接晋升到老生代空间中。

V8老生代算法(Mark-Sweep,Mark-Compact)

在老生代中的对象,因为存活对象占比较大,再采用Scavenge方式会有两个问题:

  • 一个是存活对象就较多,复制存活对象的效率将会下降;
  • 另外一个依然是浪费一半空间的问题。为此,V8在老生代中主要采用Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收。

Mark-Sweep(标记- 清除算法)

这个算法上文有提到过,这里再说一下。

  • 与Scavenge不一样,Mark-Sweep并不会将内存分为两份,因此不存在浪费一半空间的行为。Mark-Sweep在标记阶段遍历堆内存中的全部对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。
  • 也就是说,Scavenge只复制活着的对象,而Mark-Sweep只清除死了的对象。活对象在新生代中只占较少部分,死对象在老生代中只占较少部分,这就是两种回收方式都能高效处理的缘由。
  • 可是这个算法有个比较大的问题是,内存碎片太多。若是出现须要分配一个大内存的状况,因为剩余的碎片空间不足以完成这次分配,就会提早触发垃圾回收,而此次回收是没必要要的。
  • 因此在此基础上提出Mark-Compact算法。

Mark-Compact(标记-整理算法)

Mark-Compact在标记完存活对象之后,会将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的全部内存。


内存问题

  1. 如今的chrome浏览器是否还会存在内存泄漏?
  2. 内存泄漏跟内存溢出的区别是什么?
  3. chrome什么时候开始内存回收?
  4. 回收分配的内存必定比不回收要好吗?

ps: 请勿转载,只学习交流使用。

相关文章
相关标签/搜索