根据GitHut stats的统计数据显示,javascript语言在Github中的活跃项目仓库数量和总的push数量已经登上了榜首的位置,并且在愈来愈多的领域里咱们都能看见javascript持续活跃的身影和不断前行的脚步
javascript
在这篇文章中我将主要探讨javascript内存管理,调用栈以及如何处理内存泄漏问题。 在后续的文章中我会继续介绍事件循环以及js引擎的执行机制。java
javascript自带垃圾回收机制,它能够自动分配内存并回收再也不使用的内存,这也使不少开发者认为咱们没有必要去关注js的内存管理。可是我相信咱们在平时开发过程当中都或多或少的遇到过内存泄漏问题,理解javascript的内存管理机制能够帮助咱们解决此类问题并写出更好的代码,并且做为一名程序员,咱们也应当保持足够的好奇心去了解咱们写出的代码在底层的运行原理。node
在进入具体探讨以前,咱们先来看下内存究竟是什么。内存从物理意义上是指由一系列晶体管构成的能够存储数据的回路,从逻辑的角度咱们能够将内存看做是一个巨大的可读写的比特数组。它存储着咱们编写的代码以及咱们在代码中定义的各种变量。对于不少静态类型编程语言来讲,在代码进入编译阶段时编译器会根据变量声明时指定的类型提早申请分配给该变量的内存(好比,整型变量对应的是4个字节,浮点数对应8个字节)。内存区域分为栈空间和堆空间两部分,对于能够肯定大小的变量,它们会被存储在栈空间中,好比:git
int n; // 4 bytes
int x[4]; // array of 4 elements, each 4 bytes
double m; // 8 bytes
复制代码
还有一种类型的变量,不能在编译阶段就肯定其须要多大的存储区域,其占用内存大小是在运行时肯定的,好比:程序员
int n = readInput(); // n的大小依赖于用户的输入
复制代码
对于这一类型的变量,它们会被存储在堆空间中。内存静态分配(static allocation)与动态分配(Dynamic allocation)的区别以下所示:
算法
对于任何编程语言,内存的生命周期都基本一致,以下所示:
express
var n = 374; // allocates memory for a number
var s = 'sessionstack'; // allocates memory for a string
var o = {
a: 1,
b: null
}; // allocates memory for an object and its contained values
var a = [1, null, 'str']; // (like object) allocates memory for the
// array and its contained values
function f(a) {
return a + 3;
} // allocates a function (which is a callable object)
// function expressions also allocate an object
someElement.addEventListener('click', function() {
someElement.style.backgroundColor = 'blue';
}, false);
// 函数调用触发的内存分配
var d = new Date(); // allocates a Date object
var e = document.createElement('div'); // allocates a DOM element
// 方法的调用也能够触发
var s1 = 'sessionstack';
var s2 = s1.substr(0, 3); // s2 is a new string
// Since strings are immutable,
// JavaScript may decide to not allocate memory,
// but just store the [0, 3] range.
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
// new array with 4 elements being
// the concatenation of a1 and a2 elements
复制代码
在讨论该算法前,咱们先来看下什么是引用(reference)。所谓引用是指一个对象与另外一个对象的链接关系,若是对象A能够隐式或显式的访问对象B,那么咱们就能够说对象A拥有一个对对象B的引用,好比在javascript中一个object能够经过__proto__访问到其prototype对象(隐式),也能够直接访问其属性(显式)。
对于引用计数算法来讲,它断定一个目标是能够被回收的标志就是该目标再也不存在与其余对象的引用关系,好比:编程
var o1 = {
o2: {
x: 1
}
};
// 2 objects are created.
// 'o2' is referenced by 'o1' object as one of its properties.
// None can be garbage-collected
var o3 = o1; // the 'o3' variable is the second thing that
// has a reference to the object pointed by 'o1'.
o1 = 1; // now, the object that was originally in 'o1' has a
// single reference, embodied by the 'o3' variable
var o4 = o3.o2; // reference to 'o2' property of the object.
// This object has now 2 references: one as
// a property.
// The other as the 'o4' variable
o3 = '374'; // The object that was originally in 'o1' has now zero
// references to it.
// It can be garbage-collected.
// However, what was its 'o2' property is still
// referenced by the 'o4' variable, so it cannot be
// freed.
o4 = null; // what was the 'o2' property of the object originally in
// 'o1' has zero references to it.
// It can be garbage collected.
复制代码
在上述示例中,o4就是能够被回收的,引用计数在大多数状况下都是没什么问题的,可是当咱们遇到循环引用它就会遇到麻烦,好比:数组
function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 references o2
o2.p = o1; // o2 references o1. This creates a cycle.
}
f();
复制代码
在上述示例中,o1,o2互相引用,使得彼此都不能被释放。浏览器
标记清除判断某个对象是否能够被回收的标志是该对象不能再被访问到。其执行过程总共分为三步:
内存泄漏是指再也不使用的内存区域没有被回收,致使这一块内存区域被白白浪费。虽然咱们有前面提到的垃圾回收算法,可是咱们在平常开发过程当中仍然会时时遇到内存泄漏的问题,主要有如下三种类型:
function foo(arg) {
bar = "some text";
}
复制代码
在这段代码中咱们在函数foo里给变量bar赋值了一个字符串,可是咱们没有在该函数做用域内先声明它,在javascript执行过程当中,bar会被挂载到全局变量window中(假设当前是浏览器环境),看成是window的属性。这带来的问题是即便函数foo执行完毕,该变量仍然是可访问到的,其占用的内存不会获得释放,从而致使内存泄漏。
咱们在平常开发过程当中要尽可能避免使用全局变量,除了污染全局做用域的问题,内存泄漏也是一个不容忽视的因素。
2. 闭包
内部函数能够访问其外部做用域内的变量,闭包为咱们编写javascript代码带来史无前例的灵活性,可是闭包也有可能会带来内存泄漏的风险。好比下面这段代码:
var theThing = null;
var replaceThing = function () {
var originalThing = theThing;
var unused = function () {
if (originalThing) // a reference to 'originalThing'
console.log("hi");
};
theThing = {
longStr: new Array(1000000).join('*'),
someMethod: function () {
console.log("message");
}
};
};
setInterval(replaceThing, 1000);
复制代码
在函数replaceThing中,函数unused会造成一个闭包并含有对originalThing的引用,一旦replaceThing执行,theThing会被赋予一个对象做为新值,在该对象中也会定义一个新的闭包someMethod, 这两个闭包是在相同的父级做用域中建立的,所以它们会共享外部做用域。因为someMethod方法能够经过theThing在replaceThing外部访问到,即便unused没有被调用,它对变量originalThing的引用会使该做用域不会被回收。由于每一个闭包做用域都含有对longstr的间接引用,这种状态下会致使大量的内存泄漏。
3. dom引用
先来看下面这一段代码:
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image')
};
function doStuff() {
elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
// The image is a direct child of the body element.
document.body.removeChild(document.getElementById('image'));
// At this point, we still have a reference to #button in the
//global elements object. In other words, the button element is
//still in memory and cannot be collected by the GC.
}
复制代码
在上述代码中咱们在两个地方保存了对image元素的引用,当函数removeImage执行时尽管image元素被删除,可是全局变量elements中仍然存在对button元素的引用,内存回收时不会将该元素回收。
除此以外还有另外一种状况也值得引发注意。若是你存在对某个table cell(td 标签)的引用,当你在dom树中删除它所属的table但该引用并无删除时也一样会发生内存泄漏,垃圾回收器并不会像你所想的那样回收全部只保留cell,而是会将整个table都保存在内存中,由于该table是cell的父节点,该cell依然会保持对其父节点的引用。
调用栈是内存中的一块存储区域,它负责记录程序当前的执行位置,咱们能够经过一个示例来看下调用栈的工做模式,先来看以下代码:
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
复制代码
当这段代码开始执行时,调用栈会随着函数的调用发生变化
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
复制代码
当这段代码执行时报错提示以下所示:
function foo() {
foo();
}
foo();
复制代码
它会不断的调用自身,其调用栈存储示意图和执行报错以下所示:
本篇文章主要探讨了javascript中的内存管理策略,介绍了内存的分配,内存的回收以及三种容易致使内存泄漏的场景还有代码执行用到的调用栈等等,属于javascript中比较基础但却容易忽视的知识点,但愿对您有所帮助。