首页 > 系统相关 >内存泄漏常见情况及处理方法

内存泄漏常见情况及处理方法

时间:2023-10-25 14:23:13浏览次数:50  
标签:泄漏 DOM 常见 回收 内存 监听器 引用

内存泄漏可以被视为你家中的水泄漏;虽然一开始小滴水可能看起来不是什么大问题,但随着时间的推移,它们可能会造成重大损失。同样,在JavaScript中,当不再需要的对象没有从内存中释放时,就会发生内存泄漏。随着时间的推移,这种累积的内存使用可以减慢甚至崩溃应用程序。

定义:当不再用到的对象内存,没有及时被回收或者无法被回收就会导致内存泄漏。

垃圾回收器

在编程领域,尤其是在处理 JavaScript 等解释型语言时,内存管理至关重要。幸运的是JavaScript 内置了一个名为 "垃圾回收器"(GC:Garbage Collection)的机制来帮助实现这一目标。想象一下,一个勤劳的清洁工会定期清扫你的房子,捡起任何不用的物品并丢弃,以保持整洁。

垃圾回收器会定期检查不再需要或不再可访问的对象,并释放它们占用的内存。在理想情况下,它可以无缝运行,确保未使用的内存无需任何人工干预即可回收。然而,就像我们的清洁工有时可能会忽略隐藏角落里的闲置物品一样,垃圾回收器也可能会遗漏因引用而无意中保持存活的对象,从而导致内存泄漏。

垃圾回收机制方法

标记清除法

过程:

  1. 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  2. 然后从各个根对象开始遍历,把能够访问到的变量置1
  3. 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  4. 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

优点:

实现比较简单,只存在打标记和不打标记两种情况,这种情况使得可以采用二进制01来进行标记

缺点:

内存碎片化: 这样内存里面空闲的位置不是连续的,这样就可以使用操作系统学到的方法来分配内存了,比如先什么最先适配、最优适配、最坏适配…

引用计数法

过程:

  1. 当声明一个变量并将一个引用类型赋值给该变量时该值引用次数加1
  2. 当这个变量指向其他一个时该值的引用次数便减1
  3. 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存

缺点:

  1. 循环引用会导致内存泄漏
  2. 计数器需要占很大位置。

导致应用程序内存泄漏的因素

1.全局变量

在 JavaScript 中,最高级别的作用域是全局作用域。在此作用域中声明的变量可从代码中的任何地方访问,这可能很方便,但也有风险。对这些变量的不当管理可能会导致意外的内存保留。

原因是什么?当一个变量在未使用 let 、const 或 var 声明的情况下被错误赋值时,它就会成为一个全局变量。此类变量驻留在全局作用域中,除非显式删除,否则会在应用程序的整个生命周期中持续存在。这个是由于历史遗留原因。可使用use strict开启JS严格模式来避免。

例如:假设你正在创建一个计算矩形面积的函数:

function calculateArea(width, height) {

  area = width * height; //无意中创建全局变量“area”

  return area;

}

calculateArea(10, 5);

这里 area 变量无意中被全局化,因为它没有与 let 、const 或 var 一起声明。这意味着函数执行后,area 仍然可以访问并占用内存:

console.log(area); // Outputs: 50

避免:最佳做法是始终使用 let 、const 或 var 声明变量,以确保它们具有正确的作用域,不会无意中成为全局变量。此外,如果有意使用全局变量,请确保它们对于全局访问是必不可少的,并有意识地管理它们的生命周期。

 

修改:正确对 area 变量进行作用域设置:

function calculateArea(width, height) {

  let area = width * height;

  return area;

}

calculateArea(10, 5);

 

项目中的bug实例:showBimface的filter

 

2.定时器和回调函数

在JavaScript中提供了内置函数,允许在特定的时间段后异步执行代码(使用 setTimeout)或以规律的间隔执行(使用 setInterval)。尽管它们非常强大,但如果没有正确管理,它们可能无意中导致内存泄漏。

原因:如果一个间隔或超时引用了一个对象,只要定时器还在运行,它就可以保持该对象在内存中,即使应用程序的其他部分不再需要该对象。

避免:关键是在不需要定时器时始终停止它们。如果完成了一个间隔或超时,使用clearInterval()或clearTimeout()分别清除它们。这会停止间隔并允许其回调中引用的任何对象有资格进行垃圾回收,前提是没有其他挥之不去的引用。

 

3.闭包

在JavaScript中,函数具有“记忆”它们创建时的环境的特殊能力。这种能力使内部函数可以访问外部(封闭)函数的变量,即使外部函数已经完成其执行。这种现象被称为“闭包”。

 

原因:闭包的能力伴随着责任。闭包保持对其外部环境变量的引用,这意味着如果闭包仍然活着(例如作为回调或在事件监听器中),它引用的变量将不会被垃圾回收,即使外部函数早已完成其执行。

例如:有一个创建倒计时的函数:

