JavaScript中的内存泄漏以及如何处理

 

本文翻译自:https://blog.sessionstack.com/how-javascript-works-memory-management-how-to-handle-4-common-memory-leaks-3f28b94cfbec

转载请注明出自:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。

 

随着现在的编程语言功能越来越成熟、复杂,内存管理也容易被大家忽略。本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题。

 

概述

像C语言这样的编程语言,具有简单的内存管理功能函数,例如malloc( )和free( )。开发人员可以使用这些功能函数来显式地分配和释放系统的内存。

当创建对象和字符串等时,JavaScript就会分配内存,并在不再使用时自动释放内存,这种机制被称为垃圾收集。这种释放资源看似是“自动”的,但本质是混淆的,这也给JavaScript(以及其他高级语言)的开发人员产生了可以不关心内存管理的错误印象。其实这是一个大错误。

即使使用高级语言,开发人员也应该理解内存管理的知识。有时自动内存管理也会存在问题(例如垃圾收集器中的错误或实施限制等),开发人员必须了解这些问题才能正确地进行处理。

 

内存生命周期

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

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

  • 分配内存 - 内存由操作系统分配,允许程序使用它。在简单的编程语言中,这个过程是开发人员应该处理的一个显式操作。然而,在高级编程语言中,系统会帮助你完成这个操作。
  • 内存使用 -这是程序使用之前申请内存的时间段,你的代码会通过使用分配的变量

来对内存进行读取和写入操作。

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

 

什么是内存?

在硬件层面上,计算机的内存由大量的触发器组成的。每个触发器包含一些晶体管,并能够存储一位数据。单独的触发器可以通过唯一的标识符来寻址,所以我们可以读取和覆盖它们。因此,从概念上讲,我们可以把整个计算机内存看作是我们可以读写的一大块空间。

很多东西都存储在内存中:

  1. 程序使用的所有变量和其他数据。
  2. 程序的代码,包括操作系统的代码。

编译器和操作系统一起工作,来处理大部分的内存管理,但是我们需要了解从本质上发生了什么。

编译代码时,编译器会检查原始数据类型,并提前计算它们需要多少内存,然后将所需的内存分配给调用堆栈空间中的程序。分配这些变量的空间被称为堆栈空间,随着函数的调用,内存会被添加到现有的内存之上。当终止时,空间以LIFO(后进先出)顺序被移除。例如如下声明:

int n; // 4个字节
int x [4]; // 4个元素的数组,每一个占4个字节
double m; // 8个字节

编译器插入与操作系统进行交互的代码,以便在堆栈中请求所需的字节数来存储变量。

在上面的例子中,编译器知道每个变量的确切内存地址。实际上,每当我们写入这个变量n,它就会在内部翻译成“内存地址4127963”。

注意,如果我们试图访问x[4],我们将访问与m关联的数据。这是因为我们正在访问数组中不存在的元素 - 它比数组中最后一个数据实际分配的元素多了4个字节x[3],并且可能最终读取(或覆盖)了一些m比特。这对其余部分会产生不利的后果。

当函数调用其它函数时,每个函数被调用时都会得到自己的堆栈块。它会保留所有的局部变量和一个程序计数器,还会记录执行的地方。当功能完成时,其内存块会被释放,可以再次用于其它目的。

 

动态分配

如若我们不知道编译时,变量需要的内存数量时,事情就会变得复杂。假设我们想要做如下事项:

int n = readInput(); //读取用户的输入
...
//用“n”个元素创建一个数组

在编译时,编译器不知道数组需要多少内存,因为它是由用户提供的输入值决定的。

因此,它不能为堆栈上的变量分配空间。相反,我们的程序需要在运行时明确地向操作系统请求适当的空间。这个内存是从堆空间分配的。下表总结了静态和动态内存分配之间的区别:

 

在JavaScript中分配内存

现在来解释如何在JavaScript中分配内存。

JavaScript使得开发人员免于处理内存分配的工作。

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

 

在JavaScript中使用内存

基本上在JavaScript中使用分配的内存,意味着在其中读写。

这可以通过读取或写入变量或对象属性的值,或者甚至将参数传递给函数来完成。

 

