js内存泄漏

目录

      • 从变量开始
        • 1. 变量分类
          • 基本数据类型
            • 问题:基本数据类型的值为什么不可以改变?
            • 1. 以数值类型变量来说:
            • 2. 以字符串类型来说
            • 小小注意点:
          • 引用数据类型
        • 2. 复制变量值
          • 基础数据类型
          • 引用数据类型
        • 3. 传递参数
      • 先到执行环境及作用域
        • 作用域和执行上下文
          • 解释:
          • 执行
      • 再到闭包
        • 闭包
      • 再到内存泄漏
        • 什么是内存泄漏?
        • `JavaScript`中的内存管理
          • 标记清除算法
            • 步骤
      • 闭包造成内存泄漏的情况
      • 最后是其他情况的内存泄漏
        • 1. 意外的全局变量
        • 2. 被遗忘的计时器或回调
        • 3. 超出`DOM`引用

从变量开始


1. 变量分类

基本数据类型
  • 指简单的数据段
  • 存储在栈中,栈为自动分配的内存,由系统自动释放。栈中每个变量的大小是一样的。
  • 不能为基本数据类型添加属性
  • 基本数据类型不可变
  • 在进行赋值运算时,实际上是在内存中新开辟了一段栈内存,故若存在var a = 3;a = 4的操作时,并不是改变了a的值,而是为a重新进行赋值,即在内存中新开辟了一段栈内存,然后将值赋值到新的空间中。
var str = "123";
str[0] = 4;
console.log(str);//123,并没有改变str
var a = 4;
a = 10;
console.log(a);//10 并不是改变了a的值,而是对a重新进行赋值操作
var b=a;
console.log(b);//10
问题:基本数据类型的值为什么不可以改变?
1. 以数值类型变量来说:
  • 如果我们同时定义int a = 3;int b =3,编译器先处理int a = 3

  • 首先它会在栈中创建一个变量为a的引用,然后查找有没有存放字面量为3的空间,如果没找到就开辟一个存放3的字面量的空间,然后将a指向3所在空间(实际上是在a中存储了3所在空间地址)。

  • 接着处理int b=3;。在创建完b的引用变量后,由于栈中已经存在了3这个字面量,便将b直接指向存在字面量3的空间。这样就出现了ab均指向3的情况

  • 特别注意的是:这种指向字面量的引用与指向类对象的引用不同。假定两个类对象的引用同时指向一个对象,如果一个对象引用变量修改了这个对象的内部状态,那么另一个对象引用变量也即可反映出变化。

  • 相反,通过字面量的引用来修改其值,不会导致另一个指向此字面量的引用的值也跟着改变的情况。例如var a = 3;var b= 3;a = 4;在定义完ab的值后,再令a=4;此时由于内存中没有存放字面量为4的空间,故会先创建存放字面量为4的空间,然后a指向该空间。故不会影响到b

2. 以字符串类型来说
  • 当通过索引改变字符串的某一个位置的字符时,实际上不会改变该字符串的值的。
  • 一般不建议用str[index]来去字符串中某个位置的字符,因为字符串的数组行为不标准且该方式不兼容ie6-ie8,使用[]方式也不容易区分字符串和数组,故最好采用charAt(index)的方式去访问某个位置的字符
//示例1
var myStr = "Bob";
myStr[0] = "J";
console.log(myStr); //Bob
小小注意点:

对于下面代码,建议使用直接赋值的方式来新建字符串。原因:

  • 采用第一种方式新建String对象,对象会存放在堆中,每new一次就会创建一个新的对象。而第二种方式,栈中存放字面量abc和对字面量的引用str,当之后仍有字面量取值为abc时可直接指向其所在空间,而不需要创建,有利于节省内存空间。
  • 能够在一定程度上提升程序的运行速度。当字面量存储在栈中,其值时可以共享的,并且由于栈访问速度更快,所以对于性能的提高有一定的帮助。而第一种方式每次都在堆中创建一个新的String对象,而不管其字符串值是否相等及是否有必要创建新对象,从而加重了程序的负担。并且堆的访问速度慢,对程序性能的影响也大
