JS的内存泄漏详解

介绍:
什么是垃圾回收机制
GC 即 Garbage Collection ,程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而 GC 就是负责回收垃圾的,因为他工作在引擎内部,所以对于我们前端来说,GC 过程是相对比较无感的,这一套引擎执行而对我们又相对无感的操作也就是常说的垃圾回收机制 。

垃圾产生&为何回收
JavaScript 的引用数据类型是保存在堆内存中的,然后在栈内存中保存一个对堆内存中实际对象的引用,所以,JavaScript 中对引用数据类型的操作都是操作对象的引用而不是实际的对象。可以简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的。

程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。

如果你在用 Vue 开发应用,那么就要当心内存泄漏的问题。这个问题在单页应用 (SPA) 中尤为重要,因为在 SPA 的设计中,用户使用它时是不需要刷新浏览器的,所以 JavaScript 应用需要自行清理组件来确保垃圾回收以预期的方式生效。

内存泄漏在 Vue 应用中通常不是来自 Vue 自身的,更多地发生于把其它库集成到应用中的时候

Vue官网讲解避免内存泄露

泄漏点:

1.DOM/BOM 对象泄漏
2.script 中存在对DOM/BOM 对象的引用导致
3.Javascript 对象泄漏
4.通常由闭包导致,比如事件处理回调,导致DOM对象和脚本中对象双向引用,这个时常见的泄漏原因

代码关注点:

1.DOM中的addEventLisner 函数及派生的事件监听, 比如Jquery 中的on 函数, vue 组件实例的 $on 函数,第三方库中的初始化函数
2.其它BOM对象的事件监听, 比如websocket 实例的on 函数
3.避免不必要的函数引用
4.如果使用render 函数,避免在html标签中绑定DOM/BOM 事件

Vue如何处理:

