[js] 内存
js的内存生命周期
不管什么程序语言,内存生命周期基本是一致的:
- 分配你所需要的内存
- 使用分配到的内存(读、写)
- 不需要时将其释放\归还
在所有语言中第一和第二部分都很清晰。最后一步在底层语言中很清晰,但是在像JavaScript 等上层语言中,这一步是隐藏的、透明的。
JavaScript 的内存分配
- 值的初始化
JavaScript 在定义变量时就完成了内存分配。
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 的结果
使用值
使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
当内存不再需要使用时释放
大多数内存管理的问题都在这个阶段。在这里最艰难的任务是找到“所分配的内存确实已经不再需要了”。它往往要求开发人员来确定在程序中哪一块内存不再需要并且释放它。
高级语言解释器嵌入了“垃圾回收器”,它的主要工作是跟踪内存的分配和使用,以便当分配的内存不再使用时,自动释放它。这只能是一个近似的过程,因为要知道是否仍然需要某块内存是无法判定的(无法通过某种算法解决)。
垃圾回收
引用
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 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();
IE 6, 7 使用引用计数方式对 DOM 对象进行垃圾回收。该方式常常造成对象被循环引用时内存发生泄漏.
myDivElement 这个 DOM 元素里的 circularReference 属性引用了 myDivElement,造成了循环引用。如果该属性没有显示移除或者设为 null,引用计数式垃圾收集器将总是且至少有一个引用,并将一直保持在内存里的 DOM 元素,即使其从DOM 树中删去了。如果这个 DOM 元素拥有大量的数据 (如上的 lotsOfData 属性),而这个数据占用的内存将永远不会被释放。
var div;
window.onload = function(){
div = document.getElementById("myDivElement");
div.circularReference = div;
div.lotsOfData = new Array(10000).join("*");
};
标记-清除算法
这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。
这个算法比前一个要好,因为“有零引用的对象”总是不可获得的,但是相反却不一定。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
- 循环引用不再是问题了
在上面的示例中,函数调用返回之后,两个对象从全局对象出发无法获取。因此,他们将会被垃圾回收器回收。第二个示例同样,一旦 div 和其事件处理无法从根获取到,他们将会被垃圾回收器回收。
- 限制: 那些无法从根对象查询到的对象都将被清除
尽管这是一个限制,但实践中很少会碰到类似的情况,所以一般不太会去关心垃圾回收机制。
chrome中的内存
对象的大小
将内存想象为具有原始类型(如数字和字符串)和对象(关联数组)的图形。它可以在视觉上表示为具有多个相互关联的点图。
对象可以通过两种方式驻留在内存中:
- 直接通过对象本身。
- 通过包含对其它对象的引用,这样就会阻止垃圾回收器(简称 GC)自动回收这些对象。
Shallow Size(浅尺寸)
这是指对象本身持有的内存大小。
典型的 JavaScript 对象具有一些保留的内存,用于他们的描述以及存储直接值。通常情况下,只有数组和字符串才会有比较明显的Shallow Size(浅尺寸) 。不过,字符串和外部数组往往在渲染内存中有它们自己的主存储器,在 JavaScript 堆只暴露一个小包装器对象。
渲染内存是指被检查页面被渲染的过程中使用的内存:本地内存 + 该页面中 JS 堆内存 + 所有通过该页面启动的workers(这里指Service workers)使用 JS 堆的内存。然而,即使是一个小的对象,也可以通过阻止垃圾回收器自动回收其他对象,来间接占用大量的内存。
Retained size(保留尺寸)
这是指对象本身与其依赖对象一起被删除后所释放的内存大小,并且 GC roots 无法到达该处。
GC roots 是由在从原生代码的 V8 之外引用 JavaScript 对象的时候所创建的句柄(局部或者全局的)构成的。这些句柄可以在 GC roots > Handle scope 以及 GC roots > Global handles 堆快照中找到。
有很多内部的GC roots,其中大部分对用户来说不感兴趣。
从应用程序的角度来说,有以下几种 roots:
- Window 全局对象(在每一 iframe中)。在堆快照中,有一个距离域,其包含的是在Window最短保留路径上的属性引用的数目。
- 文档 DOM 树由通过遍历文档可达到的所有本地DOM节点组成。 不是所有的节点都会有 JS 封装,但是如果他们有封装,那么只要文档还在,这些节点就可以使用。
- 有些时候,对象会被 debugger(调试器)上下文以及 DevTools 控制台保留(例如,在控制台进行评估后)。建议创建堆快照前请先清空控制台和调试器中没用的断点。
任何无法从根节点到达的元素够将被回收。
对象的保留树
堆就是由相互连接的对象构成的网络。
在数学的世界中,这种结构称作图形或者内存图。
图形由通过边缘连接的节点构成,其中节点和边都有相应的标签。
- 节点(或者对象) 是用用于构建它们的构造函数的名称来标记的。
- 边是用属性名来标记的。
Dominators(支配者)
支配者对象是由树形结构组成的,因为每个对象都刚好有一个支配者。一个对象的支配者不一定直接引用它所支配的对象,也就是说,支配树并不是图的生成树。
节点 1 支配 节点 2
节点 2 支配 节点 3、 4 和 6
节点 3 支配 节点 5
节点 5 支配 节点 8
节点 6 支配 节点 7
节点 #3 是 #10 的支配者,但是 #7 节点也在由 GC 到 #10 节点的,每条简单路径上。因此,如果对象 B 存在于从根节点到对象 A 的,每条简单路径上,那么对象 B 就是对象 A 的支配者。
V8 的细节
JS对象的表现形式
JavaScript 中有三种主要类型:
- Numbers(数字)(比如,3.14159..)
- Booleans(布尔值)(true 或者 false)
- Strings(字符串) (比如 “Werner Heisenberg”)
这些类型在树中都是叶子节点或者终结节点,并且它们不能引用其它值。
Numbers(数字) 可以像下面这样存储:
- 相邻的 31 位整数值,被称为 small integers (SMIs)
- 堆对象,被引用为heap numbers(堆数字)。堆数字用于存储不适合 SMI 形式的值,比如浮点类型(doubles),或者是需要封装(boxed)的值,比如设置其属性值的类型。
Strings(字符串) 可以被存储在:
- VM heap(虚拟机的堆)中
- 外部的renderer’s memory(渲染内存)。也就是当创建或者使用一个封装后的对象时需要使用的外部存储器,比如,脚本资源以及其他从网上接收而不是赋值到虚拟机堆中存储的内容。
新的 JavaScript 对象的内存是由特定的 JavaScript 堆(或者说 VM heap)分配的。这些对象由 V8 垃圾回收器管理,并且只要存在一个对他们的强引用就不会被回收。
Native objects(本地对象) 指的是不在 JavaScript 堆中存储的一切对象。本地对象和堆对象相反,其生存周期不由 V8 垃圾回收器管理,并且只能通过封装它们的 JavaScript 对象来使用。
Cons string 是一个保存了成对字符串的对象,并且该对象会将字符串拼接起来,最后的结果是串联后的字符串。拼接后的 cons string 的内容只有在需要的时候才会出现。
Arrays(数组) - 一个数组就是有着数字键的对象。他们广泛应用在 V8 VM 中,用于存储大量数据。在字典这样的数据结构中键值对的集合就是利用数组来备份的。
Map(映射) - 一个对象,用于描述对象及其布局的种类。
对象组
每个本地的对象组都是由保持彼此相互引用的对象组成的。以一个 DOM 子树为例,在该树中,每一个节点都一个指向父节点的连接,以及指向孩子节点和兄弟节点的链接,由此,所有的节点连成了一张图。需要注意的是,本地对象并不会在 JavaScript 堆中出现,所以它们的大小是 0。相应的,对于每个要使用本地对象都会创建一个对应的封装对象。
每个封装对象都含有一个对相应的本地对象的引用,这是为了能够将命令重定向到本地对象上。而对象组则含有这些封装的对象,但是,这并不会造成一个无法回收的死循环,因为垃圾回收器会自动释放不在引用的封装对象。但是一旦忘记了释放某个封装对象就可能造成整个组以及相关封装对象都无法被释放。