var str=new String('abc');
var str='abc';
引用数据类型
  • 多个值构成的对象
  • 内容存储在堆中,堆是动态分配的内存,指向堆的引用存储在栈中。系统不能自动释放堆中的内存。变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况进行特定的分配。
  • 可以为引用数据类型添加属性
  • 引用数据类型可变

2. 复制变量值

基础数据类型

对于基础数据类型。当对一个变量赋值为基础数据类型的值时,实际上会为该变量开辟一个新的栈内存,并将值赋值到该空间中。故在赋值后两个变量是互相不影响的。

var a = 10;
var b = a;

a ++ ;
console.log(a); // 11
console.log(b); // 10

js内存泄漏_第1张图片

引用数据类型

对于引用数据类型,在进行赋值时,实际上赋值的为变量的引用。当改变新变量的值时也会反映到旧变量对应值中。


    var a = [1,2,3];
    var b = a;
    a[0]=0;
    console.log(b);//[0,2,3]

js内存泄漏_第2张图片

3. 传递参数

ECMAScript中所有函数的参数都是按值传递的(无论是引用数据类型的变量还是基础数据类型的变量)

function addTen(num){
	num +=10;
	return num;
}
var count = 20;
var result = addTen(count);
console.log(count);//20
console.log(result);//30

这里的函数addTen()有一个参数num,当把count传入时,是将count的值赋值给num变量。故当改变num时不会影响count

function setName(obj){
	obj.name = "Zhang";
	obj = new Object();
	obj.name="yuan";
}
var person = new Object();
setName(person);
console.log(person.name);//Zhang

这里的函数setName()有一个参数obj,当把person传入时,是将person指向的堆空间的引用赋值给obj,而不是把person本身的引用赋值给obj这里可以理解为将栈中person的值赋值给objobjperson并不是相同的变量,而是指向同一片空间的两个变量),故当obj指向另一片堆空间时,并不会影响到person

先到执行环境及作用域


  • 每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个变量中。
  • 当代码在一个环境中执行时,会创建变量对象的作用域链(该作用域链会与执行环境绑定),作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。
  • 执行环境定义了环境内变量和对象有权访问的其他数据
  • 某个执行环境的所有代码执行完毕后,该环境会被销毁,保存在其中的所有变量和函数定义也随之销毁(全局环境直到应用程序退出之后才会被销毁(例如浏览器关闭之后)
  • 每个函数都有自己的执行环境。当执行流进入一个环境时,函数的环境会被推进一个环境站,在函数执行之后,栈将其环境弹出,把控制权交给之前的环境。
  • 在创建一个函数时,会先创建一个预先包含全局变量对象以及父级变量对象的作用域链,并将其保存在内部的[scope]属性中。当执行该函数时,会为函数创建一个执行环境,并将[scope]属性中的对象复制构建起执行环境的作用域链。此后,又会有一个活动对象(被作为变量对象使用)被推入作用域链的前端(下文会详细解释)。
  • 标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐渐开始向后回溯,直至找到标识符为止(如果找不到,通常会导致错误发生)。全局执行环境的变量对象始终都是作用域链中的最后一个变量对象
function compare(value1,value2){
	if(value1<value2){
		return -1;
    }else if(value1<value2){
		return 1;
	}else {
		return 0;
	}
}
var result = compare(1,2);

js内存泄漏_第3张图片

作用域和执行上下文

JavaScript的执行分为:解释执行两个阶段,这两个阶段所做的事并不一样:

解释:
  • 词法分析
  • 语法分析
  • 作用域规则确定
执行
  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

javascript的解释阶段就会确定函数的作用域,而非执行阶段。故当执行该函数时,先将当前环境的变量对象加入环境的作用域链的前端,然后需要去确定的作用域中逐层去查找取值,而不是在执行的作用域中逐层查找取值。

var x = 10
function fn() {
  console.log(x);//在函数创建时就确定了函数的作用域
}
function show(f) {
  var x = 20
  (function() {
    f() //10,而不是20
  })()
}
show(fn)

再到闭包


闭包

  • 指有权访问另一个函数作用域中的变量的函数
  • 一般来说,当函数执行完毕后,局部活动对象会被销毁,内存中仅保存全局作用域,但闭包是不同的。以下面例子来分析:当createFunction函数执行完毕后,其活动对象不会被销毁,因为匿名函数的作用域链中仍然在引用这个活动对象。换句话说,createFunction函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍会停留在内存中,直到匿名函数被销毁后,它的活动对象才会被销毁。
  • 由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。
function createFunction(propertyName){
	return function(obj1,obj2){
		var value1 = obj1[propertyName];
		var value2 = obj2[propertyName];
		if(value1<value2){
				return -1;	
		}else if(value1>value2){
				return 1;
		}else{
				 return 0;
		}
	}
}

再到内存泄漏


什么是内存泄漏?

本质上,内存泄漏可以定义为一个应用,由于某些原因不再需要的内存没有被操作系统或者空闲内存池回收。编程语言支持多种管理内存的方式。这些方式可能会减少内存泄漏的几率。然而,某一块内存是否没有用到实际上是一个不可判定的问题。换句话说,只有开发者可以弄清一块内存是否可以被操作系统回收。某些编程语言提供了帮助开发者做这个的特性。其他一些语言期望开发者可以完全明确什么时候一块内存是没被使用的。

JavaScript中的内存管理

JavaScript具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。JS的垃圾收集机制的原理是:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预订的收集时间),周期性地执行这一操作。

