浅析内存泄漏问题总结:内存生命周期、JS中的分配内存使用内存释放内存及4种常见内存泄漏(全局变量、定时器、闭包、dom引用)

1、内存的生命周期

  无论你使用那种语言,内存的生命周期基本是都差不多:分配内存 —— 使用内存 —— 释放内存,以下是生命周期中每一步发生了什么的一个概述:

  Allocate memory —— 操作系统分配内存,允许你的程序使用它。在基础语言中(例如 C ),这是一个开发者自己处理的明确操作。然而,在高级语言中,它已经为你处理了。

  Use memory —— 现在你就可以使用之前分配好的内存了。当你在代码中使用变量时,读 和 写 的操作正在发生。

  Release memory —— 现在该释放你不再需要的内存了,以便它们能够被再次使用。与分配内存的操作一样,这种操作在基础语言中是明确执行的。

2、JavaScript 中的分配内存

  现在,我们将要解释 JavaScript 中第一步(分配内存)是如何工作的。

  JavaScript 解放了开发者处理内存分配的责任——JavaScript 自己在声明 values 时就做了这件事

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);

  js中使用内存:基本上在 JavaScript 中使用内存的意思就是在内存在进行 读 和 写。这个操作可能是一个变量值的读取或写入,一个对象属性的读取或写入,甚至时向函数中传递参数。

  当内存不需要时释放内存:大多数的内存管理问题发生在这个阶段。这里最困难的任务就是确定内存何时就不再被需要了。它通常需要开发人员确定程序中哪里不再需要这样的内存,并释放它。

  高级语言拥有垃圾回收器,它的职责就是追踪内存分配和使用情况,找到不再被使用的内存,然后自动地释放它。不幸的是,这个过程只能得到一个近视的值,因为内存是否被需要是不可判定的(不能用算法求解)。大多数垃圾回收器通过判断内存是否能够被再次访问来工作的,例如:指向它的所有变量都超出了作用域。然而,这只能得到一个近似值。因为在任何位置,存储器位置可能仍然具有指向其范围的变量,但是它可能将永远不会被再次访问了。

3、垃圾回收

  垃圾回收语言中的泄漏的主要原因是不必要的引用。垃圾回收算法依靠的主要概念就是引用。

  引用计数垃圾回收法:如果一个对象指向它的引用对象数为0,那么就该垃圾回收了

  循环依赖造成无法垃圾回收的情况

  标记扫描算法:解决循环依赖的问题

  即对象不可达,就垃圾回收

  该算法由以下步骤组成:

  (1)垃圾回收器构建“roots”列表。Roots 通常是代码中保留引用的全局变量。在 JavaScript 中,“window” 对象可以作为 root 全局变量示例。

  (2)所有的 roots 被检查并标记为 active(即不是垃圾)。所有的 children 也被递归检查。从 root 能够到达的一切都不被认为是垃圾。

  (3)所有为被标记为 active 的内存可以被认为是垃圾了。收集器限制可以释放这些内存并将其返回到操作系统。

  这个算法优于前一个,因为“一个对象零引用”会让这个对象不是可达的。反过来就不一定对了,因为存在循环引用。

4、四种常见的内存泄漏

(1)意外的全局变量以及显式的全局变量

  意外的全局变量,常见的就是:1、未声明的变量;2、误用 this 指针,比如下面

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

  解决方案:

  1、为了防止这些错误的发生,可以在 JavaScript 文件开头添加 “use strict”,使用严格模式。这样在严格模式下解析 JavaScript 可以防止意外的全局变量。

  2、即使我们讨论了如何预防意外全局变量的产生,但是仍然会有很多代码用显式的方式去使用全局变量。这些全局变量是无法进行垃圾回收的(除非将它们赋值为 null 或重新进行分配)。特别是用来临时存储和处理大量信息的全局变量非常值得关注。如果你必须使用全局变量来存储大量数据,那么,请确保在使用完之后,对其赋值为 null 或者重新分配。

(2)timers 与 callbacks

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.

  这个例子阐述着 timers 可能发生的情况:计时器会引用不再需要的节点或数据。

  renderer 可能在将来会被移除,使得 interval 内的整个块都不再被需要。但是,interval handler 因为 interval 的存活,所以无法被回收(需要停止 interval,才能回收)。如果 interval handler 无法被回收,则它的依赖也不能被回收。这意味着 serverData——可能存储了大量数据,也不能被回收。在观察者模式下,重要的是在他们不再被需要的时候显式地去删除它们(或者让相关对象变为不可达)。

  还有一种定时器泄漏如下

var val = 0;
for (var i = 0; i < 90000; i++) {
  var buggyObject = {
    callAgain: function() {
      var ref = this;
      val = setTimeout(function() {
        ref.callAgain();
      }, 90000);
  }
}
buggyObject.callAgain();

  如果你想回收buggyObject,给它设为:buggyObject = null;   //虽然你想回收但是timer还在,所以还是回收不了

//解决方法,先停止定时器
clearTimeout(val);
buggyObject = null;

  所以一定要是先清除子集的内存,再清除本集的内存,才可以垃圾回收。dom引用造成的泄漏也会出现这样的问题。

(3)闭包导致的内存泄漏

(4)dom引用

  有时候,在数据结构中存储 DOM 结构是有用的。假设要快速更新表中的几行内容。将每行 DOM 的引用存储在字典或数组中可能是有意义的。当这种情况发生时,就会保留同一 DOM 元素的两份引用:一个在 DOM 树种,另一个在字典中。如果将来某个时候你决定要删除这些行,则需要让两个引用都不可达。

  还有一个额外的考虑,当涉及 DOM 树内部或叶子节点的引用时,必须考虑这一点。假设你在 JavaScript 代码中保留了对 table 特定单元格(<td>)的引用。有一天,你决定从 DOM 中删除该 table,但扔保留着对该单元格的引用。直观地来看,可以假设 GC 将收集除了该单元格之外所有的内容。实际上,这不会发生的:该单元格是该 table 的子节点,并且 children 保持着对它们 parents 的引用。也就是说,在 JavaScript 代码中对单元格的引用会导致整个表都保留在内存中的。保留 DOM 元素的引用时,需要仔细考虑。

  当原有的DOM被移除时,子结点引用没有被移除则无法回收

let select = document.querySelector;
let treeRef = select('#tree');

let leafRef = select('#leaf');   //在DOM树中leafRef是treeFre的一个子结点
select('body').removeChild(treeRef);//#tree不能被回收入,因为treeRef还在

// 解决方法
treeRef = null;  //tree还不能被回收,因为叶子结果leafRef还在
leafRef = null;  //现在#tree可以被释放了

  DOM 插入顺序导致内存泄漏

  当动态创建的 2 个不同范围的 DOM 对象附加到一起的时候,一个临时的对象会被创建。这个 DOM 对象改变范围到 document 时,那个临时对象就没用了,这个临时对象没有被回收将导致内存泄漏。如果我们将这两个DOM添加到原有的 DOM 对象上就不会产生中间临时对象。

posted @ 2019-07-04 22:52  古兰精  阅读(2182)  评论(0编辑  收藏  举报