1.如果在mounted/created 钩子中绑定了DOM/BOM 对象中的事件,需要在beforeDestroy 中做对应解绑处理
2.如果在mounted/created 钩子中使用了第三方库初始化,需要在beforeDestroy 中做对应销毁处理
3.如果组件中使用了定时器,需要在beforeDestroy 中做对应销毁处理
4.模板中不要使用表达式来绑定到特定的处理函数,这个逻辑应该放在处理函数中?
5.如果在mounted/created 钩子中使用了 o n ,需要在 b e f o r e D e s t r o y 中做对应解绑 ( on,需要在beforeDestroy 中做对应解绑( on,需要在beforeDestroy中做对应解绑(off)处理
6.某些组件在模板中使用 事件绑定可能会出现泄漏,使用$on 替换模板中的绑定

在 JavaScript 中,内存泄漏通常是由于变量、对象、闭包、事件监听器等长期存在而没有被释放引起的。这些长期存在的引用会阻止垃圾回收器回收内存,最终导致内存泄漏。

JS内存泄漏通常发生在以下情况下:

1.循环引用
当两个或多个对象之间存在相互引用,并且没有被其他对象引用,就会发生循环引用,从而导致内存泄漏。这种情况可以通过在对象之间断开引用来避免。

function createObject() {
  var obj1 = {};
  var obj2 = {};
  obj1.ref = obj2;
  obj2.ref = obj1;
  return obj1;
}
var myObj = createObject();
// 这里无法回收 myObj 和 myObj.ref 所占用的内存空间,导致内存泄漏

2.隐式全局变量
函数中的局部变量在函数执行结束后这些变量已经不再被需要,所以垃圾回收器会识别并释放它们。但是对于全局变量,垃圾回收器很难判断这些变量什么时候才不被需要,所以全局变量通常不会被回收,我们使用全局变量是 OK 的,但同时我们要避免一些额外的全局变量产生,如下:

function fn(){ 
 // test1 
 test1 = new Array(1000).fill('xianzao') 
 // thiswindowtest2 
 this.test2 = new Array(1000).fill('xianzao') 
} 
fn() 

调用函数 fn ,因为 没有声明 和 函数中this 的问题造成了两个额外的隐式全局变量,这两个变量不会被回收,这种情况我们要尽可能的避免,在开发中我们可以使用严格模式或者通过 lint 检查来避免这些情况的发生,从而降低内存成本。

除此之外,我们在程序中也会不可避免的使用全局变量,这些全局变量除非被取消或者重新分配之外也是无法回收的,这也就需要我们额外的关注,也就是说当我们在使用全局变量存储数据时,要确保使用后将其置空或者重新分配,当然也很简单,在使用完将其置为 null 即可,特别是在使用全局变量做持续存储大量数据的缓存时,我们一定要记得设置存储上限并及时清理,不然的话数据量越来越大,内存压力也会随之增高。

var test = new Array(10000) 
// do something 
test = null

3.闭包

function fn1(){
  var n=1;
}
//我想取到里面的局部变量n
function fn1(){
  var n=1;
  function fn2(){//在加一个fn2当他的子集
    alert(n);
  }
  
}

但是我在外面还是访问不到那就return出来

function fn1(){
  var n=1;
  function fn2(){//在加一个fn2当他的子集
    alert(n);
  }
return fn2(); 
//return出来后 他就给 window了所以一直存在内存中。因为一直在内存中,在IE里容易造成内存泄漏
}
fn1();

尽量书写的时候,避免这种情况。 那么怎样解决呢?
其实在函数调用后,把外部的引用关系置空就好了

function fn2(){
 let test = new Array(1000).fill('xianzao')
 return function(){
 console.log(test)
 return test
 }
}
let fn2Child = fn2()
fn2Child()
fn2Child = null

4.定时器setTimeout setInterval
当不需要setInterval或者setTimeout时,定时器没有被clear,定时器的回调函数以及内部依赖的变量都不能被回收,造成内存泄漏。比如:vue使用了定时器,需要在beforeDestroy 中做对应销毁处理。js也是一样的。
另外,浏览器中的 requestAnimationFrame 也存在这个问题,我们需要在不需要的时候用cancelAnimationFrame API 来取消使用。

clearTimeout(***)
clearInterval(***)

5.Map、Set对象
当使用 Map 或 Set 存储对象时,同 Object 一致都是强引用,如果不将其主动清除引用,其同样会造成内存不自动进行回收。

如果使用 Map ,对于键为对象的情况,可以采用 WeakMap,WeakMap 对象同样用来保存键值对,对于键是弱引用 (注:WeakMap 只对于键是弱引用),且必须为一个对象,而值可以是任意的对象或者原始值,由于是对于对象的弱引用,不会干扰 Js 的垃圾回收。

如果需要使用 Set 引用对象,可以采用 WeakSet,WeakSet 对象允许存储对象弱引用的唯一值,WeakSet 对象中的值同样不会重复,且只能保存对象的弱引用,同样由于是对于对象的弱引用,不会干扰 Js 的垃圾回收。

这里可能需要简单介绍下,谈弱引用,我们先来说强引用,之前我们说 JS 的垃圾回收机制是如果我们持有对一个对象的引用,那么这个对象就不会被垃圾回收,这里的引用,指的就是 强引用 ,而弱引用就是一个对象若只被弱引用所引用,

则被认为是不可访问(或弱可访问)的,因此可能在任何时刻被回收。

// obj 
let obj = {id: 1} 
// obj 
obj = null 
// {id: 1} 

6.如果在mounted/created 钩子中使用了 o n ,需要在 b e f o r e D e s t r o y 中做对应解绑 ( on,需要在beforeDestroy 中做对应解绑( on,需要在beforeDestroy中做对应解绑(off)处理

beforeDestroy() {
 this.bus.$off('****');
}

这是一个简单的通过重写引用来清除对象引用,使其可回收。

let obj = {id: 1} 
let user = {info: obj} 
let set = new Set([obj]) 
let map = new Map([[obj, 'xianzao']]) 
// obj 
obj = null 
console.log(user.info) // {id: 1} 
console.log(set) 
console.log(map)

这里重写了 obj 以后, {id: 1} 依然会存在于内存中,因为 user 对象以及后面的 set/map 都强引用了它,Set/Map、对象、数组对象等都是强引用,所以我们仍然可以获取到 {id: 1} ,我们想要清除那就只能重写所有引用将其置空了。

let obj = {id: 1}
let weakSet = new WeakSet([obj])
let weakMap = new WeakMap([[obj, 'xianzao']])
// obj
obj = null
// {id: 1} 垃圾回收

使用了 WeakMap 以及 WeakSet 即为弱引用,将 obj 引用置为 null 后,对象 {id: 1} 将在下一次 垃圾回收机制 中被清理出内存。

7.给DOM对象添加的属性是一个对象的引用
(‘idname’).property = testObject; // 如果DOM不被消除,则testObject会一直存在,造成内存泄漏
解决方法:
在window.onunload事件中写上:

window.onunload=function(){
  document.getElementById('idname').property = null;     //释放内存
};

8.游离DOM引用
考虑到性能或代码简洁方面,我们代码中进行 DOM 时会使用变量缓存 DOM 节点的引用,但移除节点的时候,我们应该同步释放缓存的引用,否则游离的子树无法释放:

<div id="root">
 <ul id="ul">
 <li></li>
 <li></li>
 <li id="li3"></li>
 <li></li>
 </ul>
</div>
<script>
 let root = document.querySelector('#root')
 let ul = document.querySelector('#ul')
 let li3 = document.querySelector('#li3')
 // ululGC
 root.removeChild(ul)
 // ulli3ululGC
 ul = null
 // GC
 li3 = null
</script>

如上所示,当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。

假如我们将父节点置空,但是被删除的父节点其子节点引用也缓存在变量里,那么就会导致整个父 DOM 节点树下整个游离节点树均无法清理,还是会出现内存泄漏,解决办法就是将引用子节点的变量也置空

9.从外到内执行appendChild。这时即使调用removeChild也无法释放

var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
document.body.appendChild(parentDiv); 
parentDiv.appendChild(childDiv); 

解决方法:
从内到外执行appendChild:

var parentDiv = document.createElement("div"); 
var childDiv = document.createElement("div"); 
parentDiv.appendChild(childDiv); 
document.body.appendChild(parentDiv); 

10.反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)

for(i = 0; i < 5000; i++) { 
  hostElement.text = "asdfasdfasdf"; 
} 

11.使用了第三方库或框架

在使用第三方库或框架时,需要确保它们没有内存泄漏问题。如果使用了存在内存泄漏问题的库或框架,就会导致整个应用程序出现内存泄漏问题。
而有时错误的使用第三方库也会导致内存泄漏,比如在定时器中循环渲染echarts:
有时我们不注意在循环中去执行下面的操作
let chart = echarts.init(document.getElementById(dom));
通过init方法创建echarts实例,如果不及时清理就会越来越多,占用大量内存,
这时我们可以这样
if (this.chart == undefined) {
this.chart = echarts.init(document.getElementById(dom));
}
或者在 init 之前销毁已经存在的 echarts 实例,可用 clear 和 dispose 方法,区别是clear()不会销毁实例,只是重新绘制图形,而dispose()会销毁实例,需要重新构建ECharts对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值