深入理解JavaScript垃圾回收机制与内存管理策略

作者:纸飞机时间:2026-10-29 17:38:02浏览:216

本文详细解析JavaScript自动垃圾回收机制,对比标记清除与引用计数两种核心算法的优劣。重点探讨循环引用导致的内存泄露问题及解决方案,并介绍分代回收与增量GC等现代引擎优化策略,帮助开发者掌握高效的内存管理技巧。

JavaScript垃圾回收机制概述

一、垃圾回收机制—GC

JavaScript拥有自动垃圾回收机制(GC,Garbage Collection),这意味着执行环境会自动负责管理代码执行过程中所使用的内存资源。

其基本原理是:垃圾收集器会定期(周期性)扫描并找出那些不再继续使用的变量,随后释放其所占用的内存空间。

JavaScript的垃圾回收机制逻辑简单:识别并释放不再使用的变量所占内存。然而,这一过程并非实时执行,因为实时回收的开销较大,所以垃圾回收器会按照固定的时间间隔周期性运行。

所谓不再使用的变量,即生命周期结束的变量,通常仅指局部变量。全局变量的生命周期直到浏览器卸载页面才会结束。局部变量仅在函数执行期间存在,在此过程中,系统会在栈或堆上为其分配空间以存储值。函数执行完毕后,局部变量随之销毁。但需注意,若存在闭包,内部函数引用了外部函数的变量,则外部函数不能被视为真正结束。

以下通过代码示例进行说明:


function fn1() {
 var obj = {name: 'hanzichi', age: 10};
}

function fn2() {
 var obj = {name:'hanzichi', age: 10};
 return obj;
}

var a = fn1();
var b = fn2();

分析代码执行流程:首先定义两个函数fn1和fn2。当调用fn1时,进入其执行环境,开辟内存存放对象{name: 'hanzichi', age: 10};调用结束后,退出fn1环境,该块内存会被JavaScript引擎的垃圾回收器自动释放。而在调用fn2时,返回的对象被全局变量b指向,因此该块内存不会被释放。

这里出现了一个关键问题:如何判断哪个变量是无用的?垃圾收集器必须跟踪变量的使用状态,对不再有用的变量打上标记,以便未来回收其内存。用于标记无用变量的策略因实现而异,通常有两种方式:标记清除和引用计数。其中,引用计数策略较少使用,而标记清除更为常见。

标记清除算法详解

二、标记清除

JavaScript中最常用的垃圾回收方式是标记清除。当变量进入环境(如在函数中声明变量)时,将其标记为“进入环境”。从逻辑上讲,只要执行流进入相应环境,就可能用到这些变量,因此永远不能释放进入环境的变量所占内存。当变量离开环境时,则将其标记为“离开环境”。


function test(){
 var a = 10 ; //被标记 ,进入环境 
 var b = 20 ; //被标记 ,进入环境
}
test(); //执行完毕 之后 a、b又被标离开环境,被回收。

垃圾回收器运行时,会给内存中所有变量加上标记(可采用任何标记方式)。随后,它会移除环境中变量以及被环境中的变量引用的变量(如闭包)的标记。在此之后仍保留标记的变量,被视为即将删除的变量,因为环境中的变量已无法访问它们。最后,垃圾回收器执行内存清除工作,销毁带标记的值并回收其占用的内存空间。

目前,IE、Firefox、Opera、Chrome、Safari等浏览器的JavaScript实现均使用标记清除的垃圾回收策略或类似策略,主要区别在于垃圾收集的时间间隔各不相同。

引用计数与循环引用问题

三、引用计数

引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,该值的引用次数即为1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对该值引用的变量获得了另一个值,则该值的引用次数减1。当这个值的引用次数变为0时,说明无法再访问该值,此时可将其占用的内存空间回收。当垃圾回收器下次运行时,它会释放那些引用次数为0的值所占的内存。


function test(){
 var a = {} ; //a的引用次数为0 
 var b = a ; //a的引用次数加1,为1 
 var c =a; //a的引用次数再加1,为2
 var b ={}; //a的引用次数减1,为1
}

