浅谈js内存泄露和垃圾回收机制

什么是GC?

GC是Garbage Collection的缩写,表示垃圾回收。在程序工作过程中,会产生很多’垃圾’,这些垃圾是程序不用的内存空间。GC就是负责收走垃圾的,由于GC工作在JavaScript引擎内部,因此对前端开发者来说,GC在“一定程度上”是悄无声息工作的,是不可见的。

GC主要做什么?

  • 找到内存空间中的垃圾
  • 回收垃圾,让程序员能再次利用这部分空间

所有语言都有GC吗?

不是所有语言都有GC,相对来说,高级语言里面一般有GC,比如Java、JavaScript、Python,没有GC的话,需要程序员手动管理内存,比如C语言常见的malloc/free,其实就是memory allocation的缩写,还有C++里面的new/delete。

js垃圾回收机制

垃圾回收机制能做什么?

解决内存的泄露,垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。

常见的垃圾回收方法有哪些?

引用计数

是一种不太常⻅的垃圾回收机制,是最简单的垃圾收集算法。含义就是跟踪每个值被引用的次数,如果同一个值又被赋值给另一个变量那么该值 的引用次数就加一,相同的如果包含这个值引用的变量又取得另一个值,那么引用次数就减一。当这个值的引用次数变为0时候,说明没有办法访问这 个值了,因此就可以将其占用的内存空间收回。来看个例子

var a = new Object(); // 该对象的引用计数为1(a在引用)
var b = a; // 该对象的引用计数是2(a,b)
a = null; // count = 1
b = null; // count = 0 
// GC来回收这个对象了
优点
  1. 可即刻回收垃圾,当被引用数值为0时,对象马上会把自己作为空闲空间连到空闲链表上,也就是说。在变成垃圾的时候就立刻被回收。
  2. 因为是即时回收,那么‘程序’不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
  3. 不用去遍历堆里面的所有活动对象和非活动对象
缺点
  1. 计数器需要占很大的位置,因为不能预估被引用的上限,打个比方,可能出现32位即2的32次方个对象同时引用一个对象,那么计数器就需要32位;
  2. 最大的劣势是无法解决循环引用无法回收的问题;

来看个例子:

function problem(){
    let o1 = new Object(); 
    let o2= new Object();
    o1.someOtherObject = o2; //a引 用了 b 
    o2.someOtherObject = o1;// b引用了a,形成了一个环
}
problem()

该例子中o1和o2通过各自的属性相互引用,即这俩对象引用的次数都是2,在标记清除策略的实现中,由于函数执行完毕后,这俩对象都离开了作用域,因此这种互相引用不是问题,但在引用计数策略实现中,当函数执行完毕后他们将继续存在,引用次数一直都为2,如果这个函数被多次调用的话那么会产生大量的内存得不到回收。所以就有问题。该算法的循环引用的这个问题逐渐被标记清除法替代。

标记清除

标记清除法可以很好的解决上面的循环引用问题。

js中最常用的垃圾回收方法就是标记清除,当变量进入环境时,例如,在一个函数中声明一个变量,就将这个变量标记为"进入环境",从逻辑上讲,永远不能释放进入环境变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们,而当变量离开环境时,则将其标记为"离开环境"。

活动对象

能通过引用程序引用的对象就被称为活动对象(可以直接或间接从全局变量空间中引出的对象)

非活动对象

不能通过程序引用的对象称为非活动对象(这就是被清除的目标)

标记清除主要将GC分为两个阶段:

  1. 标记阶段:把所有活动对象做上标记;
  2. 清除阶段:把没有标记(即非活动对象)销毁;

在标记-清除算法中比较重要的两个概念一个是根,一个是可达。

什么样的称之为根?
  • 有一组基本的固有可达值,由于一些原因无法删除。例如:
    本地函数的局部变量和参数;
  • 当前嵌套调用链上的其他函数的变量和参数
  • 全局变量;
  • 还有一些其他的,内部的;
什么是可达的?

如果引用或引用链可以从根访问任何其他值,则认为该值是可达的。

标记阶段

