常见的内存泄漏场景

内存泄漏Memory Leak是指程序中已动态分配的堆内存由于疏忽或错误等原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。对于内存泄露的检测,Chrome提供了性能分析工具Performance,可以比较方便的查看内存的占用情况等。

内存回收机制

C语言这样的底层语言一般都有底层的内存管理接口,例如malloc()free()等,对于JavaScript而言在创建变量时其会自动进行分配内存,并且在不使用它们时自动释放。在Js七种基本类型中的引用类型Object的变量其占据内存空间大且大小不固定,在堆内存中实际存储对象,在栈内存中存储对象的指针,对于对象的访问是按引用访问的。在栈区中执行的变量等是通过值访问,当其作用域销毁后变量也就随之销毁,而使用引用访问的堆区变量,在一个作用域消失后还可能在外层作用域或者其他作用域仍然存在引用,不能直接销毁,此时就需要通过算法计算该堆区变量是否属于不再需要的变量,从而决定是否需要进行内存回收,在Js中主要有引用计数与标记清除两种垃圾回收算法。

引用计数算法

对于引用计数垃圾回收算法,把对象是否不再需要简化定义为该对象有没有其他变量或对象引用到它,如果没有引用指向该对象,该对象将被垃圾回收机制回收。在这里,对象的概念不仅特指JavaScript对象,还包括函数作用域或者全局词法作用域。引用计数垃圾回收算法使用比较少,主要是在IE6IE7等低版本IE浏览器中使用。

var obj = {
a : {
b: 11
}
}
// 此时两个对象被创建,一个作为另一个的a属性被引用称为对象1,另一个被obj变量引用称为对象2
// 此时两个对象都有被引用的变量,都不能回收内存 var obj2 = obj;
// 此时对于obj所引用的对象2,已经有obj与Obj2两个变量的引用 obj = null;
// 将obj对于对象2的引用解除,此时对象2还存在obj2一个引用 var a2 = obj2.a;
// 引用对象1,此时对象1有a与a2两个引用 obj2 = null;
// 解除对象2的一个引用,此时对象2的引用数量为0,可以被垃圾回收
// 对象2的a属性引用被解除,此时对象1只有a2一个引用 a2 = null;
// 解除a2对于对象1的引用,此时对象1可以被垃圾回收

但是对于引用计数垃圾回收算法有个限制,当对象循环引用时,就会造成内存泄漏,也就是引用计数垃圾回收算法无法处理循环引用的对象。

function funct() {
var obj = {}; // 命名为对象1,此时引用数量为1
var obj2 = {}; // 命名为对象2,此时引用数量为1
obj.a = obj2; // obj的a属性引用obj2,此时对象2的引用数量为2
obj2.a = obj; // obj2的a属性引用obj,此时对象1的引用数量为2
return 1;
// 此时执行栈的obj变量与obj2变量被销毁,对象1与对象2的引用数量减1
// 对象1的引用数量为1,对象2的引用数量为1,两个对象都不会被引用计数算法垃圾回收
} funct();
// 两个对象被创建,并互相引用,形成了一个循环,它们被调用之后会离开函数作用域,所以它们已经不再需要了,可以被回收了,然而引用计数算法考虑到它们互相都有至少一次引用,所以它们不会被回收。

标记清除算法

对于引用计数垃圾回收算法,把对象是否不再需要简化定义为该对象是否可以获得,该算法设置一个叫做根root的对象,在Javascript里根是全局对象,垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象,以此不断向下查找。从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象,这样便解决了循环引用的问题。所有现代浏览器都使用了标记清除垃圾回收算法,所有对JavaScript垃圾回收算法的改进都是基于标记清除算法的改进。

  • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。
  • 然后,它会去掉运行环境中的变量以及被环境中变量所引用的变量的标记。
  • 此后,依然有标记的变量就被视为准备删除的变量,原因是在运行环境中已经无法访问到这些变量了。
  • 最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。

常见内存泄漏场景

意外的全局变量

JavaScript中并未严格定义对未声明变量的处理方式,即使在局部函数作用域中依旧能够定义全局变量,这种意外的全局变量可能会存储大量数据,且由于其是能够通过全局对象例如window能够访问到的,所以进行内存回收时不认为其是需要回收的内存而一直存在,只有在窗口关闭或者刷新页面时才能够被释放,造成意外的内存泄漏,在JavaScript的严格模式下此种意外的全局变量定义方式会抛出异常,另外同样可以使用eslint进行此种状态的预检查。事实上定义全局变量并不是一个好习惯,如果必须使用全局变量存储大量数据时,确保用完以后把它设置为null或者重新定义,与全局变量相关的增加内存消耗的一个主因是缓存,缓存数据是为了重用,缓存必须有一个大小上限才有用,高内存消耗导致缓存突破上限,因为缓存内容无法被回收。

function funct(){
name = "name";
}
funct();
console.log(window.name); // name
delete window.name; // 不手动删除则在不关闭或刷新窗口的情况下一直存在