标记清除算法
  • JS中常用的的垃圾收集方式是标记清除(Mark-and-sweep)。当变量进入环境时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放在环境中的变量所占用的内存。因为只要进入到环境,就会有可能用到它。当变量离开环境时,会将其标记为“离开环境”,在垃圾收集器工作时(一般垃圾收集器在内存不够时工作),会将标记为“离开环境”的变量所占用的内存收回。
  • 栈空间由操作系统分配并由操作系统自动收回。而基本变量类型是存储在栈中的,故当变量所处的栈空间被系统收回时,其中的基本变量类型的变量的空间也都被收回了(引用类型变量在栈中存储实际堆中的内容地址,当栈空间被收回时,指向堆的引用数减1,若其他地方不再引用堆中的内容,则该内容所占空间会被释放,否则不可以被释放
步骤
  • 垃圾回收器建立一个根结点的列表。根结点通常是代码中一个一直在的引用对应的全局变量。在JavaScript中,Window对象是一个作为根结点的全局变量的例子。
  • 所有的根结点被检查并且标记为活跃。所有子节点同样被递归检查,每个可以从根结点到达的节点不会被认为是垃圾。
  • 所有没被标记为活跃的内存现在可以被认为是垃圾。回收期可以释放掉那块内存并且归还给操作系统。

闭包造成内存泄漏的情况


  • 实际上,闭包并不会引起内存泄漏,只是由于IE9之前的版本对JScript对象和COM(其中BOMDOM中的对象就是使用c++COM对象的形式实现的)对象使用不同的垃圾收集(JavaScript对象使用标记清除算法,COM对象使用引用计数算法),从而导致内存无法进行回收,这是IE的问题,而非闭包的问题。具体来说,如果闭包的作用域链中保存着一个HTML 元素,那么就意味着该元素将无法被销毁
  • IE9以前,无用的JavaScript对象的循环引用可以被垃圾回收器识别并回收,但是只要循环引用中包含COM对象,垃圾回收器是无法将其识别并回收的(COM对象使用引用计数算法进行垃圾回收)
function assignHandler(){
	var element = document.getElementById("test");
	element.onclick = function(){
		alert(element.id);
	}
}

以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包又创建了一个循环引用:闭包内引用了element元素,element元素中又包含了闭包(闭包在element元素的onclick属性上),故element元素的引用数至少为1,故其不会被回收。

function assignHandler(){
	var element = document.getElementById("test");
	var id = element.id;
	element.onclick = function(){
		alert(id);
	}
	element= null;
}

以上代码通过赋值将element.id的副本保存在变量id变量中,在闭包中并不是去访问element,而是直接访问id,故消除了循环引用。但这样还是不能解决内存泄漏的问题,因为该闭包的作用域链上包含其所在函数的变量对象。而变量对象中包含了element元素,故DOM元素的引用数至少为1.因此,需要将element设置为null,才能解除对DOM对象的引用,顺利地减少引用数,确保正常回收其占用的内存(当该dom元素在页面移除时可能会出现无用的情况

最后是其他情况的内存泄漏


1. 意外的全局变量

  • JS中不进行声明的变量会成为全局变量,它在应用程序退出之前不会被销毁。若不慎创建了一个无用的全局变量,且该变量存储了大量的数据,则会造成内存泄漏
  • 避免:在JavaScript文件最前面添加use strict。在严格模式下不允许使用未声明的变量。
//两种方式都是创建了全局变量
function foo(arg) {
	bar = "this is a hidden global variable";
}

function foo() {
	this.variable = "potential accidental global";//this==window
}
foo();

2. 被遗忘的计时器或回调

  • 若在程序中某个计时器中所需要的引用对象不再存在时,该计时器也就没有作用了,此时必须要将该计时器移除移除,否则会造成内存泄漏。
  • 观察者情况下,一旦观察对象不被需要(或相关的对象快要访问不到)就创建明确移除他们的处理函数很重要(在过去,这由于特定浏览器(IE6)不能很好的管理循环引用,曾经尤为重要)。现如今,一旦观察对象变成不可访问的,即使观察者处理函数(观察者)没有明确地被移除,多数浏览器可以并会回收观察者处理函数。
var someResource = getData();
setInterval(function() {
var node = document.getElementById('Node');
if(node) {
node.innerHTML = JSON.stringify(someResource));
}
}, 1000);

对于上述代码,当node被移除时,该计时器便没有实际作用。由于计时器并没有被清除,故其不能被回收,其依赖也不能被回收(即someResource),若someResource中存储了大量的数据,那么它会导致内存泄漏

var element = document.getElementById('button');
function onClick(event) {
element.innerHtml = 'text';
}
element.addEventListener('click', onClick);//为观察对象绑定观察处理函数
element.removeEventListener('click', onClick);//在观察对象被移除之前移除观察者处理函数
element.parentNode.removeChild(element);

在上述代码中,在观察对象element被移除之前先移除了观察者处理函数,这样就不会在IE9之前由于COM对象与JS的代码间的循环引用不会被检测到从而导致内存泄漏。
像Jquery一样的框架和库做了在处置一个节点前(当为其使用特定的API的时候)移除监听者的工作。这被在库内部处理,即使在像老版本IE一样有问题的浏览器里面跑,也会确保没有泄漏产生。

3. 超出DOM引用

  • 有时保存DOM节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行DOM存在字典中(JSON键值对)或者数组很意义(每次查找DOM都需要一定的时间消耗)。此时,同样的DOM元素存在两个引用,一个在DOM树中,另一个在字典中。此后如果想要删除这些行时,需要将两处引用都清除,否则会导致内存泄漏
  • 还要考虑DOM树内部或子节点的引用问题。如果此时JS代码中保存了表格某一个的引用。将来决定删除整个表格时,直观上以为只会删除除该以外的其他节点。但是事实是:是表格的子节点,子元素和父元素是引用关系。由于代码保留了的引用,导致整个表格仍待在内存中(简单来说就是会因为一个子节点保留,而导致整个表格都保留,因为子节点会引用父节点,父节点的引用数至少为1))。故保存DOM元素的引用的时候,要小心谨慎。
var elements = {
    button: document.getElementById('button'),
    image: document.getElementById('image'),
    text: document.getElementById('text')
};
function doStuff() {
    elements .image.src = 'http://some.url/image';
    elements .button.click();
    console.log(elements .text.innerHTML);
    // 更多逻辑
}
function removeButton() {
    // 按钮是 body 的后代元素
    document.body.removeChild(document.getElementById('button'));
    // 此时,仍旧存在一个全局的 #button 的引用
    // elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}

参考:
4类 JavaScript 内存泄漏及如何避免
JavaScript高级程序设计
部分图源js深拷贝vs浅拷贝
如何理解js中基本数据类型的值不可变

你可能感兴趣的:(javascript)