在这里插入图片描述
可以理解成全局作用域,GC从全局作用域的变量,沿作用域逐层往里遍历(深度遍历),当遍历到堆中对象时,说明该对象被引用着,则打上一个标记,继续递归遍历(因为肯定存在堆中对象引用另一个堆中对象),直到遍历到最后一个(最深的一层作用域)节点。

在这里插入图片描述
标记完成之后,就是这样的:
在这里插入图片描述

清除阶段

遍历,是遍历整个堆,回收没有打上标记的对象。

来看个例子:

function test() {
  var a = 10 //标记"进入环境"
  var b = 'hello' //标记"进入环境"
}
test() // 执行完毕,a和b被标记"离开环境",被回收
优点
  • 实现简单,打标记分为打或不打两种可能,所以就一位二进制位就可以表示;
  • 解决了循环引用问题
缺点
  • 造成碎片化(有点类似磁盘的碎片化)
  • 再分配时遍次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端;
总结:
  1. 离开作用域的值将被自动标记为可以回收的,因此在垃圾收集期间被删除;
  2. 标记清除是当前主流的垃圾回收算法,是给当前不适用或者适用的值加上标记,然后再使用这种算法,
  3. 引用计数目前js引擎都不再使用这种算法,但在ie中访问非原生js对象(列入dom)这种算法仍然会导致问题。
  4. 解除变量的引用不仅有助于消除循环引用现象,而且对垃圾收集也有好处,为了确保有效滴回收内存,应该及时解除不在使用的全局对象、全 局对象属性以及循环引用变量的引用。

引起内存泄露的原因

内存泄漏怎么造成的? 程序未能释放已经不再使用的内存

  1. 意外的全局变量

原因:全局变量,不会被回收。

解决:使用严格模式避免。

function fun() { 
   bar = "aaa";
}
// 还有this创建的全局变量
function fun(){
   this.bar = "aaa"
}
// 等同于
function fun() { window.bar === "aaa" }

// 解决方法,在JavaScript文件中添加'use strict',开启严格模式,可以有效地避免上述问题
function foo(arg) {
   "use strict" // 在foo函数作用域内开启严格模式
    bar = "aaa"; // 报错:因为bar还没有被声明
}
  1. console.log

console.log: 向web开发控制台打印一条消息,常在开发时调试分析。有时在开发时,需要打印一些对象信息,但发布时却忘记去掉console.log语句,这可能造成内存泄露。

在传递给console.log的对象是不能被垃圾回收 ,因为在代码运行之后需要在开发工具能查看对象信息。所以最好不要在生产环境中console.log任何对象。

  1. 闭包

原因:闭包可以维持函数内局部变量,使其得不到释放。

解决:将事件处理函数定义在外部,解除闭包;不要把外层引用,赋予内部变量;不要随意引用外部函数;内部函数一定要注意外层环;尽量不用使用全局变量保存状态;

  1. 被遗忘的定时器或者回调

原因:定时器中有dom的引用,即使dom删除了,但是定时器还在,所以内存中还是有这个dom。

解决:手动删除定时器和dom

  1. dom

在js中,DOM操作是非常耗时的,由于js引擎独立于渲染引擎,而DOM是位于渲染引擎的,相互访问需要耗费一定内存资源,如Chrome浏览器中JavaScript/ECMAScript位于V8中而DOM位于WebCore,假如将JavaScript/ECMAScript、DOM分别想象成两座孤岛,两岛之间通过一座收费桥连接,过桥需要交纳一定“过桥费”。JavaScript/ECMAScript每次访问DOM时,都需要交纳“过桥费”。因此访问DOM次数越多,费用越高,⻚面性能就会受到很大影响。

为了减少DOM访问次数,一般情况下,当需要多次访问同一个DOM方法或属性时,会将DOM引用缓存到一个局部变量中。但如果在执行某些删除、更新操作后,可能会忘记释放掉代码中对应的DOM引用,这样会造成DOM内存泄露。

来看个例子:

var elements ={
    button:document.getElementById("button"),
    image:document.getElementById('image')
}
function doStuff(){
    elements.image.src = 'http://example.com/a.png'
}
function removeImage(){
    document.body.removeChild(document.getElementById('image'))
}
// 此时在全局元素对象中仍在引用button,垃圾收集无法将其回收造成内存泄露
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值