当内存不再需要时进行释放

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

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

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

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

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

 

内存引用

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

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

在这种情况下,“对象”的概念扩展到比普通JavaScript对象更广泛的范围,并且还包含函数范围。

 

引用计数垃圾收集

这是最简单的垃圾收集算法。如果有零个引用指向它,则该对象会被认为是“垃圾收集” 。

看看下面的代码:

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.

 

周期引起问题

在周期方面有一个限制。例如下面的例子,创建两个对象并相互引用,这样会创建一个循环引用。在函数调用之后,它们将超出范围,所以它们实际上是无用的,可以被释放。然而,引用计数算法认为,由于两个对象中的每一个都被引用至少一次,所以两者都不能被垃圾收集机制收回。

function f() {
  var o1 = {};
  var o2 = {};
  o1.p = o2; // o1 references o2
  o2.p = o1; // o2 references o1. This creates a cycle.
}

f( );

 

 

标记和扫描算法

为了决定是否需要对象,标记和扫描算法会确定对象是否是活动的。

标记和扫描算法经过以下3个步骤:

  1. roots:通常,root是代码中引用的全局变量。例如,在JavaScript中,可以充当root的全局变量是“窗口”对象。Node.js中的相同对象称为“全局”。所有root的完整列表由垃圾收集器构建。
  2. 然后算法会检查所有root和他们的子对象并且标记它们是活动的(即它们不是垃圾)。任何root不能达到的,将被标记为垃圾。
  3. 最后,垃圾回收器释放所有未标记为活动的内存块,并将该内存返回给操作系统。

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

 

周期不再是问题了

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

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

 

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

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

  1. 大量的分配被执行。
  2. 大多数这些元素(或所有这些元素)被标记为无法访问(假设我们将一个引用指向不再需要的缓存)。
  3. 没有进一步的分配执行。

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

 

什么是内存泄漏?

内存泄漏是应用程序使用过的内存片段,在不再需要时,不能返回到操作系统或可用内存池中的情况。

编程语言有各自不同的内存管理方式。但是是否使用某一段内存,实际上是一个不可判定的问题。换句话说,只有开发人员明确的知道是否需要将一块内存返回给操作系统。

 

四种常见的JavaScript内存泄漏

1:全局变量

JavaScript以一种有趣的方式来处理未声明的变量:当引用未声明的变量时,会在全局对象中创建一个新变量。在浏览器中,全局对象将是window,这意味着

function foo(arg) {
    bar = "some text";
}

相当于:

function foo(arg) {
    window.bar = "some text";
}

bar只是foo函数中引用一个变量。如果你不使用var声明,将会创建一个多余的全局变量。在上述情况下,不会造成很大的问题。但是,如若是下面的这种情况。

你也可能不小心创建一个全局变量this:

function foo() {
    this.var1 = "potential accidental global";
}

// Foo called on its own, this points to the global object (window)
// rather than being undefined.
foo( );

你可以通过在JavaScript文件的开始处添加‘use strict’;来避免这中错误,这种方式将开启严格的解析JavaScript模式,从而防止意外创建全局变量。

意外的全局变量当然是一个问题。更多的时候,你的代码会受到显式的全局变量的影响,而这些全局变量在垃圾收集器中是无法收集的。需要特别注意用于临时存储和处理大量信息的全局变量。如果必须使用全局变量来存储数据,那么确保将其分配为空值,或者在完成后重新分配。

 

2:被遗忘的定时器或回调

下面列举setInterval的例子,这也是经常在JavaScript中使用。

对于提供监视的库和其它接受回调的工具,通常在确保所有回调的引用在其实例无法访问时,会变成无法访问的状态。但是下面的代码却是一个例外:

var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); //This will be executed every ~5 seconds.

上面的代码片段显示了使用引用节点或不再需要的数据的定时器的结果。

该renderer对象可能会在某些时候被替换或删除,这会使interval处理程序封装的块变得冗余。如果发生这种情况,那么处理程序及其依赖项都不会被收集,因为interval需要先停止。这一切都归结为存储和处理负载数据的serverData不会被收集的原因。

当使用监视器时,你需要确保做了一个明确的调用来删除它们。

