一文理清由闭包引发内存泄漏和垃圾回收机制

4,527 阅读5分钟

闭包

  • 闭包的定义:当内部的函数被保存到外部时,将会生成闭包,闭包会导致原有的作用域链不释放,造成内存泄漏。
  • 闭包的好处:
    • 变量长期驻扎在内存中
    • 避免污染全局变量
    • 私有成员的存在
  • 闭包的坏处:
    • 增大内存的使用量
    • 容易造成内存泄漏
  • 闭包的作用/使用场景
    • 实现共有变量 =》 做累加器
      • 代码实现
        function add() {
            var count = 0;
            function demo() {
                count ++;
                console.log(count);
            }
            return demo;
        }
        var counter = add();
        counter();
        counter();
        
    • 可以做缓存
      • 代码实现
        function eater() {
            var food = '';
            var obj = {
                eat: function() {
                    console.log('i am eating' + ' ' + food);
                    food = '';
                },
                push: function(myFood) {
                    food = myFood;
                }
            }
            return obj;
        }
        var eater1 = eater();
        eater1.push('banana');
        eater1.eater();
        
    • 可以实现封装 属性私有化
      • 代码实现
        function Hang(name, wife) {
            var prepareWife = 'xiaozhang';
            this.name = name;
            this.wife = wife;
            this.divorce = function() {
                this.wife = prepareWife;
            }
            this.changePrepareWife = function(target) {
                prepareWife = target;
            }
            this.sayPrepareWife = function() {
                console.log(prepareWife);
            }
        }
        var deng = new Hang('deng', 'xiaoliu');
        deng.prepareWife;
        deng.sayPrepareWife();
        
  • 闭包的防御
    • 闭包会导致多个执行函数共用一个公有变量,如果不是特殊需要,应该尽量防止这种情况发生。
  • 解决闭包的方法
    • 使用立即执行函数

立即执行函数

  • 立即执行函数定义:此类函数没有声明,在一次执行过后释放,适合做初始化工作。
  • 立即执行函数和普通函数的区别:立即执行函数执行完就被释放

内存泄漏

  • 内存泄漏定义:应用程序不再需要占用的时候,由于某些原因,内存没有被操作系统或可用内存池回收。
  • 内存泄漏的例子:
    • 意外的全局变量
      • 在函数内未声明的变量就赋值,这样会在全局对象创建一个新的变量。
        function bar() {
            say = 'hehe';
        }
        
        即==
        function bar() {
            window.say ='hehe';
        }
        
      • 或者是使用this创建了全局的变量
        function foo() {
            this.name = 'hehe';
        }
        foo();
        
    • 被遗忘的计时器或回调函数
      • 使用计时器setInterval()未清除,在老版本的IE6是无法处理循环引用的,会造成内存泄漏。
    • 脱离DOM的引用的
    • 闭包
  • 如何识别的内存泄漏
    • Chrome
      • Timeline
      • Profiles
    • Node
  • 怎么解决
    • 手动释放
      • 代码实现
        var arr = [1, 2, 3];
        arr = null;
        
    • 使用弱引用(weakset和weakmap)
      • 优点:WeakMap里面对element的引用就是弱引用,不会被计入垃圾回收机制的。也就是说一旦消除对该节点的引用,它的占用内存就会被垃圾回收机制释放。WeakMap保存的这个键值对,也会自动消失。
      • 代码实现
        const vm = new WeakMap();
        const element = document.getElementById('example');
        vm.set(element, 'something');
        vm.get(element);
        

垃圾回收机制

  • 定义:
    • 找到内存空间中的垃圾。
    • 回收垃圾,让程序员能再次利用这部分的空间。
  • 常用的垃圾回收算法
    • 引用计数法
      • 定义:跟踪记录每个值被引用的次数。
      • 优点:
        • 可即刻回收:当被引用数值为0,对象马上会把自己作为空闲空间连到空闲链表上。也就是说,在变成垃圾的时候就立刻被回收。
        • 因为是即使回收,那么程序不会暂停去单独使用很长一段时间的GC,那么最大暂停时间很短。
        • 不用去遍历堆里面的所有活动对象和非活动对象。
      • 缺点:
        • 计数器需要占很大的位置:因为不能预估被引用的上限,打个比方,可能出现32位即2的32次方个对象同时引用一个对象,那么计时器就需要32位。
        • 最大的劣势是无法解决循环引用无法回收的问题,这就是IE之前出现的问题。
      • 代码示例
        var a = new Object();
        var b = a;
        a = null;
        b = null;
        
    • 标记清除法(在V8引擎使用最多的)
      • 定义:
        • 标记阶段:把所有活动对象做上标记
        • 清除阶段:把没有标记(也就是说非活动对象)销毁
      • 优点:
        • 实现简单,打标记用一位二进制就可以表示
        • 解决了循环引用的问题
      • 缺点:
        • 造成碎片化(有点类似磁盘的碎片化)
        • 再分配时遍历次数多,如果一直没有找到合适的内存块大小,那么会遍历空闲链表(保存堆中所有空闲地址空间的地址形成的链表)一直遍历到尾端。
    • 复制算法
      • 将一块内存空间分为两部分,分别为From空间和To空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From空间中,当From空间被占满的时,新生代GC就会启动了。算法就检查From空间中存活的对象复制到To空间中,如果有失活的对象就会销毁,当赋值完成后将From,空间和To空间互换,这样GC就结束了。
  • 减少JavaScript中垃圾回收
    • 对象优化
      • 最大限度复用对象,不用尽可能设置为null,尽快被垃圾回收掉。
        var obj = {};
        for(var i = 0; i < 10; i++) { 
          // var obj = {}; // 这样每次都会创建一个对象
          obj.age = 19;
          obj.name = 'hehe';
          console.log(obj);
        }
        
    • 数组Array优化
      • 将[]赋值给一个数组对象,是清空数组的捷径,但是这样又创建一个新的空对象,并且将原来的数组对象变成了一片小内存垃圾。
        var arr = [1, 2, 3];
        arr = [];
        
      • 将数组长度赋值为0,也能达到清空数组的目的,并且同时能实现数组重用,减少内存垃圾的产生。
        var arr = [1, 2, 3];
        arr.length = 0;
        
    • 方法function优化
      • 例如在游戏的主循环中,setTimout或requestAnimationFrame来调用一个成员方法是很常见的。每次调用都返回一个新的方法对象,这就导致了大量的方法对象垃圾。为了解决这个方法,可以将作为返回值的方法保存起来。
        function say() {
          console.log('hehe');
        }
        setTimeout((function(self) {
          return function() {
            self.say();
          }
        })(this), 16)
        
        
        // 优化
        this.sayFunc = (function(self) {
          return function() {
            self.say();
          }
        })(this);
        setTimout(this.sayFunc, 16)
        

你的点赞是我持续输出的动力,希望能帮助到大家,互相学习,有任何问题下面留言,一定回复!!!