原文地址: How JavaScript works: memory management...
javascript
本文永久连接: https://github.com/AttemptWeb...
java
有部分的删减和修改,不过大部分是参照原文来的,翻译的目的主要是弄清JavaScript的垃圾回收机制,以为有问题的欢迎指正。
git
如今咱们将解释第一步(分配内存)是如何在JavaScript中工做的。github
JavaScript 减轻了开发人员处理内存分配的责任 - JavaScript本身执行了内存分配,同时声明了值。web
var n = 374; // 为number分配内存
var s = 'sessionstack'; // 为string分配内存
var o = {
a: 1,
b: null
}; //为对象及属性分配内存
function f(a) {
return a + 3;
} // 为函数分配内存
// 函数表达式分配内存
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);复制代码
基本上在 JavaScript 中使用分配的内存,意味着在其中读写。算法
这能够经过读取或写入变量或对象属性的值,甚至传递一个变量给函数来完成。数组
因为发现一些内存是否“再也不须要”事实上是不可断定的,因此垃圾收集在实施通常问题解决方案时具备局限性。下面将解释主要垃圾收集算法及其局限性的基本概念。
浏览器
prototype
(隐式引用)和它的属性值(显式引用)。
在这种状况下,“对象”的概念扩展到比普通JavaScript对象更普遍的范围,并包含函数做用域(或全局词法范围)。缓存
词法做用域定义了变量名如何在嵌套函数中解析:即便父函数已经返回,内部函数仍包含父函数的做用域。
这是最简单的垃圾收集算法。 若是有零个指向它的引用,则该对象被认为是可垃圾回收的
。 请看下面的代码:bash
var o1 = {
o2: {
x: 1
}
};
// 两个对象被建立。
// ‘o1’对象引用‘o2’对象做为其属性。
// 不能够被垃圾收集
var o3 = o1; // ‘o3’变量是第二个引用‘o1‘指向的对象的变量.
o1 = 1; // 如今,在‘o1’中的对象只有一个引用,由‘o3’变量表示
var o4 = o3.o2; // 对象的‘o2’属性的引用.
// 此对象如今有两个引用:一个做为属性、另外一个做为’o4‘变量
o3 = '374'; // 原来在“o1”中的对象如今为零,对它的引用能够垃圾收集。
// 可是,它的‘o2’属性存在,由‘o4’变量引用,所以不能被释放。
o4 = null; // ‘o1’中最初对象的‘o2’属性对它的引用为零。它能够被垃圾收集。复制代码
在周期循环
中有一个限制。在下面的例子中,两个对象被建立并相互引用,这就建立了一个循环。在函数调用以后,它们会超出界限,因此它们其实是无用的,而且能够被释放。然而,引用计数算法认为,因为两个对象中的每个都被至少引用了一次,因此二者都不能被垃圾收集。
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // ‘o1’ 应用 ‘02’ o1 references o2
o2.p = o1; // ‘o2’ 引用 ‘o2’ . 一个循环被建立
}
f();复制代码
为了肯定是否须要某个对象,本算法判断该对象是否可访问。
标记和扫描算法通过这 3个步骤
:
根节点
:通常来讲,根是代码中引用的全局变量。例如,在 JavaScript 中,能够充当根节点的全局变量是“window”对象。Node.js 中的全局对象被称为“global”。完整的根节点列表由垃圾收集器构建。任何根节点不能访问的变量将被标记为垃圾
。垃圾收集器释放全部未被标记为活跃的内存块,并将这些内存返回给操做系统
。标记和扫描算法行为的可视化。(Mark and sweep) 标记与清除
由于“一个对象有零引用”致使该对象不可访问,因此这个算法比前一个算法更好。咱们在周期中看到的情形恰巧相反,是不正确的。 截至 2012 年,全部现代浏览器都内置了标记扫描式的垃圾回收器
。去年在 JavaScript 垃圾收集(通用/增量/并发/并行垃圾收集)领域中所作的全部改进都是基于这种算法(标记和扫描)的实现改进,但这不是对垃圾收集算法自己的改进,也不是对判断一个对象是否可访问这个目标的改进。
在上面的例子中,函数调用返回后,两个对象再也不被全局对象中的变量引用。所以,垃圾收集器会认为它们不可访问。
即便两个对象之间有引用,根节点不在访问它们。
尽管垃圾收集器很方便,但他们也有本身的一套策略。其中之一是不肯定性。换句话说,GC(垃圾收集器)是不可预测的。你不能肯定一个垃圾收集器什么时候会执行收集。这意味着在某些状况下,程序其实须要更多的内存。其余状况下,在特别敏感的应用程序中,短暂和卡顿多是明显的。尽管不肯定性意味着不能肯定一个垃圾收集器什么时候执行收集,大多数 GC 共享分配中的垃圾收集通用模式。若是没有执行分配,大多数 GC 保持空闲状态。考虑以下场景:
大量的分配被执行。
大多数这些元素(或所有)被标记为不可访问(假设咱们废除一个指向咱们再也不须要的缓存的引用)。
没有执行更深的内存分配。
在这种状况下,大多数 GC 不会运行任何更深层次的收集。换句话说,即便存在变量可用于收集,收集器也不会收集这些引用。这些并非严格的泄漏,但仍会致使高于平常的内存使用率。
内存泄漏 是
应用程序过去使用,但再也不须要的还没有返回到操做系统或可用内存池的内存片断
。因为没有被释放而致使的,它将可能引发程序的卡顿和崩溃。
function foo(arg) {
bar = "some text";
// window.bar = "some text";
}复制代码
假设 bar 的目的只是引用 foo 函数中的一个变量。然而不使用 var
来声明它,就会建立一个冗余的全局变量
。
你能够经过在 JavaScript 文件的开头添加 'use strict'; 来避免这些后果,这将开启一种更严格的 JavaScript 解析模式,从而防止意外建立全局变量。
意外的全局变量固然是个问题,然而更常出现的状况是,你的代码会受到显式的全局变量的影响,而这些全局变量没法经过垃圾收集器收集。须要特别注意用于临时存储和处理大量信息的全局变量。若是你必须使用全局变量来存储数据,当你这样作的时候,要保证一旦完成使用,就把他们赋值为 null 或从新赋值
。
咱们以常常在 JavaScript 中使用的 setInterval
为例。
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每5秒执行一次.复制代码
上面的代码片断显示了使用定时器引用节点或无用数据的后果。它既不会被收集,也不会被释放。没法被垃圾收集器收集,频繁的被调用,占用内存。而正确的使用方法是,确保一旦依赖于它们的事件已经处理完成,就经过明确的调用来删除它们。
闭包是JavaScript开发的一个关键点:一个内部函数能够访问外部(封闭)函数的变量
。
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // originalThing 被引用
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);复制代码
一旦调用了 replaceThing
函数,theThing
就获得一个新的对象,它由一个大数组和一个新的闭包(someMethod
)组成。然而 originalThing
被一个由 unused
变量(这是从前一次调用 replaceThing
变量的 Thing
变量)所持有的闭包所引用。须要记住的是一旦为同一个父做用域内的闭包建立做用域,做用域将被共享
。
在这个例子中,someMethod
建立的做用域与 unused
共享。unused
包含一个关于 originalThing
的引用。即便 unused
从未被引用过,someMethod
也能够经过 replaceThing
做用域以外的 theThing
来使用它(例如全局的某个地方)。因为 someMethod
与 unused
共享闭包范围,unused
指向 originalThing
的引用强制它保持活动状态(两个闭包之间的整个共享范围)。这阻止了它们的垃圾收集。
在上面的例子中,为闭包 someMethod
建立的做用域与 unused
共享,而 unused
又引用 originalThing
。someMethod
能够经过 replaceThing
范围以外的 theThing
来引用,尽管 unused
历来没有被引用过。事实上,unused
对 originalThing
的引用要求它保持活跃,由于 someMethod
与 unused
的共享封闭范围。
全部这些均可能致使大量的内存泄漏。当上面的代码片断一遍又一遍地运行时,您能够预期到内存使用率的上升。当垃圾收集器运行时,其大小不会缩小。一个闭包链被建立(在例子中它的根就是 theThing
变量),而且每一个闭包做用域都包含对大数组的间接引用。
有些状况下开发人员在变量中存储 DOM 节点。假设你想快速更新表格中几行的内容。若是在对象中存储对每一个 DOM 行的引用,就会产生两个对同一个 DOM 元素的引用:一个在 DOM 树中,另外一个在对象中。若是你决定删除这些行,你须要记住让两个引用都没法访问。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// image 元素是body的直接子元素。
document.body.removeChild(document.getElementById('image'));
// 咱们仍然能够在全局元素对象中引用button。换句话说,button元素仍在内存中,没法由GC收集
}复制代码
在涉及 DOM 树内的内部节点或子节点时,还有一个额外的因素须要考虑。若是你在代码中保留对table
表格单元格(td
标记)的引用,并决定从 DOM 中删除该table
表格但保留对该特定单元格td
的引用,则能够预见到严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元格td
以外的全部东西。但状况并不是如此。因为单元格td
是table
表格的子节点,而且子节点保持对父节点的引用,因此对table
表格对单元格td的这种单引用会把整个table
表格保存在内存中。
咱们在 SessionStack 尝试遵循这些最佳实践,编写正确处理内存分配的代码,缘由以下:
一旦将 SessionStack 集成到你的生产环境的 Web 应用程序中,它就会开始记录全部的事情:全部的 DOM 更改,用户交互,JavaScript 异常,堆栈跟踪,失败网络请求,调试消息等。
经过 SessionStack web 应用程序中的问题,并查看全部的用户行为。全部这些都必须在您的网络应用程序没有性能影响的状况下进行。
因为用户能够从新加载页面或导航你的应用程序,全部的观察者,拦截器,变量分配等都必须正确处理,这样它们才不会致使任何内存泄漏,也不会增长咱们正在整合的Web应用程序的内存消耗。
这里有一个免费的计划因此你能够试试看.
How JavaScript works: memory management...