在JavaScript中如何做内存管理?

引言:本文将会讨论内存及JavaScript中的内存管理方式,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题

什么是内存?

内存(Memory)也被称为内存储器,其作用是用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。只要计算机在运行中,CPU就会把需要运算的数据调到内存中进行运算,当运算完成后CPU再将结果传送出来,内存的运行也决定了计算机的稳定运行。因此从概念上讲,我们可以把整个计算机内存看作是我们可以读写的一大块空间。很多东西都存储在内存中:

  • 程序使用的所有变量和其他数据。

  • 程序的代码,包括操作系统的代码。

内存的生命周期

无论你使用的是什么编程语言,内存生命周期几乎都是一样的:

在JavaScript中如何做内存管理?

以下是对内存生命周期中每个步骤发生的情况的概述:

申请内存(内存分配)

内存由操作系统分配,允许程序使用它。在简单的编程语言中,这个过程是开发人员应该处理的一个显式操作。然而,在高级编程语言中,系统会帮助你完成这个操作。

根据内存分配的方式分为了静态内存和动态内存,下表总结了静态和动态内存分配之间的区别:

在JavaScript中如何做内存管理?

  • 静态内存分配在编译时完成,不占用CPU资源; 动态内存分配在运行时,分配与释放都占用CPU资源。

  • 静态内存在栈(stack)上分配; 动态内存在堆(heap)上分配。

  • 动态内存分配需要指针和引用类型支持,静态不需要。

  • 静态内存分配是按计划分配,由编译器负责; 动态内存分配是按需分配,由程序员负责。

使用内存

这是程序使用之前申请内存的时间段,你的代码会通过使用分配的变量来对内存进行读取和写入操作。

栈内存:在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,系统就在栈中为这个变量分配内存空间,当超过变量的作用域后,系统会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

堆内存:堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。

释放内存

对于不再需要的内存进行释放的操作,以便确保其变成空闲状态并且可以被再次使用。与分配内存操作一样,这个操作在简单的编程语言中是需要显示操作的。

JavaScript的内存管理

JavaScript是所谓的垃圾回收语言之一。垃圾回收语言,通过定期检查哪些事先被分配的内存块仍然可以被应用的其他部分“访问”到,来帮助开发者管理内存。换句话说,垃圾回收语言从“哪些内存是仍然被需要的?”到“哪些内存是仍然可以被应用的其他部分访问到的”减少了管理内存的问题。差异很微妙,但是很重要:当只有开发者知道一块分配了的内存将来会被需要,访问不到的内存可以在算法上被决策并标记为系统回收内存。

以Google的V8引擎为例,在V8引擎中所有的JavaScript对象都是通过堆来进行内存分配的。当我们在代码中声明变量并赋值时,V8引擎就会在堆内存中分配一部分给这个变量。如果已申请的内存不足以存储这个变量时,V8引擎就会继续申请内存,直到堆的大小达到了V8引擎的内存上限为止(默认情况下,V8引擎的堆内存的大小上限在64位系统中为1464MB,在32位系统中则为732MB)。

另外,V8引擎对堆内存中的JAVASCRIPT对象进行分代管理。新生代:新生代即存活周期较短的JavaScript对象,如临时变量、字符串等;老生代:老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

在JavaScript中分配内存

值的初始化:在为变量赋值的时候,javascript会完成内存的分配工作。

var n = 254; // 为数字分配内存
var s = 'sessionstack'; // 为字符串分配内存
var o = {
a: 12,
b: null
}; // 为对象及其包含的值分配内存
var a = [1, null, 'str']; //(类似对象)为数组及其包含的值分配内存。
function f(a) {
return a + 3;
}// 为函数分配内存(函数是可调用的对象)
someElement.addEventListener('click', function(){
someElement.style.backgroundColor = 'blue';
}, false);// 函数表达式同样也是对象,存在分配内存的情况

通过函数调用完成分配:一些函数调用也会导致对象分配

var d = new Date(); // 分配日期对象
var e = document.createElement('div'); // 分配DOM元素

一些方法会分配新值或者对象:

var s1 = 'sessionstack';
var s2 = s1.substr(0, 3);
// S2是一个新字符串,因为字符串是不可变的,javascript会为[0, 3]范围的内容创建一个新的字符串。
var a1 = ['str1', 'str2'];
var a2 = ['str3', 'str4'];
var a3 = a1.concat(a2);
//a3是具有4个元素的新数组是a1和a2元素的合并

在JavaScript中使用内存

基本上在JavaScript中使用分配的内存,意味着在其中读写。这可以通过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。

在JavaScript中释放内存

大部分内存泄漏问题都是在这个阶段产生的,这个阶段最难的问题就是确定何时不再需要已分配的内存。它通常需要开发人员确定程序中的哪个部分不再需要这些内存,并将其释放。

高级语言嵌入了一个名为垃圾收集器的功能,其工作是跟踪内存分配和使用情况,以便在不再需要分配内存的情况下自动释放内存。

不幸的是,这个过程无法做到那么准确,因为像某些内存不再需要的问题是不能由算法来解决的。

大多数垃圾收集器通过收集不能被访问的内存来工作,例如指向它的变量超出范围的这种情况。然而,这种方式只能收集内存空间的近似值,因为在内存的某些位置可能仍然有指向它的变量,但它却不会被再次访问。

由于确定一些内存是否“不再需要”,是不可判定的,所以垃圾收集机制就有一定的局限性。下面将解释主要垃圾收集算法及其局限性的概念。

