整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

javascript 的垃圾回收机制

javascript 的垃圾回收机制

S自带一套内存管理引擎,负责创建对象、销毁对象,以及垃圾回收。这期探讨一下垃圾回收机制。垃圾回收机制主要是由一个叫垃圾收集器(garbage collector,简称GC)的后台进程负责监控、清理对象,并及时回收空闲内存。

和C#、Java一样JavaScript有自动垃圾回收机制,也就是说执行环境会负责管理代码执行过程中使用的内存,在开发过程中就无需考虑内存分配及无用内存的回收问题了。
而 JavaScript 在创建对象(对象、字符串等)时会为它们分配内存,不再使用时会“自动”释放内存,这个过程称为垃圾收集。

内存生命周期中的每一个阶段:

分配内存 —? 内存是由操作系统分配的,它允许您的程序使用它。在低级语言(例如 C 语言)中,这是一个开发人员需要自己处理的显式执行的操作。然而,在高级语言中,系统会自动为你分配内在。
使用内存 — 这是程序实际使用之前分配的内存,在代码中使用分配的变量时,就会发生读和写操作。
释放内存 — 释放所有不再使用的内存,使之成为自由内存,并可以被重新利用。与分配内存操作一样,这一操作在低级语言中也是需要显式地执行。

四种常见的内存泄漏:全局变量,未清除的定时器,闭包,以及 dom 的引用

  1. 全局变量 不用 var 声明的变量,相当于挂载到 window 对象上。如:b=1; 解决:使用严格的模式
  2. 被遗忘的定时器和回调函数
  3. 闭包
  4. 没有清理的 DOM 元素引用

S垃圾收集机制

JS会在创建变量时自动分配内存,在不使用地时候会自动周期性的释放内存,释放的过程就叫 "垃圾回收"。这个机制有好的一面,当然也也有不好的一面。一方面自动分配内存减轻了开发者的负担,开发者不用过多的去关注内存使用,但是另一方面,正是因为因为是自动回收,所以如果不清楚回收的机制,会很容易造成混乱,而混乱就很容易造成"内存泄漏".由于是自动回收,所以就存在一个 "内存是否需要被回收的" 的问题,但是这个问题的判定在程序中意味着无法通过某个算法去准确完整的解决,后面探讨的回收机制只能有限地去解决一般的问题。

回收算法

垃圾回收对是否需要回收的问题主要依赖于对变量的判定是否可访问,由此衍生出两种主要的回收算法:

  • 标记清理
  • 引用计数

标记清理

标记清理是js最常用的回收策略,2012年后所有浏览器都使用了这种策略,此后的对回收策略的改进也是基于这个策略的改进。其策略是:

  1. 变量进入上下文,也可理解为作用域,会加上标记,证明其存在于该上下文;
  2. 将所有在上下文中的变量以及上下文中被访问引用的变量标记去掉,表明这些变量活跃有用;
  3. 在此之后再被加上标记的变量标记为准备删除的变量,因为上下文中的变量已经无法访问它们;
  4. 执行内存清理,销毁带标记的所有非活跃值并回收之前被占用的内存;

局限

  • 由于是从根对象(全局对象)开始查找,对于那些无法从根对象查询到的对象都将被清除
  • 回收后会形成内存碎片,影响后面申请大的连续内存空间

引用计数

引用计数策略相对而言不常用,因为弊端较多。其思路是对每个值记录它被引用的次数,通过最后对次数的判断(引用数为0)来决定是否保留,具体的规则有

  • 声明一个变量,赋予它一个引用值时,计数+1;
  • 同一个值被赋予另外一个变量时,引用+1;
  • 保存对该值引用的变量被其他值覆盖,引用-1;
  • 引用为0,回收内存;

局限

最重要的问题就是,循环引用 的问题

function refProblem () {
	let a=new Object();
	let b=new Object();
	a.c=b;
	b.c=a;  //互相引用
}
复制代码

根据之前提到的规则,两个都互相引用了,引用计数不为0,所以两个变量都无法回收。如果频繁地调用改函数,则会造成很严重的内存泄漏。

Nodejs V8回收机制

V8的回收机制基于 分代回收机制 ,将内存分为新生代(young generation)和老生代(tenured generation),新生代为存活时间较短的对象,老生代为存活时间较长或者常驻内存的变量。

V8堆的构成

