Javascript如何避免内存泄漏

任何程序都有可能发生内存“泄漏”(即申请了系统内存并且在工作完成后没有释放),并且对于使用非托管语言(unmanaged languages)(如C语言)的开发者来说,内存的分配和释放是一个主要的关注点。JavaScript是一种内存托管(memory-managed)的语言,垃圾回收过程能够帮助程序员自动地处理内存的分配和释放。该机制解决了大部分困扰的非托管代码的问题,但是,认为内存托管语言不会产生内存泄漏却是错误的。

垃圾回收进程尝试推断何时可以安全地回收不再使用的变量,通常是通过判定程序是否能够通过变量之间形成的引用网络到达该变量。当确信变量是不可达的,就在它上面标上可以回收的记号,并且在回收器的下一次清理中(可能在未来的任意时刻)释放相关的内存。在托管语言中产生内存泄漏非常简单:只需使用完变量而忘记解除引用。

我们来考虑一个简单的例子,其中定义了一个描述家庭宠物及其主人的对象模型。首先看看主人,以Person对象描述:function Person(name){ this.name=name; this.pets=new Array(); } 一个主人可以养一只或者多只宠物。当主人得到了一只宠物,他告诉宠物现在自己是它的主人: Person.prototype.addPet=function(pet){ this.pets[pet.name]=pet; if (pet.assignOwner){ pet.assignOwner(this); } } 类似的,当主人从他的宠物列表中删除了一只宠物,他告诉宠物自己不再是它的主人:

Js代码this.removePet(petName)=function{ var orphan=this.pets[petName]; this.pets[petName]=null; if (orphan.unassignOwner){ orphan.unassignOwner(this); } } 主人在任何时刻都知道谁是他的宠物并且能够通过提供的addPet()和removePet()方法来管理宠物列表。主人在领养或不再领养宠物时都会通知该宠物,这基于一个假设,即每个宠物都会遵守这个契约(在JavaScript中,这个契约是隐含的,可以在运行时检查是否遵守了契约)。

宠物多种多样,在这里定义了两种:猫和狗。它们的区别在于对待被领养的态度上,猫并不在意被谁所领养,而狗一生都会伴随领养它的主人(我为这个普遍的观点向动物世界道歉!)。

因此宠物猫的定义看起来像是这样:function Cat(name){ this.name=name; } Cat.prototype.assignOwner=function(person){} Cat.prototype.unassignOwner=function(person){} 猫对于是否被领养并不感兴趣,因此仅仅提供了契约方法的空实现。

另一方面,我们可以将狗定义为忠实地记得它的主人是谁,即使被遗弃了仍然保持对主人的“引用”(一些狗确实如此function Dog(name){ this.name=name; } Dog.prototype.assignOwner=function(person){ this.owner=person; } Dog.prototype.unassignOwner=function(person){ this.owner=person; } Cat和Dog对象都是Pet的行为恶劣的实现。作为宠物,它们严格依照契约的文字来实现,但是却没有遵循契约的灵魂。在Java或C#的实现中,我们可以明确地定义Pet接口,但是那样仍然不能阻止实现违背契约的灵魂。在现实编程世界中,对象建模者花费了大量的时间防止出现接口的行为恶劣的实现,尽力封堵所有可能被利用的漏洞。

我们来将这个对象模型具体化。在下面的脚本中,我们创建了三个对象:

jim,人(Person)
whiskers,猫(Cat)
fido,狗(Dog) 

首先,我们实例化一个人(Person)(第1步):

var jim=new Person(“jim”);

我们也给了jim一只宠物狗(第3步)。作为一个全局变量来声明,fido比whiskers稍微多一点优势:

var fido=new Dog(“fido”);
jim.addPet(fido);

有一天,jim送掉了他的猫(第4步):

jim.removePet(“whiskers”);

后来,他又送掉了他的狗(第5步)。也许他移民了?

jim.removePet(“fido”);

我们对jim失去了兴趣并且释放了对他的引用(第6步):

jim=null;

最后,我们又释放了对fido的引用(第7步):

fido=null;

在第6步和第7步之间,我们可能相信已经通过设置jim为null摆脱了他。事实上,他仍然被fido引用并且仍然可以通过代码fido.owner到达。垃圾回收器无法将他释放,只能留下他潜伏在JavaScript引擎的堆空间里,占用着宝贵的内存。直到第7步,当fido声明为null时,jim才变成不可达的,随后内存才能被释放。

在简单的脚本中,这是一个很小的、临时性的问题,但是这个例子展示了表面上很随意的决定对于垃圾回收过程所产生的影响。fido可能在删除jim后没有被直接删除,并且,如果它拥有记住多于一个前任主人的能力,在销毁之前可能会将大批Person对象密封在堆空间中,使其过着暗无天日的生活。如果我们选择以内嵌方式声明fido并且将那只猫声明为全局变量,将不存在任何这类的问题。为了评估fido行为的严重性,我们需要问自己以下的问题:

当它引用其他已删除的对象时,将会消耗多少内存?我们知道头脑简单的fido一次只能记住一个Person,但是尽管如此,Person可能还包含500个其他仅能通过他自身才能到达的对宠物猫的引用,因此额外的内存消耗可能无法估量。

额外的内存消耗将会保持多长时间?在这个简单的脚本中,答案是“不是很久”,但是我们可能稍后会在删除jim和删除fido之间添加额外的步骤。而且,JavaScript开发者总是以事件驱动的方式编程,因此,如果删除jim和删除fido发生在分离的事件处理函数中,我们将很难预言一个确定的答案。如果不去做某种类型的用例分析,甚至都无法给出一个概率性的答案。

任何一个问题都不像它们看起来那么容易回答。我们能够做的,就是在编写和修改代码时,对这类问题保持关注,并且执行测试以验证我们的假设是否正确。当编写代码的时候,我们就应该考虑应用的使用模式,而不只是在事后追悔莫及。

以上内容覆盖了内存管理的通用原则。