1、内存的生命周期
内存分配:当我们申明变量、函数、对象的时候,系统会自动为他们分配内存
内存使用:即读写内存,也就是使用变量、函数等
内存回收:使用完毕,由垃圾回收机制自动回收不再使用的内存
2. Js 中的内存分配
1const n = 123; // 给数值变量分配内存2const s = "azerty"; // 给字符串分配内存34const o = {5 a: 1,6 b: null,7}; // 给对象及其包含的值分配内存
3. Js 中的内存使用
使用值的过程实际上是对分配内存进行读取与写入的操作。 读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
1var a = 10; // 分配内存2console.log(a); // 对内存的使用
4. Js 中的垃圾回收机制
垃圾回收算法主要依赖于引用的概念。
在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。
例如,一个 Javascript 对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
4.1 引用计数垃圾回收
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。 如果没有其他对象指向它了,说明该对象已经不再需了。
但它却存在一个致命的问题:循环引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。
4.2 标记清除算法
标记清除算法将“不再使用的对象”定义为“无法达到的对象”。 简单来说,就是从根部(在 JS 中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
4.2.1 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。
4.2.2 从根部出发将能触及到的对象的标记清除。
4.2.3 那些还存在标记的变量被视为准备删除的变量。
4.2.4 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。
5. 常见的内存泄露
5.1 全局变量
1function foo() {2 bar1 = "some text"; // 没有声明变量 实际上是全局变量 => window.bar13 this.bar2 = "some text"; // 全局变量 => window.bar24}5foo();
5.2 未被清理的定时器和回调函数
如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。
1var serverData = loadData();2setInterval(function () {3 var renderer = document.getElementById("renderer");4 if (renderer) {5 renderer.innerHTML = JSON.stringify(serverData);6 }7}, 5000); // 每 5 秒调用一次
5.3 闭包
在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露
1var theThing = null;2var replaceThing = function () {3 var originalThing = theThing;4 var unused = function () {5 if (originalThing)6 // 对于 'originalThing'的引用7 console.log("hi");8 };9 theThing = {10 longStr: new Array(1000000).join("*"),11 someMethod: function () {12 console.log("message");13 },14 };15};16setInterval(replaceThing, 1000);
这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。 同时 unused 是一个引用了 originalThing 的闭包。 这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。 当这段代码被反复执行时,内存会持续增长。
5.3 DOM 引用
很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中。
1var elements = {2 image: document.getElementById("image"),3};4function doStuff() {5 elements.image.src = "http://example.com/image_name.png";6}7function removeImage() {8 document.body.removeChild(document.getElementById("image"));9 // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.10}
上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
6. 如何避免内存泄露
1减少不必要的全局变量,使用严格模式避免意外创建全局变量。2在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。3组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。
7. 实现 sizeOf 函数, Get size of a JavaScript object in Bytes
1const seen = new WeakSet();23function sizeOfObject(object) {4 if (object == null) {5 return 0;6 }7 let bytes = 0;8 const properties = Object.keys(object);9 for (let i = 0; i < properties.length; i++) {10 const key = properties[i];11 // Do not recalculate circular references12 if (typeof object[key] === "object" && object[key] !== null) {13 if (seen.has(object[key])) {14 continue;15 }16 seen.add(object[key]);17 }18 bytes += calculator(key);19 bytes += calculator(object[key]);20 }21 return bytes;22}2324function calculator(object) {25 const objectType = typeof object;26 switch (objectType) {27 case "string":28 return object.length * 2;29 case "boolean":30 return 4;31 case "number":32 return 8;33 case "object":34 if (Array.isArray(object)) {35 return object.map(calculator).reduce(function (acc, curr) {36 return acc + curr;37 }, 0);38 } else {39 return sizeOfObject(object);40 }41 default:42 return 0;43 }44}4546const testData = {47 a: 111,48 b: "cccc",49 2222: false,50};5152console.log(calculator(testData));