V8将堆分成了几个不同的区域

  • 新生代(New Space/Young Generation): 大多数新生对象被分配到这,分为两块空间,整体占据小块空间,垃圾回收的频率较高,采用的回收算法为 Scavenge 算法
  • 老生代(Old Space/Old Generation):大多数在新生区存活一段时间后的对象会转移至此,采用的回收算法为 标记清除 & 整理(Mark-Sweep & Mark-Compact,Major GC) 算法,内部再细分为两个空间 指针空间(Old pointer space): 存储的对象含有指向其他对象的指针 数据空间(Old data space):存储的对象仅包含数据,无指向其他对象的指针
  • 大对象空间(Large Object Space):存放超过其他空间(Space)限制的大对象,垃圾回收器从不移动此空间中的对象
  • 代码空间(Code Space): 代码对象,用于存放代码段,是唯一拥有执行权限的内存空间,需要注意的是如果代码对象太大而被移入大对象空间,这个代码对象在大对象空间内也是拥有执行权限的,但不能因此说大对象空间也有执行权限
  • Cell空间、属性空间、Map空间 (Cell ,Property,Map Space): 这些区域存放Cell、属性Cell和Map,每个空间因为都是存放相同大小的元素,因此内存结构很简单。

Scavenge 算法

Scavenge 算法是新生代空间中的主要算法,该算法由 C.J. Cheney 在 1970 年在论文 A nonrecursive list compacting algorithm 提出。 Scavenge 主要采用了 Cheney算法,Cheney算法新生代空间的堆内存分为2块同样大小的空间,称为 Semi space,处于使用状态的成为 From空间 ,闲置的称为 To 空间。垃圾回收过程如下:

  • 检查From空间,如果From空间被分配满了,则执行Scavenge算法进行垃圾回收
  • 如果未分配满,则检查From空间的是否有存活对象,如果无存活对象,则直接释放未存活对象的空间
  • 如果存活,将检查对象是否符合晋升条件,如果符合晋升条件,则移入老生代空间,否则将对象复制进To空间
  • 完成复制后将From和To空间角色互换,然后再从第一步开始执行

晋升条件

  1. 经历过一次Scavenge 算法筛选;
  2. To空间内存使用超过25%;

标记清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)算法

之前说过,标记清除策略会产生内存碎片,从而影响内存的使用,这里 标记整理算法(Mark-Compact)的出现就能很好的解决这个问题。标记整理算法是在 标记清除(Mark-Sweep )的基础上演变而来的,整理算法会将活跃的对象往边界移动,完成移动后,再清除不活跃的对象。

由于需要移动移动对象,所以在处理速度上,会慢于Mark-Sweep。

全停顿(Stop The World )

为了避免应用逻辑与垃圾回收器看到的逻辑不一样,垃圾回收器在执行回收时会停止应用逻辑,执行完回收任务后,再继续执行应用逻辑。这种行为就是 全停顿,停顿的时间取决于不同引擎执行一次垃圾回收的时间。这种停顿对新生代空间的影响较小,但对老生代空间可能会造成停顿的现象。

增量标记(Incremental Marking)

为了解决全停顿的现象,2011年V8推出了增量标记。V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直至标记完成。

内存泄漏

内存泄漏的问题难以察觉,在函数被调用很多次的情况下,内存泄漏可能是个大问题。常见的内存泄漏主要有下面几个场景。

意外声明全局变量

function hello (){
	name='tom'
}
hello();
复制代码

未声明的对象会被绑定在全局对象上,就算不被使用了,也不会被回收,所以写代码的时候,一定要记得声明变量。

定时器

let name='Tom';
setInterval(()=> {
  console.log(name);
}, 100);
复制代码

定时器的回调通过闭包引用了外部变量,如果定时器不清除,name会一直占用着内存,所以用定时器的时候最好明白自己需要哪些变量,检查定时器内部的变量,另外如果不用定时器了,记得及时清除定时器。

闭包

let out=function() {
  let name='Tom';
  return function () {
    console.log(name);
  }
}
复制代码

由于闭包会常驻内存,在这个例子中,如果out一直存在,name就一直不会被清理,如果name值很大的时候,就会造成比较严重的内存泄漏。所以一定要慎重使用闭包。

事件监听

mounted() {
window.addEventListener("resize",  ()=> {
	//do something
});
}
复制代码

在页面初始化时绑定了事件监听,但是在页面离开的时候未清除事监听,就会导致内存泄漏。

最后

文章为参考资料总结的笔记文章,我最近在重学js,会将复习总结的文章记录在Github,有想一起复习的小伙伴可私信一起参与复习总结!

过去,我们浏览静态网站时无须过多关注内存管理,因为加载新页面时,之前的页面信息会从内存中删除。 然而,随着单页Web应用(SPA)的兴起,应用程序消耗的内存越来越多,这不仅会降低浏览器性能,甚至会导致浏览器卡死。

因此,在编码实践中,开发人员需要更加关注与内存相关的内容。因此,小编今天将为大家介绍JavaScript内存泄漏的编程模式,并提供一些内存管理的改进方法。