幸运的是,大多数现代浏览器都会为你做这件事:即使你忘记删除监听器,当被监测对象变得无法访问,它们就会自动收集监测处理器。这是过去的一些浏览器无法处理的情况(例如旧的IE6)。

看下面的例子:

var element = document.getElementById('launch-button');
var counter = 0;

function onClick(event) {
   counter++;
   element.innerHtml = 'text ' + counter;
}

element.addEventListener('click', onClick);

// Do stuff

element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

// Now when element goes out of scope,
// both element and onClick will be collected even in old browsers // that don't handle cycles well.

由于现代浏览器支持垃圾回收机制,所以当某个节点变的不能访问时,你不再需要调用removeEventListener,因为垃圾回收机制会恰当的处理这些节点。

如果你正在使用jQueryAPI(其他库和框架也支持这一点),那么也可以在节点不用之前删除监听器。即使应用程序在较旧的浏览器版本下运行,库也会确保没有内存泄漏。

 

3:闭包

JavaScript开发的一个关键方面是闭包。闭包是一个内部函数,可以访问外部(封闭)函数的变量。由于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变量所持有的闭包所引用(这是theThing从以前的调用变量replaceThing)。需要记住的是,一旦在同一父作用域中为闭包创建了闭包的作用域,作用域就被共享了。

在这种情况下,闭包创建的范围会将someMethod共享给unused。然而,unused有一个originalThing引用。即使unused从未使用过,someMethod 也可以通过theThing在整个范围之外使用replaceThing。而且someMethod通过unused共享了闭包范围,unused必须引用originalThing以便使其它保持活跃(两封闭之间的整个共享范围)。这就阻止了它被收集。

所有这些都可能导致相当大的内存泄漏。当上面的代码片段一遍又一遍地运行时,你会看到内存使用率的不断上升。当垃圾收集器运行时,其内存大小不会缩小。这种情况会创建一个闭包的链表,并且每个闭包范围都带有对大数组的间接引用。

 

4:超出DOM引用

在某些情况下,开发人员会在数据结构中存储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() {
    // 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.
}

在涉及DOM树内的内部节点或叶节点时,还有一个额外的因素需要考虑。如果你在代码中保留对表格单元格(标签)的引用,并决定从DOM中删除该表格,还需要保留对该特定单元格的引用,则可能会出现严重的内存泄漏。你可能会认为垃圾收集器会释放除了那个单元之外的所有东西,但情况并非如此。由于单元格是表格的一个子节点,并且子节点保留着对父节点的引用,所以对表格单元格的这种引用,会将整个表格保存在内存中。

 

总结

以上内容是对JavaScript内存管理机制的讲解,以及常见的四种内存泄漏的分析。希望对JavaScript的编程人员有所帮助。

 

相关阅读:

【报表福利大放送】100余套报表模板免费下载

JavaScript 开发人员需要知道的简写技巧

1分钟选好最合适你的JavaScript框架

Top 10 JavaScript编辑器,你在用哪个?

 

原文地址:https://www.cnblogs.com/powertoolsteam/p/how-javascript-works-memory-management.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