Netscape Navigator 3是最早采用引用计数策略的浏览器,但很快它遇到了一个严重问题:循环引用。循环引用指的是对象A中包含一个指向对象B的指针,而对象B中也包含一个指向对象A的引用。


function fn() {
 var a = {};
 var b = {};
 a.pro = b;
 b.pro = a;
}

fn();

在上述代码中,a和b的引用次数均为2。当fn()执行完毕后,两个对象均已离开环境。在标记清除方式下,这没有问题;但在引用计数策略下,由于a和b的引用次数不为0,垃圾回收器不会回收其内存。如果fn函数被大量调用,将导致内存泄露,在IE7与IE8上表现为内存直线上升。

我们知道,IE中有一部分对象并非原生JavaScript对象。例如,其内存泄露的DOM和BOM中的对象是使用C++以COM对象形式实现的,而COM对象的垃圾回收机制采用的就是引用计数策略。因此,即使IE的JavaScript引擎采用标记清除策略,但JavaScript访问的COM对象依然基于引用计数策略。换句话说,只要在IE中涉及COM对象,就会存在循环引用问题。


var element = document.getElementById("some_element");
var myObject = new Object();
myObject.e = element;
element.o = myObject;

此例在一个DOM元素(element)与一个原生JavaScript对象(myObject)之间创建了循环引用。其中,变量myObject有一个名为element的属性指向element对象;而变量element也有一个属性名为o回指myObject。由于存在这个循环引用,即使例子中的DOM从页面中移除,它也永远不会被回收。

看上面的例子,有同学可能会觉得太弱了,谁会做这样无聊的事情?其实,我们是否就在做类似的事情?


window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){};
};

这段代码看起来没什么问题,但是obj引用了document.getElementById(“element”),而document.getElementById(“element”)的onclick方法会引用外部环境中的变量,自然也包括obj,是不是很隐蔽啊。

解决办法

最简单的方式就是自己手工解除循环引用,比如刚才的函数可以这样处理:


myObject.element = null;
element.o = null;



window.onload=function outerFunction(){
 var obj = document.getElementById("element");
 obj.onclick=function innerFunction(){};
 obj=null;
};

将变量设置为null意味着切断变量与它此前引用的值之间的连接。当垃圾回收器下次运行时,就会删除这些值并回收它们占用的内存。

需要注意的是,IE9+并不存在循环引用导致DOM内存泄露问题,这可能是微软做了优化,或者DOM的回收方式已经改变。

内存管理与GC优化策略

四、内存管理

1、什么时候触发垃圾回收?

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。

微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把触发条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多

2、合理的GC方案

1)、JavaScript引擎基础GC方案是(simple GC):mark and sweep(标记清除),即:

  • (1)遍历所有可访问的对象。
  • (2)回收已不可访问的对象。

2)、GC的缺陷

和其他语言一样,JavaScript的GC策略也无法避免一个问题:GC时,停止响应其他操作,这是为了安全考虑。而JavaScript的GC在100ms甚至以上,对一般的应用还好,但对于JS游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是新引擎需要优化的点:避免GC造成的长时间停止响应。

3)、GC优化策略

David大叔主要介绍了2个优化方案,而这也是最主要的2个优化方案了:

(1)分代回收(Generation GC) 这个和Java回收策略思想是一致的。目的是通过区分“临时”与“持久”对象;多回收“临时对象”区(young generation),少回收“持久对象”区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。如图:

JavaScript垃圾回收机制概述

这里需要补充的是:对于tenured generation对象,有额外的开销:把它从young generation迁移到tenured generation,另外,如果被引用了,那引用的指向也需要修改。

(2)增量GC 这个方案的思想很简单,就是“每次处理一点,下次再处理一点,如此类推”。如图:

JavaScript垃圾回收机制概述

这种方案,虽然耗时短,但中断较多,带来了上下文切换频繁的问题。

因为每种方案都其适用场景和缺点,因此在实际应用中,会根据实际情况选择方案。

比如:低 (对象/s) 比率时,中断执行GC的频率,simple GC更低些;如果大量对象都是长期“存活”,则分代处理优势也不大。

参考:

以上就是关于JavaScript的垃圾回收机制与内存管理的全部内容,希望对大家的学习有所帮助。

作者最新文章