什么是内存泄漏以及如何发现它?

什么是内存泄漏?

JavaScript对象被保存在浏览器内存的堆中,并通过引用方式访问。值得一提的是,JavaScript垃圾回收器则运行于后台,并通过识别无法访问的对象来释放并恢复底层存储空间,从而保证JavaScript引擎的良好运行状态。

当内存中的对象在垃圾回收周期中应该被清理时,若它们被另一个仍然存在于内存中的对象通过一个意外的引用所持有,就会引发内存泄漏问题。这种情况下,冗余对象会继续占据内存空间,导致应用程序消耗过多的内存资源,并可能导致性能下降和表现不佳的情况出现。因此,及时清理无用对象并释放内存资源是至关重要的,以确保应用程序的正常运行和良好的性能表现。

如何发现内存泄漏?

那么如何知道代码中是否存在内存泄漏?内存泄漏往往隐蔽且很难检测和定位。即使代码中存在内存泄漏,浏览器在运行时也不会返回任何错误。如果注意到页面的性能逐渐下降,可以使用浏览器内置的工具来确定是否存在内存泄漏以及是哪个对象引起的。

任务管理器(不要与操作系统的任务管理器混淆)提供了浏览器中所有选项卡和进程的概览。Chrome 中,可以通过在 Linux 和 Windows 操作系统上按 Shift+Esc 来打开任务管理器;而在 Firefox 中,通过在地址栏中键入 about:performance 则可以访问内置的管理器,它可以显示每个标签的 JavaScript 内存占用情况。如果网站停留在那里什么都不做,但 JavaScript内存使用量逐渐增加,那很可能是存在内存泄漏。

开发者工具提供了一些先进的内存管理方法,例如,使用Chrome浏览器的性能记录工具,可以对页面的性能进行可视化分析。在这个过程中,可以通过一些指标来判断是否存在内存泄漏问题,比如堆内存使用量增加的情况,并及时采取措施解决这些问题,以确保应用程序的正常运行和良好的性能表现。

另外,通过Chrome和Firefox的开发者工具提供的内存工具,可以进一步探索内存使用情况。队列内存使用快照的比较可以显示在两个快照之间分配了多少内存以及分配的位置,并提供额外信息来帮助识别代码中存在问题的对象。这些工具为开发者提供了便利,能够更好地进行内存管理和性能优化,提高应用程序的质量和性能。

JavaScript代码中常见的内存泄漏的常见来源:

研究内存泄漏问题就相当于寻找符合垃圾回收机制的编程方式,有效避免对象引用的问题。下面小编就为大家介绍几个常见的容易导致内存泄漏的地方:

1.全局变量

全局变量始终存储在根目录下,且永远不会被回收。而在JavaScript的开发中,一些错误会导致局部变量被转换到了全局,尤其是在非严格的代码模式下。下面是两个常见的局部变量被转化到全局变量的情况:

  1. 为未声明的变量赋值
  2. 使用this指向全局对象。
function createGlobalVariables() {
  leaking1='I leak into the global scope'; // 为未声明的变量赋值
  this.leaking2='I also leak into the global scope'; // 使用this指向全局对象
};
createGlobalVariables();
window.leaking1; 
window.leaking2; 

注意:严格模式("use strict")将帮助您避免上面示例中的内存泄漏和控制台错误。

2.闭包

函数中定义的变量会在函数退出调用栈并且在函数外部没有指向它的引用时被清除。而闭包则会保持被引用的变量一直存在,即便函数的执行已经终止。

function outer() {
  const potentiallyHugeArray=[];

  return function inner() {
    potentiallyHugeArray.push('Hello'); // function inner is closed over the potentiallyHugeArray variable
    console.log('Hello');
  };
};
const sayHello=outer(); // contains definition of the function inner

function repeat(fn, num) {
  for (let i=0; i < num; i++){
    fn();
  }
}
repeat(sayHello, 10); // each sayHello call pushes another 'Hello' to the potentiallyHugeArray

// now imagine repeat(sayHello, 100000)

在这个例子中,potentiallyHugeArray从未被任何函数返回,也无法被访问,但它的大小会随着调用 inner 方法的次数而增长。

3.定时器

在JavaScript中,使用使用 setTimeout 或 setInterval函数引用对象是防止对象被垃圾回收的最常见方法。当在代码中设置循环定时器(可以使 setTimeout 表现得像 setInterval,即使其递归)时,只要回调可调用,定时器回调对象的引用就会永远保持活动状态。

例如下面的这段代码,只有在移除定时器后,data对象才会被垃圾回收。在没有移除setInterval之前,它永远不会被删除,并且data.hugeString 会一直保留在内存中,直到应用程序停止。

