js的垃圾回收机制+常见的内存泄漏+闭包详解+执行上下文

js的垃圾回收机制

垃圾回收机制也称Garbage Collection简称GC。在JavaScript中拥有自动的垃圾回收机制,通过一些回收算法,找出不再使用引用的变量或属性,由JS引擎按照固定时间间隔周期性的释放其所占的内存空间。在C/C++中需要程序员手动完成垃圾回收。
垃圾回收过程是不实时进行的,因为JavaScript是一门单线程的语言,每次执行垃圾回收,会使程序应用逻辑暂停,执行完垃圾后回收再执行应用逻辑,这种行为称为全停顿,所以一般垃圾回收会在cpu闲时进行。

如何通过某种方式找到所谓的垃圾,是垃圾回收的重点,下面是常见的GC算法:

1.引用计数算法

实现原理:
内部通过引用计数器,来维护当前对象的引用数,从而判断该对象的引用数是否为0,来决定它是否是一个垃圾对象。如果引用数值为0,GC就开始工作,将其所在的内存空间进行回收释放和再使用

优点:可以即时回收垃圾对象、减少程序卡顿时间
缺点:无法回收循环引用的对象、资源消耗较大

2.标记清除算法

实现原理:
核心思想就是将整个垃圾回收操作分为两个阶段:
遍历所有的对象找到活动对象,进行标记的操作
遍历所有的对象,找到那些没有标记的对象进行清除。(注意在第二阶段中也会把第一阶段涉及的标志给抹掉,便于GC下次能够正常的工作)

通过两次的遍历行为把我们当前的垃圾空间进行回收,最终交给我们的空闲列表进行维护。

优点:可以回收循环引用的对象空间。相对于引用计数算法来说:解决对象循环引用的不能回收问题。
缺点:容易产生碎片化空间,浪费空间、内存碎片化、分配速度慢、不能立即回收垃圾对象。
空间碎片化:所谓空间碎片化就是由于当前所回收的垃圾对象,在地址上面是不连续的,由于这种不连续造成了我们在回收之后分散在各个角落,造成后续使用的问题

3.标记整理算法

实现原理:
标记整理可以看做是标记清除的增强;
标记阶段的操作和标记清除一致;
清除阶段之前/标记结束后 会先执行整理,将不需要清理的对象向内存的一端移动,使得他们在地址上是一个连续的空间,最后清理掉边界的内存。
优点:减少碎片化空间
缺点:分配速度慢、不会立即回收垃圾对象

先明确一点现代浏览器采用的是标记清除/整理,而老浏览器采用的是引用计数,因为引用计数这种机制有个很严重的bug即不能回收循环引用的对象

大多数浏览器都是基于标记清除/整理算法,不同的只是在运行垃圾回收的频率具有差异。V8 对其进行了一些优化加工处理。

V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收。(V8的回收机制也是基于标记清除/整理算法)

V8的垃圾回收策略详解

常见的内存泄漏

内存泄漏指程序不再使用或不需要的一块内存,但是由于某种原因没有及时被释放时。在代码中创建对象和变量会占用内存,但是javaScript是有自己的垃圾回收机制也称Garbage Collection简称GC,可以确定那些变量不再需要,并将其清除。但是当你的代码存在逻辑缺陷的时候,你以为你已经不需要,但是程序中还存在着引用,导致程序运行完后并没有合适的回收所占用的空间,导致内存不断的占用,运行的时间越长占用的就越多,随之出现的是,性能不佳,高延迟,频繁崩溃。

1.不正当的闭包

闭包(个人理解):是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。
JavaScript高级程序设计:闭包是指有权访问另一个函数作用域中的变量的函数,在本质上,闭包是将函数内部和函数外部连接起来的桥梁

function fn1(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log('hahaha')
  }
}
let fn1Child = fn1()
fn1Child()

上面显然它是一个典型闭包,但是它并没有造成内存泄漏,因为返回的函数中并没有对 fn1 函数内部的引用,也就是说,函数 fn1 内部的 test 变量完全是可以被回收的,那我们再来看:

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

上面显然它也是闭包,并且因为 return 的函数中存在函数 fn2 中的 test 变量引用,所以 test 并不会被回收,也就造成了内存泄漏

根据垃圾回收机制,被另一个作用域引用的变量不会被回收。除非解除调用才能销毁。那么怎样解决呢?
其实在函数调用后,把外部的引用关系置空就好了,如下:

function fn2(){
  let test = new Array(1000).fill('isboyjc')
  return function(){
    console.log(test)
    return test
  }
}
let fn2Child = fn2()
fn2Child()
fn2Child = null // 阻止内存泄漏  //浏览器每隔一段时间垃圾回收机制就会去找,你把它置为一个空指针就代表这个已经没用了,GC就会回收,fn2Child没用了,则引用关系没了->test就没用了->回收

函数销毁阶段:当函数执行完成后,当前执行上下文会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文

当闭包包含的父级函数执行完毕后,其对应的作用域链会销毁(弹出执行上下文栈并且销毁),但是由于闭包的存在,父级函数中变量对象会一直存在内存中。只有当闭包销毁,才会从内存中释放掉。所以,过度使用闭包,会存在内存泄漏的风险。(函数和函数中的变量存储的位置本身就在不同的位置,函数是栈指向堆中的值,变量是存在栈中)

// 关于内存释放的面试题
 一般情况下,函数执行形成一个私有的作用域/执行上下文,当执行完成后就销毁了->节省内存空间
