JS的垃圾回收机制

JS的垃圾回收机制

JS垃圾回收机制
JS环境中分配的内存,一般有如下生命周期:

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
    全局变量一般不会回收,一般是局部变量的值不用了,会被自动回收掉

JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收

垃圾回收算法
所谓垃圾回收,核心思想就是如何判断内存是否已经不再使用了,如果是,则视为垃圾,释放掉
常用的浏览器垃圾回收算法:引用计数 和 标记清除

引用计数:IE采用的是引用计数,它定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用,如果没有其他对象指向它了,说明该对象已经不再需要了

标记清除:现在的浏览器都已经不再使用引用计数算法了,大多是基于标记清除算法的改进算法
标记清除法:

  • 标记清除算法将 “不再使用的对象” 定义为 “无法达到的对象”
  • 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象
  • 凡是能从根部到达的对象,都是还需要使用的。那些无法从根部出发触及到的对象被标记为不再使用,稍后进行回收

具体验证代码

1.引用计数

<script>

    let person = {          // person变量,存储着对象的引用,引用计数+1 // 1
        name: 'ifCode',
        age: 22
    }
    let p = person          // p变量,也存储着对象的引用,引用计数+1 // 2

    person = 1              // person中存1了,引用计数-1 // 1

    p = null                // p中存null了,引用计数-1 // 0,没有任何变量存储着对象空间的引用,这块空间就会被释放


// 引用计数的致命问题:循环引用=>内存泄漏(访问不到,也不释放)
    function cycle(){
        let o1 = {}
        let o2 = {}
        o1.a = o2
        o2.a = o1
        return "cycle reference"
    }
    cycle()
</script>

2.标记清除

<script>

// 按照标记清除算法来看,这个例子中的内存已经正确被垃圾回收了
    function cycle(){
        let o1 = {}
        let o2 = {}
        o1.a = o2
        o2.a = o1
        return "cycle reference"
    }
    cycle()

</script>

补充

关于内存泄漏

在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

也就是说,如果内存不需要时,没有经过生命周期的释放期,那么就存在内存泄漏。

内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。

JavaScript 内存泄漏的一些场景

JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况。程序员要让浏览器内存泄漏,浏览器也是管不了的。

下面有些例子是在执行环境中,没离开当前执行环境,还没触发标记清除法。所以你需要读懂上面 JavaScript 的内存回收机制,才能更好理解下面的场景。

意外的全局变量

// 在全局作用域下定义

function count(number) {
  // basicCount 相当于 window.basicCount = 2;
  basicCount = 2;
  return basicCount + number;
}

不过在 eslint 帮助下,这种场景现在基本没人会犯了,eslint 会直接报错,了解下就好。

被遗忘的计时器

无用的计时器忘记清理是新手最容易犯的错误之一。

就拿一个 vue 组件来做例子。

<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
}
</script>

上面的组件销毁的时候,setInterval 还是在运行的,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候清除计时器,如下:

<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    this.refreshInterval = setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
  beforeDestroy() {
    clearInterval(this.refreshInterval)
  },
}
</script>
被遗忘的事件监听器

无用的事件监听器忘记清理是新手最容易犯的错误之一。

还是继续使用 vue 组件做例子。

<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    window.addEventListener('resize', () => {
      // 这里做一些操作
    })
  },
}
</script>

上面的组件销毁的时候,resize 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:

<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    this.resizeEventCallback = () => {
      // 这里做一些操作
    }
    window.addEventListener('resize', this.resizeEventCallback)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizeEventCallback)
  },
}
</script>
被遗忘的 ES6 Set 成员

如果对 Set 不熟悉,可以看这里。

如下是有内存泄漏的(成员是引用类型的,即对象):

let map = new Set();
let value = { test: 22};
map.add(value);

value= null;
需要改成这样,才没内存泄漏:

let map = new Set();
let value = { test: 22};
map.add(value);

map.delete(value);
value = null;

有个更便捷的方式,使用 WeakSet,WeakSet 的成员是弱引用,内存回收不会考虑到这个引用是否存在。

let map = new WeakSet();
let value = { test: 22};
map.add(value);

value = null;
被遗忘的 ES6 Map 键名

如果对 Map 不熟悉,可以看这里。

如下是有内存泄漏的(键值是引用类型的,即对象):

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
key = null;

需要改成这样,才没内存泄漏:

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

map.delete(key);
key = null;

有个更便捷的方式,使用 WeakMap,WeakMap 的键名是弱引用,内存回收不会考虑到这个引用是否存在。

let map = new WeakMap();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

key = null;
被遗忘的订阅发布事件监听器

这个跟上面的被遗忘的事件监听器的道理是一样的。

假设订阅发布事件有三个方法 emit 、on 、off 三个方法。

还是继续使用 vue 组件做例子。

<template>
  <div @click="onClick"></div>
</template>

<script>
import customEvent from 'event'

export default {
  methods: {
    onClick() {
      customEvent.emit('test', { type: 'click' })
    },
  },
  mounted() {
    customEvent.on('test', data => {
      // 一些逻辑
      console.log(data)
    })
  },
}
</script>

上面的组件销毁的时候,自定义 test 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:

<template>
  <div @click="onClick"></div>
</template>

<script>
import customEvent from 'event'

export default {
  methods: {
    onClick() {
      customEvent.emit('test', { type: 'click' })
    },
  },
  mounted() {
    customEvent.on('test', data => {
      // 一些逻辑
      console.log(data)
    })
  },
  beforeDestroy() {
    customEvent.off('test')
  },
}
</script>
被遗忘的闭包

闭包是经常使用的,闭包能给我们带来很多便利。

首先看下这个代码:

function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
const reverseName = closure()

// 这里调用了 reverseName
reverseName();
上面有没有内存泄漏?

上面是没有内存泄漏的,因为name 变量是要用到的(非垃圾)。这也是从侧面反映了闭包的缺点,内存占用相对高,量多了会有性能影响。

但是改成这样就是有内存泄漏的:

function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
const reverseName = closure()

在当前执行环境未结束的情况下,严格来说,这样是有内存泄漏的,name 变量是被 closure 返回的函数调用了,但是返回的函数没被使用,这个场景下 name 就属于垃圾内存。name 不是必须的,但是还是占用了内存,也不可被回收。

当然这种也是极端情况,很少人会犯这种低级错误。这个例子可以让我们更清楚的认识内存泄漏。

脱离 DOM 的引用

每个页面上的 DOM 都是占用内存的,假设有一个页面 A 元素,我们获取到了 A 元素 DOM 对象,然后赋值到了一个变量(内存指向是一样的),然后移除了页面的 A 元素,如果这个变量由于其他原因没有被回收,那么就存在内存泄漏,如下面的例子:

class Test {
  constructor() {
    this.elements = {
      button: document.querySelector('#button'),
      div: document.querySelector('#div'),
      span: document.querySelector('#span'),
    }
  }
  removeButton() {
    document.body.removeChild(this.elements.button)
    // this.elements.button = null
  }
}

const a = new Test()
a.removeButton()

上面的例子 button 元素 虽然在页面上移除了,但是内存指向换为了 this.elements.button,内存占用还是存在的。所以上面的代码还需要这样写: this.elements.button = null,手动释放这个内存。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值