function setCallback() {
  const data={
    counter: 0,
    hugeString: new Array(100000).join('x')
  };

  return function cb() {
    data.counter++; // data object is now part of the callback's scope
    console.log(data.counter);
  }
}

setInterval(setCallback(), 1000); // how do we stop it?

那么应该如何避免上述这种情况的发生呢?可以从以下两个方法入手:

  1. 注意定时器回调引用的对象。
  2. 必要时取消定时器。

如下方的代码所示:

function setCallback() {
  // 'unpacking' the data object
  let counter=0;
  const hugeString=new Array(100000).join('x'); // gets removed when the setCallback returns

  return function cb() {
    counter++; // only counter is part of the callback's scope
    console.log(counter);
  }
}

const timerId=setInterval(setCallback(), 1000); // saving the interval ID

// doing something ...

clearInterval(timerId); // stopping the timer i.e. if button pressed

4.事件监听

活动的事件监听器会阻止其范围内的所有变量被回收。一旦添加,事件监听器会一直生效,直到下面两种情况的发生:

  1. 通过 removeEventListener() 移除。
  2. 相关联的 DOM 元素被移除。

在下面的示例中,使用匿名内联函数作为事件监听器,这意味着它不能与 removeEventListener() 一起使用。此外,由于document 不能被移除,触发方法中的内容会一直驻留内存,即使只使用它触发一次。

const hugeString=new Array(100000).join('x');

document.addEventListener('keyup', function() { // anonymous inline function - can't remove it
  doSomething(hugeString); // hugeString is now forever kept in the callback's scope
});

那么如何避免这种情况呢?可以通过removeEventListener()释放监听器:

function listener() {
  doSomething(hugeString);
}

document.addEventListener('keyup', listener); // named function can be referenced here...
document.removeEventListener('keyup', listener); // ...and here 

如果事件监听器只需要运行一次,addEventListener() 可以带有第三个参数,一个提供附加选项的对象。只要将 {once: true} 作为第三个参数传递给 addEventListener(),监听器将在事件处理一次后自动删除。

document.addEventListener('keyup', function listener() {
  doSomething(hugeString);
}, {once: true}); // listener will be removed after running once

5.缓存

如果不断向缓存中添加内容,而未使用的对象也没有移除,也没有限制缓存的大小,那么缓存的大小就会无限增长:

let user_1={ name: "Peter", id: 12345 };
let user_2={ name: "Mark", id: 54321 };
const mapCache=new Map();

function cache(obj){
  if (!mapCache.has(obj)){
    const value=`${obj.name} has an id of ${obj.id}`;
    mapCache.set(obj, value);

    return [value, 'computed'];
  }

  return [mapCache.get(obj), 'cached'];
}

cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_1); // ['Peter has an id of 12345', 'cached']
cache(user_2); // ['Mark has an id of 54321', 'computed']

console.log(mapCache); // ((…)=> "Peter has an id of 12345", (…)=> "Mark has an id of 54321")
user_1=null; // removing the inactive user

// Garbage Collector
console.log(mapCache); // ((…)=> "Peter has an id of 12345", (…)=> "Mark has an id of 54321") // first entry is still in cache

为了解决这个问题,需要清除不需要的缓存:

一种有效的解决内存泄漏问题的方法是使用WeakMap。它是一种数据结构,其中键引用被保持为弱引用,并且仅接受对象作为键。如果使用对象作为键,并且它是唯一引用该对象的引用,相关条目将从缓存中移除,并进行垃圾回收。在下面的示例中,当替换user_1后,与之关联的条目将在下一次垃圾回收时自动从WeakMap中移除。

let user_1={ name: "Peter", id: 12345 };
let user_2={ name: "Mark", id: 54321 };
const weakMapCache=new WeakMap();

function cache(obj){
  // ...same as above, but with weakMapCache

  return [weakMapCache.get(obj), 'cached'];
}

cache(user_1); // ['Peter has an id of 12345', 'computed']
cache(user_2); // ['Mark has an id of 54321', 'computed']
console.log(weakMapCache); // ((…)=> "Peter has an id of 12345", (…)=> "Mark has an id of 54321"}
user_1=null; // removing the inactive user

// Garbage Collector

console.log(weakMapCache); // ((…)=> "Mark has an id of 54321") - first entry gets garbage collected

结论

对于复杂的应用程序,检测和修复 JavaScript 内存泄漏问题可能是一项非常艰巨的任务。了解内存泄漏的常见原因以防止它们发生是非常重要的。在涉及内存和性能方面,最重要的是用户体验,这才是最重要的。