内存引用

垃圾收集算法所依赖的主要概念之一就是内存引用。

在内存管理情况下,如果一个对象访问变量(可以是隐含的或显式的),则称该对象引用另一个对象。例如,JavaScript对象具有对其原对象(隐式引用)及其属性值(显式引用)的引用。

在这种情况下,“对象”的概念超出了javascript传统意义上对象的概念,他还包括函数作用域和全局作用域。

使用引用计数算法的垃圾回收

这是最简单的垃圾收集算法。引入了 “对象不再需要” 和 “没有其他对象引用该对象” 的概念。当该对象的引用指针变为0的时候,就认为他可以被回收。看看下面的代码:

var o1 = {
o2: {
x: 1
}
};
// 两个对象被创建,'o1'包含'o2'对象的引用,没有垃圾可以收集。
var o3 = o1; //'o3'为'o1'对象的引用
o1 = 1; //现在'o1'已经不是对象的引用,对象只有'o3'一个引用。
var o4 = o3.o2; // 引用指向了对象的属性'o2'
o3 = '374'; // 现在对象有零个引用指向它,可以认为是“垃圾收集”, 然而,它的属性'o2'依然被'o4'指向,所以不能被释放。
o4 = null; // 原本'o2'的引用已经取消,'o1' 对象有零个引用指向它,可以被回收。

循环引起对象无法释放:该算法有其局限性,当一个对象引用另外一个对象,当形成循环引用时,即时他们不再被需要了,垃圾收集器也不会回收他们。。例如下面的例子,创建两个对象并相互引用,这样会创建一个循环引用。在函数调用之后,它们将超出范围,所以它们实际上是无用的,可以被释放。然而,引用计数算法认为,由于两个对象中的每一个都被引用至少一次,所以两者都不能被垃圾收集机制收回。

function f() {
var o1 = {};
var o2 = {};
o1.p = o2; // o1 引用 o2
o2.p = o1; // o2 引用 o1,这创造了一个循环。
}
f();

在JavaScript中如何做内存管理?

现实中的例子:ie6、7中,在dom对象上使用引用计数的算法,这里会存在内存泄露的问题。

var div = document.createElement("div");
div.onclick = function(){
doSomething();
}; // div 通过 click 属性引用了事件处理程序
// 当事件处理函数中访问了div变量的时候,会形成循环引用,将导致两个对象都不会被回收,造成内存泄露

标记和扫描算法

为了决定是否需要对象,标记和扫描算法会确定对象是否是活动的。标记和扫描算法经过以下3个步骤:

  1. roots:通常,root是代码中引用的全局变量。例如,在JavaScript中,可以充当root的全局变量是“窗口”对象。Node.js中的相同对象称为“全局”。所有root的完整列表由垃圾收集器构建。

  2. 然后算法会检查所有root和他们的子对象并且标记它们是活动的(即它们不是垃圾)。任何root不能达到的,将被标记为垃圾。

  3. 最后,垃圾回收器释放所有未标记为活动的内存块,并将该内存返回给操作系统。

在JavaScript中如何做内存管理?

这个算法比引用计数垃圾收集算法更好。JavaScript垃圾收集(代码/增量/并发/并行垃圾收集)领域中所做的所有改进都是对这种标记和扫描算法的实现改进,但不是对垃圾收集算法本身的改进。

在上面的相互引用例子中,在函数调用返回之后,两个对象不再被全局对象可访问的对象引用。因此,它们将被垃圾收集器发现,从而进行收回。

即使在对象之间有引用,它们也不能从root目录中访问,从而会被认为是垃圾而收集。

优点:周期将不再是一个问题。在“循环引起对象无法释放”的例子中,函数调用结束之后,这两个对象不会被全局对象引用,也不会被全局对象引用的对象引用。因此,他们会被javascript垃圾回收器标记为不可访问对象。这种事情同样也发生在“现实中的例子”中,当div和事件处理函数被垃圾回收器标记为不可访问,他们就会被释放掉。

局限:对象需要明确的标记为不可访问,这种标记的方法存在局限,但是我们在编程中被没有接触到他,所以我们很少关心垃圾回收相关的内容。。

抵制垃圾收集器的直观行为

尽管垃圾收集器使用起来很方便,但它们也有自己的一套标准,其中之一是非决定论。换句话说,垃圾收集是不可预测的。你不能真正知道什么时候进行收集,这意味着在某些情况下,程序会使用更多的内存,虽然这是实际需要的。在其它情况下,在特别敏感的应用程序中,短暂暂停是很可能出现的。尽管非确定性意味着不能确定何时进行集合,但大多数垃圾收集实现了共享在分配期间进行收集的通用模式。如果没有执行分配,大多数垃圾收集会保持空闲状态。如以下情况:

  • 大量的分配被执行。

  • 大多数这些元素(或所有这些元素)被标记为无法访问(假设我们将一个引用指向不再需要的缓存)。

  • 没有进一步的分配执行。

在这种情况下,大多数垃圾收集不会做出任何的收集工作。换句话说,即使有不可用的引用需要收集,但是收集器不会进行收集。虽然这并不是严格的泄漏,但仍会导致内存使用率高于平时。

小结:我们在学习了JavaScript的内存管理后,在js编程时应尽量避免全局变量的使用和减少不必要的引用,防止内存泄漏。在下一篇文章中我们将学习JavaScript的内存泄漏以及处理方法。欢迎大家提出不同看法,大家一起讨论学习。

想了解更多编程内容,请关注本头条号。

相关推荐