「通俗读物」- 内存泄露

前言

大家好,我是珂圩

内存泄漏大部分人肯定都遇到过,但是为什么会产生内存泄漏呢?

本章你将能了解到 ⬇️

什么是内存泄漏?

垃圾回收机制是什么?

常见的内存泄漏情况及如何解决?

什么是内存泄漏

内存泄漏(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存

程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

总体来说就是不用了就释放掉,怎么才算是被释放了呢

举个🌰

此时的test变量就在占用内存,如果我们一直不使用的话,它将一直占用内存

js 复制代码
    var test = 1

如何释放呢?将变量变为null,此时该内存就释放了

js 复制代码
    var test = null

下面我们更加具体的来了解一下这个内存释放的过程,也就是垃圾回收机制

垃圾回收机制

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

通常情况下有两种实现方式:

  • 引用计数
  • 标记清除

引用计数

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

js 复制代码
const arr = [1, 2, 3, 4];
console.log('hello world');

上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存

如果需要这块内存被垃圾回收机制释放,只需要设置如下:

js 复制代码
arr = null

通过设置arrnull,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了

标记清除

JavaScript最常用的垃圾收回机制

当变量进入执行环境时,就标记这个变量为"进入环境"。

进入环境的变量所占用的内存就不能释放,

当变量离开环境时,则将其标记为"离开环境"

垃圾回收程序运行的时候,会标记内存中存储的所有变量。

然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

举个🌰:

js 复制代码
var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

为什么会存在两种垃圾回收机制呢?

在JavaScript中,一般情况下,大多数现代的JavaScript引擎都采用标记清除作为主要的垃圾收集算法 ,因为它能够有效地处理循环引用,并且通常具有更好的性能。引用计数通常被认为是一种较为简单和原始的垃圾收集方式,由于其无法处理循环引用等情况,因此在实践中很少被单独使用。

循环引用这个你可能有点陌生,一个例子带你看明白!

js 复制代码
let obj1 = {};
let obj2 = {};
obj1.ref = obj2;
obj2.ref = obj1;

这种情况下,obj1obj2 彼此相互引用,形成了一个循环引用。如果使用引用计数算法,这种循环引用会导致两个对象的引用计数永远不会为零,因此它们永远不会被释放,从而导致内存泄漏

但是,在标记清除算法中,垃圾收集器会通过跟踪引用链来识别可访问的对象,并标记它们。然后,它会在不可达的对象上执行清除操作。对于循环引用,垃圾收集器能够识别到 obj1obj2 彼此引用,但由于它们无法被外部引用到,因此它们被认为是不可达的对象。在标记阶段后,垃圾收集器会将这两个对象都标记为待清除的对象,并在清除阶段释放它们占用的内存。

通过标记清除算法,循环引用不会导致内存泄漏,因为垃圾收集器能够识别和处理这种情况,从而正确地释放循环引用对象的内存。

注:"不可达"指的是在程序中无法通过任何引用链访问到的对象,换句话说,当一个对象没有被程序中的任何变量、属性或其他对象所引用时,它就被认为是不可达的。

在JavaScript中,主要的垃圾收集工作是由JavaScript引擎(如V8、SpiderMonkey、JavaScriptCore等)负责的,而开发者通常无需考虑垃圾收集算法的选择。 这些引擎在设计时已经根据性能和内存管理的需求选择了合适的垃圾收集策略。

三、常见内存泄露情况

一、 意外的全局变量

js 复制代码
function foo(arg) {
    bar = "this is a hidden global variable";
}

另一种意外的全局变量可能由 this 创建:

js 复制代码
function foo() {
    this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

使用严格模式,可以避免意外的全局变量

二、 定时器也常会造成内存泄露

js 复制代码
var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

三、 包括闭包,维持函数内局部变量,使其得不到释放

js 复制代码
function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放');
  };
  obj = null; // 解决方法
}

四、 没有清理对DOM元素的引用同样造成内存泄露

js 复制代码
const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用

五、 包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听

js 复制代码
// 创建一个长期存在的对象
let obj = {};

// 添加一个事件监听器
obj.onClick = function() {
  console.log('Button clicked!');
};

// 模拟DOM中的元素,它会持有一个对obj.onClick的引用
let button = document.createElement('button');
button.addEventListener('click', obj.onClick);

// 不正确地从DOM中移除元素
// button.parentNode.removeChild(button);

// 注意:如果正确地移除了事件监听器,可以取消注释上面的代码行,并观察内存泄漏是否消失

// 不再使用obj时,忘记移除事件监听器
// obj = null;

避免内存泄漏并优化内存使用

  1. 及时释放不再需要的引用

    • 当不再需要一个对象或变量时,及时将其设置为 null 或者从父对象中移除,这样可以帮助垃圾收集器回收这些不再需要的内存。
  2. 避免循环引用

    • 避免创建循环引用,尤其是在涉及到事件监听器、闭包、对象引用等场景中。如果确实需要使用循环引用,请谨慎处理,以免导致内存泄漏。
  3. 正确使用闭包

    • 当在闭包中引用外部变量时,确保不会无意中导致长期持有对外部变量的引用,从而防止内存泄漏。
  4. 移除事件监听器

    • 当不再需要事件监听器时,一定要记得手动移除它们,以防止长期保留对DOM元素的引用而导致内存泄漏。
  5. 使用对象池和缓存

    • 对于需要频繁创建和销毁的对象,可以考虑使用对象池来重复利用对象,以减少内存分配和释放的开销。

注:对象池是一种用于缓存和重复利用对象的技术。它通常用于需要频繁创建和销毁对象的场景,以减少内存分配和释放的开销,从而提高性能和效率。

  1. 避免频繁创建大型对象

    • 减少不必要的大型对象的创建,特别是在循环中或递归函数中。如果可能的话,尽量重用现有对象。
  2. 避免全局变量

    • 减少全局变量的使用,因为全局变量会一直存在于内存中,直到应用程序关闭。使用模块化的方式来组织代码,以限制变量的作用域。
  3. 定期检查内存使用情况

    • 使用浏览器的开发者工具或者内存分析工具来定期检查内存使用情况,及时发现潜在的内存泄漏问题,并进行优化。
  4. 避免大型数据集的浅拷贝

    • 当需要操作大型数据集时,避免使用浅拷贝,因为浅拷贝会复制引用,而不是创建新的对象。这可能导致意外保留对原始数据的引用,从而导致泄漏。
  5. 使用适当的数据结构

    • 根据应用场景选择合适的数据结构。例如,使用 MapSet 来管理唯一值,使用 WeakMapWeakSet 来处理对象的弱引用等。

结语

文章中提到的技术点,如果有不懂得或者想要了解更多的评论区告诉我,我会针对性的出一期更详细的文章。

如果此文章对你有帮助,点个赞支持一下

后续还会不定时出一些文章或针对某个领域的系列文章

有兴趣的可以关注一下!

相关推荐
有梦想的刺儿11 分钟前
webWorker基本用法
前端·javascript·vue.js
cy玩具32 分钟前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161771 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test2 小时前
js下载excel示例demo
前端·javascript·excel
Yaml42 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事2 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo2 小时前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v2 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫2 小时前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web