被遗忘的计时器

计时器setInterval必须及时清理,否则可能由于其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。

<template>
<div></div>
</template> <script>
export default {
creates: function() {
this.refreshInterval = setInterval(() => this.refresh(), 2000);
},
beforeDestroy: function() {
clearInterval(this.refreshInterval);
},
methods: {
refresh: function() {
// do something
},
},
}
</script>

脱离DOM的引用

有时保存DOM节点内部数据结构很有用,例如需要快速更新表格的几行内容,把每一行DOM存成字典或者数组很有意义。此时同样的DOM元素存在两个引用:一个在DOM树中,另一个在字典中。将来如果决定删除这些行时,需要把两个引用都清除。此外还要考虑DOM树内部或子节点的引用问题,假如你的JavaScript代码中保存了表格某一个<td>的引用,将来决定删除整个表格的时候,直觉认为GC会回收除了已保存的<td>以外的其它节点,实际情况并非如此,此<td>是表格的子节点,子元素与父元素是引用关系,由于代码保留了<td>的引用,导致整个表格仍待在内存中,所以在保存DOM元素引用的时候,要小心谨慎。

var elements = {
button: document.getElementById("button"),
image: document.getElementById("image"),
text: document.getElementById("text")
};
function doStuff() {
elements.image.src = "https://www.example.com/1.jpg";
elements.button.click();
console.log(elements.text.innerHTML);
// 更多逻辑
}
function removeButton() {
// 按钮是 body 的后代元素
document.body.removeChild(elements.button);
elements.button = null; // 清除对于这个对象的引用
}

闭包

闭包是JavaScript开发的一个关键方面,闭包可以让你从内部函数访问外部函数作用域,简单来说可以认为是可以从一个函数作用域访问另一个函数作用域而非必要在函数作用域中实现作用域链结构。由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过度使用闭包可能会导致内存占用过多,在不再需要的闭包使用结束后需要手动将其清除。

function debounce(wait, funct, ...args){ // 防抖函数
var timer = null;
return () => {
clearTimeout(timer);
timer = setTimeout(() => funct(...args), wait);
}
} window.onscroll = debounce(300, (a) => console.log(a), 1);

被遗忘的监听者模式

当实现了监听者模式并在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。

<template>
<div ></div>
</template> <script>
export default {
created: function() {
global.eventBus.on("test", this.doSomething);
},
beforeDestroy: function(){
global.eventBus.off("test", this.doSomething);
},
methods: {
doSomething: function() {
// do something
},
},
}
</script>

被遗忘的事件监听器

当事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收,如果内部引用的变量存储了大量数据,可能会引起页面占用内存过高,这样就造成意外的内存泄漏。

<template>
<div></div>
</template> <script>
export default {
created: function() {
window.addEventListener("resize", this.doSomething);
},
beforeDestroy: function(){
window.removeEventListener("resize", this.doSomething);
},
methods: {
doSomething: function() {
// do something
},
},
}
</script>

被遗忘的Map

当使用Map存储对象时,类似于脱离DOM的引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收,对于键为对象的情况,可以采用WeakMapWeakMap对象同样用来保存键值对,对于键是弱引用的而且必须为一个对象,而值可以是任意的对象或者原始值,且由于是对于对象的弱引用,其不会干扰Js的垃圾回收。

var elements = new Map();
elements.set("button", document.getElementById("button"));
function doStuff() {
elements.get("button").click();
// 更多逻辑
}
function removeButton() {
// 按钮是 body 的后代元素
document.body.removeChild(elements.get("button"));
elements.delete("button"); // 清除对于这个对象的引用
}

被遗忘的Set

当使用Set存储对象时,类似于脱离DOM的引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收,如果需要使用Set引用对象,可以采用WeakSetWeakSet对象允许存储对象弱引用的唯一值,WeakSet对象中的值同样不会重复,且只能保存对象的弱引用,且由于是对于对象的弱引用,其不会干扰Js的垃圾回收。

var elements = new Set();
var btn = document.getElementById("button");
elements.add(btn);
function doStuff() {
btn.click();
// 更多逻辑
}
function removeButton() {
document.body.removeChild(btn); // 按钮是 body 的后代元素
elements.delete(btn); // 清除Set中对于这个对象的引用
btn = null; // 清除引用
}

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://zhuanlan.zhihu.com/p/60538328
https://juejin.im/post/6844903928060985358
https://jinlong.github.io/2016/05/01/4-Types-of-Memory-Leaks-in-JavaScript-and-How-to-Get-Rid-Of-Them/

