常见的内存泄漏场景

内存泄漏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. 【题解】CF1426D Non-zero Segments

    题目戳我 \(\text{Solution:}\) 若\([l,r]\)子段和是\(0,\)则\(sum[r]=sum[l-1].\) 于是我们可以考虑维护当前哪一个前缀和出现过.对于区间\([l,r ...

  2. C#数据结构-双向链表

    链表的概念以及链表与数组的差异不做过多的叙述,相信大家都耳熟能详,这里以c#语言实现简单的双向链表,作为备用,记录下~ public class Node<T> { private Nod ...

  3. 每日一题 LeetCode 491. 递增子序列 【递推】【递增子序列】【动态规划】

    题目链接 https://leetcode-cn.com/problems/increasing-subsequences/ 题目说明 题解 主要方法:递推:动态规划 解释说明: 数据表示:观察数据范 ...

  4. Oracle体系结构概述与SQL解析剖析

    Oracle服务器 是一个数据库管理系统,它提供了一种全面.开放.集成的方法来管理信息. Oracle服务器由Oracle数据库和Oracle实例组成. oracle数据库软件和Oracle数据库软件 ...

  5. GC调优-XX:PrintGCDetails深度解析

    查看程序运行GC的运行情况 资源充足的GC情况 新生代 老年代 元空间 因为现在资源充足没有发生GC *案例:将JVM初始化内存与最大内存(防止内存抖动,反复GC)调至10m,new一个50m的数组对 ...

  6. dict, hash

    dict: dictKey -- > dictVal example: dictEntry *dictFind(dict *d, const void *key)     Key is like ...

  7. 有感于“U盘型人才”

    先转载一篇互联网上转载比较多的一篇文章,文章是一名职业规划师写的:        上一阶段欠的债,下一阶段总要还,剩男剩女的家里比较着急也是这个道理,该结婚的时候不结婚,生涯任务没完成,必将影响下一段 ...

  8. 多测师讲解jmeter _安装和配置环境(00)_高级讲师肖sir

    1.下载jmeter包,我们已经下载了有现成的: 2.安装jjdk默认安装或自定义安装 默认安装的路径: 如下图 3.第三步:安装完成后配置JDK的环境变量  位置:计算机→属性→高级系统设置→高级→ ...

  9. 【7】进大厂必须掌握的面试题-Java面试-Jsp

    1. jsp的生命周期方法是什么? 方法 描述 公共无效的jspInit() 与servlet的init方法相同,仅被调用一次. 公共无效_jspService(ServletRequest requ ...

  10. 如何从0到1的构建一款Java数据生成器-第二章

    前提 在上一章我们提到了并且解决了几只拦路虎,承上启下,下面我们一起来实现一款数据生成器. 对外API /** * @description: 本地数据生成API * @author: peter * ...