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、为了防止这些错误的发生,可以在 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 对象上就不会产生中间临时对象。
原文地址:https://www.cnblogs.com/goloving/p/11135426.html