Js中常见的内存泄漏场景的更多相关文章

  1. Android中常见的内存泄漏

    为什么会产生内存泄漏? 当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏. ...

  2. .NET中常见的内存泄漏和解决办法

    在.NET中,虽然CLR的GC垃圾回收器帮我们自动回收托管堆对象,释放内存,最大程度避免了"内存泄漏"(应用程序所占用的内存没有得到及时释放),但.NET应用程序"内存泄 ...

  3. android中常见的内存泄漏和解决的方法

    android中的内存溢出预计大多数人在写代码的时候都出现过,事实上突然认为工作一年和工作三年的差别是什么呢.事实上干的工作或许都一样,产品汪看到的结果也都一样,那差别就是速度和质量了. 写在前面的一 ...

  4. Android性能优化之常见的内存泄漏

    前言 对于内存泄漏,我想大家在开发中肯定都遇到过,只不过内存泄漏对我们来说并不是可见的,因为它是在堆中活动,而要想检测程序中是否有内存泄漏的产生,通常我们可以借助LeakCanary.MAT等工具来检 ...

  5. js的内存泄漏场景、监控以及分析

    内存泄漏 Q:什么是内存泄漏? 字面上的意思,申请的内存没有及时回收掉,被泄漏了 Q:为什么会发生内存泄漏? 虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存 ...

  6. 5个Android开发中比较常见的内存泄漏问题及解决办法

    android中一个对象已经不需要了,但是其他对象还持有他的引用,导致他不能回收,导致这个对象暂存在内存中,这样内存泄漏就出现了.   内存泄漏出现多了,会是应用占用过多的没存,当占用的内存超过了系统 ...

  7. JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏

    摘要: 作者将自己常用的JavaScript模块分享给大家. 原文:JavaScript如何工作:内存管理+如何处理4个常见的内存泄漏 作者:前端小智 Fundebug经授权转载,版权归原作者所有. ...

  8. JavaScript 工作原理之三-内存管理及如何处理 4 类常见的内存泄漏问题(译)

    原文请查阅这里,本文有进行删减,文后增了些经验总结. 本系列持续更新中,Github 地址请查阅这里. 这是 JavaScript 工作原理的第三章. 我们将会讨论日常使用中另一个被开发者越来越忽略的 ...

  9. Android开发 |常见的内存泄漏问题及解决办法

    在Android开发中,内存泄漏是比较常见的问题,有过一些Android编程经历的童鞋应该都遇到过,但为什么会出现内存泄漏呢?内存泄漏又有什么影响呢? 在Android程序开发中,当一个对象已经不需要 ...

随机推荐

  1. python双向链表的实现

    python双向链表和单链表类似,只不过是增加了一个指向前面一个元素的指针,下面的代码实例了python双向链表的方法 示意图: python双向链表实现代码: # -*- coding: utf-8 ...

  2. Java 从现在到次日零时还剩余多少秒

    参考:计算从现在到凌晨00:00还剩余多少秒 应用场景:某些应用需要在特定的时间点更新数据 1 import java.text.DateFormat; 2 import java.text.Simp ...

  3. 【题解】[CEOI2004]锯木厂选址

    Link \(\text{Solution:}\) 注意到题目中的编号是倒着的,于是我们的距离要预处理的是后缀和. 考虑如何\(n^2\)搞: 设\(dp[i]\)表示选择\(i\)为第二个中转点的最 ...

  4. C++调用全局函数与类成员函数

    void testfunc(void *param) { printf("\n\tcall global function %s\n", param); } void *GetCl ...

  5. Python 疑难问题:[] 与 list() 哪个快?为什么快?快多少呢?

    本文出自"Python为什么"系列,请查看全部文章 在日常使用 Python 时,我们经常需要创建一个列表,相信大家都很熟练了吧? # 方法一:使用成对的方括号语法 list_a ...

  6. vue打包之后在本地运行,express搭建服务器,nginx 本地服务器运行

    一.使用http-server 1.安装http-server npm install -g http-server 2.通过命令进入到dist文件夹 3.运行http-server 以上在浏览器输入 ...

  7. 打印一个N*N的方阵,N为每边字符的个数( 3〈N〈20 ),写出来真是泪牛满面啊。

    本程序可以完美输出,效果如下: 相信你已经很期待代码实现了吧,其实代码真的不难,关键是思想.我说说我的想法吧,首先我构造了一个数组,保存了上面图片中字符一行的一半.聪明的你肯定看出来了,上面多数字符都 ...

  8. centos8平台:用fontconfig安装及管理字体(fc-list/fc-match/fc-cache)

    一,fc-list所属的rpm包 [root@blog ~]$ whereis fc-list fc-list: /usr/bin/fc-list /usr/share/man/man1/fc-lis ...

  9. Ubuntu服务安装

    一.ifconfig命令安装 sudo apt install net-tools 二.ssh服务安装 sudo apt-get install openssh-server netstat -ltn ...

  10. 面试官:为什么MySQL的索引要使用B+树,而不是其它树?比如B树?

    InnoDB的一棵B+树可以存放多少行数据? 答案:约2千万 为什么是这么多? 因为这是可以算出来的,要搞清楚这个问题,先从InnoDB索引数据结构.数据组织方式说起. 计算机在存储数据的时候,有最小 ...