function createCountdown(start) {

  let count = start;

 

  return function() {

    return count--;

  };

}

 

let countdownFrom10 = createCountdown(10);

这里,countdownFrom10 是一个闭包。每次调用它时,它会将 count 变量减少一个。由于内部函数保持对 count 的引用,count 变量不会被垃圾回收,即使在程序的其他地方没有对createCountdown函数的其他引用。

 

现在想象一下,如果count是一个更大、更消耗内存的对象,闭包无意中将其保留在内存中。

 

避免:虽然闭包是一个强大的特性并且经常是必要的,但重要的是要注意它们引用的内容。确保:

①    只捕获需要的内容:除非必要,不要在闭包中捕获大对象或数据结构。

②    完成后断开引用:如果一个闭包被用作事件监听器或回调后不再需要它,就删除监听器或使回调为null,以断开闭包的引用。

修改:有意断开引用:

function createCountdown(start) {

  let count = start;

 

  return function() {

    return count--;

  };

}

 

let countdownFrom10 = createCountdown(10);

 

countdownFrom10 = null;

 

4. 事件监听器

在JavaScript中的事件监听器通过允许 “监听”特定的事件(如点击或按键)并在这些事件发生时采取行动,实现交互性。但与其他JavaScript功能一样,如果不仔细管理,它们可能会成为内存泄漏的来源。

原因:当你将事件监听器附加到DOM元素时,它在该函数(通常是闭包)和该元素之间创建了一个绑定。如果删除了元素或不再需要该事件监听器,但没有明确删除监听器,关联的函数仍留在内存中,可能保留其引用的其他变量和元素。

 

例如:假设将一个点击监听器附加到一个按钮:

const button = document.getElementById('myButton');

 

button.addEventListener('click', function() {

  console.log('Button was clicked!');

});

现在在应用程序中从DOM中删除按钮:

button.remove();

即使按钮从DOM中删除,事件监听器的函数仍然保留对按钮的引用。这意味着按钮不会被垃圾回收,导致内存泄漏。

 

避免:关键是积极管理你的事件监听器:

 

①     明确删除:在删除元素或不再需要它们时,使用removeEventListener()始终删除事件监听器。

②    使用一次:如果你知道一个事件只需要一次,你可以在添加监听器时使用{ once: true }选项。 修改上面的示例以进行正确管理:

const button = document.getElementById('myButton');

 

function handleClick() {

  console.log('Button was clicked!');

}

 

button.addEventListener('click', handleClick);

 

// 稍后在代码中,当我们完成按钮时:

button.removeEventListener('click', handleClick);

button.remove();

 

通过在删除按钮之前明确地删除事件监听器,我们确保监听器的函数和按钮本身都可以被垃圾回收。

 

5. 分离的DOM元素

文档对象模型(DOM)是网页上所有元素的分层表示。当你修改DOM,例如通过删除元素,但仍然在JavaScript中持有对该元素的引用,你就已经创建了所谓的** “分离的DOM元素” **。这些元素不再可见,但由于它们仍然被代码引用,所以它们不能被垃圾回收。

 

原因:当从DOM中删除元素但仍有指向它们的JavaScript引用时,会创建分离的DOM元素。这些引用阻止垃圾回收器回收这些元素占用的内存。

 

例如:假设有一个物品列表,并且决定删除一个:

let listItem = document.getElementById('itemToRemove');

listItem.remove();

现在,即使您已经从DOM中删除了 listItem,你仍然在 listItem 变量中对其有引用。这意味着实际的元素仍然在内存中,从DOM中分离但占用空间。

 

避免:为了防止分离的DOM元素引起的内存泄漏,使引用为 null,即删除DOM元素后,使对其的任何引用为 null:

listItem.remove();

listItem = null;

限制元素引用:只在绝对需要时存储对DOM元素的引用。

 

修改示例以防止内存泄漏:

 

let listItem = document.getElementById('itemToRemove');

listItem.remove();

listItem = null;  // 断开对分离的DOM元素的引用

 

通过在从DOM中删除 listItem 后使 listItem 引用为null,确保垃圾回收器可以回收已删除元素占用的内存。

 

6. Websockets和外部连接

Websockets 提供了一个全双工通信通道,通过单个、长时间的连接。这使它非常适合实时应用,如聊天应用、在线游戏和实时体育更新。然而,由于 Websockets 的性质是保持开放的,如果不正确处理,它们可能成为内存泄漏的潜在来源。

 

原因:当 Websockets和其他持久的外部连接管理不当时,它们即使不再需要也可以持有对象或回调的引用。这可以阻止这些引用的对象被垃圾回收,导致内存泄漏。

 

例如:假设一个应用程序,该应用程序打开一个 websocket 连接以接收实时更新:

let socket = new WebSocket('ws://example.com/updates');

 

socket.onmessage = function(event) {

  console.log(`Received update: ${event.data}`);

};

 