function fn() {
        var i = 10;
        return function (n) {
            console.log(n + (++i));
        }
}
    var f = fn();
    f(15);    //26  //执行完函数作用域链会销毁,但变量i一直存在内存中,变量所占的内存不会释放。除非f=null
    
    fn()(15); //26  //这是自执行函数,每次执行完作用域/执行上下文销毁,内存释放。但不会立即释放,要等子函数执行完后一起释放(虽然也是闭包,但是自执行,计算机知道它什么时候执行完->进而销毁)

不正当的使用闭包可能会造成内存泄漏(减少使用闭包,闭包会造成内存泄漏这句话是错的)

执行上下文:
JavaScript标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文”(execution context,简称 EC,也可以叫做执行环境)。

执行上下文栈:
我们可以知道,当程序运行时,可能产生多个执行上下文。为了对这些执行上下文进行管理,JavaScript引擎创建了一个后进先出的栈式结构(LIFO)–执行上下文栈(Execution context stack 简称 ECS)。

栈溢出:执行栈本身是有容量限制的,当执行栈内部的执行上下文对象积压到一定程度如果继续积压,就会报栈溢出(stack overflow)的错误
在这里插入图片描述
由图可知:
1.开始执行任何JavaScript代码前,会创建全局上下文并压入栈,所以全局上下文一直在栈底。
2.每次调用函数都会创建新的执行上下文(即便在函数内部调用自身),并压入栈。
3.函数执行完毕返回,其执行上下文出栈。
4.所有代码运行完毕,执行上下文栈只剩全局执行上下文。
5.没有相互引用的函数它是入栈执行完就出栈了,然后后面其他函数再入栈,所以才不会轻易造成栈溢出。
6.js的垃圾回收机制在cpu空闲时把没用的回收,栈会被重新排(标记整理算法:清除阶段之前会先执行整理,移动对象位置,使得他们在地址上是一个连续的空间)。

2.全局变量

我们知道 JavaScript 的垃圾回收是自动执行的,垃圾回收器每隔一段时间就会找出那些不再使用的数据,并释放其所占用的内存空间。

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

function fn(){
  // 没有声明从而制造了隐式全局变量test1
  test1 = 55
  
  // 函数内部this指向window,制造了隐式全局变量test2
  this.test2 =55
}
fn()

3.遗忘的定时器

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        node.innerHTML = JSON.stringify(someResource));
        // 定时器的回调函数没有释放
    }
    // 同时node、someResource 存储了大量数据 也无法回收
}, 1000);

解决方法: 在定时器完成工作的时候,用clearInterval 或者 clearTimeout手动清除定时器
另外,浏览器中的 requestAnimationFrame 也存在这个问题,我们需要在不需要的时候用 cancelAnimationFrame API 来取消使用

4.没有清理的DOM元素的引用(dom清空或删除时,事件以及 document.querySelector等对元素的引用未清除)

<div id="root">
  <ul id="ul">
    <li></li>
    <li></li>
    <li id="li3"></li>
    <li></li>
  </ul>
</div>
  let root = document.querySelector('#root')
  let ul = document.querySelector('#ul')
  let li3 = document.querySelector('#li3')
   root.removeChild(ul)   // 由于li3保留了对DOM节点的引用,整个ul及其子元素都不能GC
   console.log(ul);  //  能console出整个ul 没有被回收
   
   解决方法,手动置空:
  ul = null   // 虽置空了ul变量,但由于li3变量引用ul的子节点,所以ul元素依然不能被GC
  li3 = null   // 已无变量引用,此时可以GC

举一个闭包有关的一个例子(可以说明同一个实例,内存共享,多个实例相互不受影响)

function createFunc() {
  var result = new Array()

  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      console.log(i)
    }
  }
  return result
}
var result = createFunc()
result[0]() //10
result[1]() //10
result[2]() //10
result[3]() //10    //都是10,与想法冲突
result[4]() //10
result[5]() //10
result[6]() //10
result[7]() //10

改为

function createFunc() {
  var result = new Array()

  for (var i = 0; i < 10; i++) {
    result[i] = (function (num) {   //形成闭包环境,多个实例相互不受影响
      return function() {
        console.log(num)
      }
    })(i) //自调用外层的函数,内层return的函数才会被result[0]()调用 
  }
  return result
}
var result = createFunc()
result[0]() //0
result[1]() //1
result[2]() //2
result[3]() //3
result[4]() //4
result[5]() //5
result[6]() //6
result[7]() //7

闭包的用途:
1.从外部读取内部的变量

function f1(){
    var n=666;
    function f2(){
        console.log(n);
    }
    return f2;
}
var resule=f1();
resule()  ///666

2.将创建的变量的值始终保持在内存中:

function f1(){
    var n=12;
    function f2(){
        console.log(++n);
        
    }
    return f2;
}
var result=f1();
result(); //13

上面代码中,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

3.封装对象的私有属性和私有方法

function f1(n) {
  return function () {
    return n++;
  };
}
var a1 = f1(1);
a1() // 1
a1() // 2

var a2 = f1(5);
a2() // 5
a2() // 6
//这段代码中,a1 和 a2 是相互独立的,各自返回自己的私有变量。

闭包的优缺点:
优点:
1.可以访问到函数内部的局部变量,

2.可以避免全局变量的污染,

3.这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除。

缺点:增大了内存消耗,滥用闭包会影响性能,导致内存泄漏等问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值