首先明确一点,什么是垃圾?
是当写代码的人觉得某个东西不需要了,那就是垃圾。
垃圾回收机制(Garbage Collection)简称GC
垃圾回收是指自动释放不再使用的内存。js中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
那垃圾回收器怎么知道这个东西你是否需要呢?
其实它不知道,但是访问不到的东西你一定不想要,那这些东西就会被回收
内存的生命周期
内存管理大概分成三个步骤:
- 内存分配: 当声明变量、函数、对象的时候,系统会自动分配内存;
- 内存使用: 使用分配到的内存进行读写操作,也就是使用变量、函数等;
- 内存回收: 使用完毕后,将空间进行释放和归还,由垃圾回收器自动回收不再使用的内存。
说明:
全局变量一般不会回收,除非把页面关闭,页面关闭就被自动释放
一般情况下局部变量的值,不使用之后会立马被自动回收掉
但是当程序中分配的内存由于某种原因程序未被释放掉或无法释放的时候,这种情况叫做内存泄漏。
尽管js的垃圾回收机制强大,但内存泄漏仍然可能发生。当内存泄漏变大后,就会严重的影响系统的运行。
让不需要的内存不可触达即可。把不需要的变量设置为null,就触达不了该变量之前的值了,那么垃圾回收器就会回收掉。
算法说明
堆栈空间区别
栈:由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面。
堆:一般由程序员分配释放,若程序员不释放,则由垃圾回收机制回收。复杂数据类型放到堆里面。
垃圾回收算法就是垃圾回收器按照固定的时间间隔,周期性地寻找那些不再使用的变量,然后将其清除或释放。
在浏览器的发展史上,用到两种主要的垃圾回收算法:引用计数法和标记清除法。
引用计数
IE采用的是引用计数算法,但现在没那么常用。
其思路是对每个值都记录它被引用的次数。
算法:
- 当变量声明赋值后,值得引用数为1
- 如果同一个值被赋值给另一个变量后,引用数加1
- 如果该值引用的变量被其他值给覆盖了,引用数减1
- 如果引用数为0,则释放内存
let person = {
name:'tom',
age:18
}
// 声明变量并给它赋一个引用值,引用数为1
let people = person
//将同一个值赋给新的变量people,因为复制的是地址,则指向同一个值,引用数加1,引用数为2
person = null
// 该变量被null覆盖,引用数减1,引用数为1
people = null
//该变量被null覆盖,引用数减1,引用数为0
//该值的引用数为0,则释放内存,回收该对象
由此看出,引用计数法是一个简单有效的算法
但是,引用计数法有一个致命的问题就是嵌套引用(循环引用)
所谓嵌套引用,就是对象a引用了对象b,对象b也引用了对象a,两个对象互相引用。
function demo(){
let obj1 = {}
let obj2 = {}
obj1.a = obj2 //把obj2的值存在了obj1里面
obj2.a = obj1 //把obj1的值存在了obj2里面
}
demo()
//因为obj1,obj2是局部变量,所以理论上demo执行完后,内存将会被释放
// 但是两个对象循环引用,就一直引用,引用数不会为0,就一直不会被回收掉
这样的相互引用,它们的引用数都不为0,垃圾回收器不会进行回收,就会导致内存泄漏。
想要解决,就要手动将obj1=null,obj2=null,obj1.a=null,obj2.a=null
标记清除
现在的浏览器已经不再使用引用计数法了
引用计数法的核心是不再使用了就被回收,而标记清除法的核心是找不到就被回收
算法:
- 从js的根部(全局对象)开始,定时扫描内存中的对象,然后将遍历到的所有对象分别给它们打上标记
- 将无法从根部出发触及到的对象被标记为不再使用
- 垃圾回收器做一次内存清理
标记清除法解决了引用计数法循环引用的问题。
function demo(){
let obj1 = {}
let obj2 = {}
obj1.a = obj2 //把obj2的值存在了obj1里面
obj2.a = obj1 //把obj1的值存在了obj2里面
}
demo()
// 因为demo里面的变量是局部变量,当函数执行完后,从根部出发找不到变量,所以就被清除掉
其他算法优化补充
js引擎比较广泛的采用就是可达性中的标记清除法,类似于V8引擎为了进行更好的优化,在算法的实现细节上也会结合一些其他的算法。
标记整理
和标记清除类似。
假如多个对象分配的内存是连续的,但销毁了一些内存后导致这些内存不再连续。就会导致产生很多内存碎片,如果不进行清理内存碎片,就会对存储造成影响。
那么标记整理法就能够有效地解决这个问题。
回收期间同时会将保留的存储对象搬运汇集到连续的存储空间,从而整合空间内存,避免内存碎片化。
分代收集
对象被分成两组,分成“新的”和“旧的”。
许多对象出现后完成它们的工作并很快死区,它们可以很快被清理,将此保存到“新生代”内存里
而那些长期存活的对象会变得“老旧”,而且被检查的频次也会减少,将此保存到“旧生代”内存里
新生代内存还分为两个区域,使用区和空闲区。
新加入的对象都会存放在使用区,将垃圾回收后剩余的数据放入空闲区,然后将使用区和空间区进行交换。如果经过回收之后数据依然一直存在,则把数据放入旧生代内存中
增量收集
如果有多个对象,想要一次性遍历所有对象并且标记出来,则需要很长一段事件,就会带来明显的延迟。
因此V8想要将垃圾收集工作分成几部分来做,然后将这几部分逐一进行处理,这样就会有许多小的延迟,而不是一个大的延迟。
闲时收集
垃圾回收器只会在CPU空闲时尝试运行,以减少可能对代码执行的影响