现在,如果在某个时候,页面导航离开了应用的这一部分或关闭了使用此连接的特定UI组件,但忘记关闭 websocket,它仍然保持打开状态。与其事件监听器关联的任何对象或闭包都不能被垃圾回收。

 

避免:积极管理websocket连接至关重要。

① 明确关闭:当不再需要时,始终使用 close() 方法关闭 websocket 连接:

socket.close();

② 引用为 null:关闭 websocket 连接后,使任何关联的引用为 null 以帮助垃圾回收器:

socket.onmessage = null;

socket = null;

 

③ 错误处理:实施错误处理以检测连接何时丢失或意外终止,然后清理任何相关的资源。

继续上面的示例,正确的管理看起来是这样的:

 

let socket = new WebSocket('ws://example.com/updates');

 

socket.onmessage = function(event) {

  console.log(`Received update: ${event.data}`);

};

 

// 稍后在代码中,当连接不再需要时:

socket.close();

socket.onmessage = null;

socket = null;

7. 常用工具

预防内存泄漏的最佳方法是尽早检测它们。浏览器开发者工具,尤其是Chrome DevTools中的 “Memory”标签尤其有用,允许监视内存使用情况,拍摄快照并随着时间的推移跟踪更改。

8. 总结

①      定期审核:定期审查代码以确保遵循最佳实践。

②      测试:添加新功能后,测试潜在的内存泄漏。

③      代码卫生:保持代码整洁、模块化并且记录完善。

④      第三方库:明智地使用它们。有时它们可能是内存泄漏的原因。

标签:泄漏,DOM,常见,回收,内存,监听器,引用
From: https://www.cnblogs.com/LeoXnote/p/17787118.html

相关文章

  • linux中执行uefi runtime service call的内存上下文切换
    当linuxkernel从UEFI启动之后尽管bootservice退出了但是仍然可以使用runtimeservice。这就引发了一个问题:存在于uefi内存空间的code如何被kernel调用。首先找一个调用efiruntimeservice的例子:staticvoidefi_call_rts(structwork_struct*work){...switch(e......
  • linux 内存盘的使用方式与验证
    linux内存盘的使用方式与验证背景某些情况下,硬盘的写入是一个很大的瓶颈使用内存文件系统的方式应该能够极大的提高IO的速度.内存盘的优点是比较快,缺点就是数据不是持久化的.其实还是有很多可以持续优化的方式与方法的.可以最大化的磁盘的IO速度等.内存盘的多种模......
  • 五大内存分区
    c/c++:五大内存分区(笔记)_内存有那5部分_深海中的咸鱼的博客-CSDN博客java:Java5大内存区域-CSDN博客......
  • Kubernetes常见面试题
    说明:以下问题只做简单总结,详细内容请参考链接:https://github.com/bregman-arie/devops-exercises/blob/master/topics/kubernetes/README.md#kubernetes-1011、k8s是什么,为什么企业选择使用它     k8s是一个开源应用,给用户提供了管理、部署、扩展容器的能力。将容器运......
  • szfpga Lattice高速下载器HW-USBN-2B 常见问题解答
      .产品特点     1).支持windows7,Windows10操作系统,两个操作系统非常稳定不断线。  2).支持JTAG模式,速度快,最高30Mb/s,调试serdescore,不会像hw-usbn-2a出现错误。如这种错误Error:failedtosetcablepor(cable:USBport:EzUSB-0error:-1)  3). ......
  • 《面试1v1》JVM内存模型
    我是javapub,一名Markdown程序员从......
  • 常见的几个音乐库
    #Installmusic-relatedlibssudoapt-getinstall-ylibsndfile1-devsudoapt-getinstall-yfluidsynthsudoapt-getinstall-yffmpegsudoapt-getinstall-ylilypondlibsndfile1是一个库,专门用于读取和写入多种音频文件格式,如WAV、AIFF、FLAC等。这个库提供......
  • malloc划分内存空间大小
    今天写c语言,犯了一个很失败的错误,类似于typedefint*intp;intpptr=(intp)malloc(sizeof(intp));如果是int,那么本身占用内存就很小,也许能正确运行代码,但是如果内存空间大一点的,肯定直接报错了,因为划分的还没要用的多,。。。。编译器也不会报错。。。......
  • 可重入锁ReentrantLock在性能测试常见用法
    在进行Java多线程编程的过程中,始终绕不开一个问题:线程安全。一般来说,我们可以通过对一些资源加锁来实现,大多都是通过synchronized关键字实现。在做性能测试时,如果TPS或者QPS要求没有特别高,synchronized一招鲜基本也能满足大部分的需求了。对于一招鲜无法很好解决的问题,就需要......
  • python内存监测工具memory_profiler
    内存监测工具memory_profiler目录内存监测工具memory_profiler安装参数注解简单使用输出在日志中mprof使用参考资料memory_profiler是Python的一个第三方库,其功能时基于函数的逐行代码分析工具memory_profiler是一个监控进程内存消耗的模块,也可以逐行分析Python程序的内存......