阅读本文之前,分享大家一张图片,看图会发现JavaScript开发需求最高,占比达到42.84%,因此掌握JavaScript语言好工作就不愁啦,工欲善其事必先利其器,那么选择IDE来开发是至关重要的,本文指出常用的几款JavaScript IDE,分析其优缺点,如有不完善的请大家补充
Promises是一种关于异步编程的规范,目的是将异步处理对象和处理规则进行规范化,为异步编程提供统一接口。本文简要的介绍了Promises的基础知识,希望我们我们能够更好的使用Promises,更轻松的编写代码。
引子 Patrick Catanzariti 是一名Web开发工程师,最近他在 sitepoint 发表了《JavaScript Beyond the Web in 2014》,介绍了JavaScript在物联网中的应用,非常有意思。做为JavaScript的爱好者和从业者,我在这里把它翻译了,以飨
小编吐血整理加上翻译,太辛苦了~求赞! 本文主要总结了JavaScript 常用功能总结,如一些常用的JS 对象,基本数据结构,功能函数等,还有一些常用的设计模式。 目录: 众所周知,JavaScript是动态的面向对象的编程语言,能够实现以下效果: 1. 丰富Web 网页功能 2. 丰富Web界面
微软于今日(2015年12月10日)宣布即将开源Chakra核心控件,并改名为“ChakraCore”,该控件包含所有Edge JavaScript 引擎的所有核心功能。ChakraCore 将于下月发布在GitHub中。 Chakra提供了顶级的JavaScript处理功能,并具有非常强大的性能优
通过统计数据库中的1000多个项目,我们发现在 JavaScript 中最常出现的错误有10个。本文会向大家介绍这些错误发生的原因以及如何防止。
TypeScript 和 JavaScript 是目前项目开发中较为流行的两种脚本语言,我们已经熟知 TypeScript 是 JavaScript 的一个超集,但是 TypeScript 与 JavaScript 之间又有什么样的区别呢?在选择开发语言时,又该如何抉择呢?
本文是2017年 JavaScript 框架回顾系列的最后的一篇文章,主要介绍 JavaScript 的后端框架情况。
本文来源于多年的 JavaScript 编码技术经验,适合所有正在使用 JavaScript 编程的开发人员阅读。本文的目的在于帮助大家更加熟练的运用 JavaScript 语言来进行开发工作。
对于前端开发人员来说,如果能够掌握交互式网页中的数据可视化技术,则是一项很棒的技能。当然,通过一些 JavaScript 的图表库也会使前端的数据可视化变得更加容易。
几乎每隔一个星期,就有一个新的 JavaScript 库席卷网络社区!Web 社区日益活跃、多样,并在多个领域快速成长。想要研究每一个重要的 JavaScript 框架和库,是个不可能完成的任务。接下来,我会分享一些前端开发的最著名和最有影响力的框架和库。下面,就让我们一起来看看,顶级的 JavaS
AngularJ.js 由google开发,2009年首次发布 很流行的前端框架 使用Angular.js创建第一个UI,成本很低 对于团队来说,AngularJ.js有许多很棒的工具可用 很适合创建一个快速、混合型复杂的解决方案 比起React,更合适于创建小型企业级应用 由Google负责维护基
Javascript框架(以下简称框架)也被称为Javascript库,是一组包含丰富功能和函数的JavaScript代码集,能够帮助开发者快速完成Web设计和开发工作。随着Web社区的越发活跃,新的框架也层出不穷,目前流行的有:Angular、React、Vue.js和Knockout等。 面对如
对于 JavaScript 社区来说,npm 的主要功能之一就是帮助开发者发掘所需的 npm Registry 中的库和框架。npm 强大的搜索功能能够帮助找到一组相关的软件包,同时其内置的的文档和使用统计信息,可以帮助开发者决定使用哪一种软件包。
前言 SpreadJS作为一款性能出众的纯前端电子表格控件,自2015年发布以来,已经被广泛应用于各领域“在线Excel”数据管理项目中。NPM,作为管理Node.js库最有力的手段,解决了很多NodeJS代码部署的问题。 如今,为让您更方便的使用产品和更好地管理项目中的SpreadJS代码,我们已
前一篇文章中,我们介绍了2017年 JavaScript 框架的整体情况。我们也了解到在众多的前端框架中,目前最为庞大又在快速增长的当属React了,本文就来重点介绍React的生态系统。
ES2017标准已经于2017年6月份正式定稿了,并广泛支持最新的特性:异步函数。如果你曾经被异步JavaScript的逻辑困扰,这么新函数正是为你设计的。
本文将会讨论10个优秀的支持JavaScript,HTML5和CSS开发,并且可以使用Markdown进行文档编写的文本编辑器。
随着现在的编程语言功能越来越成熟、复杂,内存管理也容易被大家忽略。本文将会讨论JavaScript中的内存泄漏以及如何处理,方便大家在使用JavaScript编码时,更好的应对内存泄漏带来的问题。
JavaScript 作为当前最为常见的直译式脚本语言,已经广泛应用于 Web 应用开发中。为了提高Web应用的性能,从 JavaScript 的性能优化方向入手,会是一个很好的选择。本文从加载、上下文、解析、编译、执行和捆绑等多个方面来讲解 JavaScript 的性能优化技巧,以便让更多的前端开