首先我们要知道如果没有垃圾回收机制我们的代码会发生什么。。。
答案。。。
内存泄漏!
所以什么是内存泄漏呢?
我们知道程序运行是需要内存的,操作系统或者运行时(runtime)就必须要提供内存。对于持续运行的程序或者服务来说,必须要及时释放不再用的内存。否则,内存占用越来越高。就会导致系统性能(卡顿等),程序崩溃等。
而不在使用的内存没有得到及时的释放,就叫做内存泄漏。
有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。
char buffer; buffer = (char) malloc(42); free(buffer);
上面是 C 语言代码,malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。
这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector)。
垃圾回收机制
垃圾回收机制会在创建变量时自动分配内存,在不使用的时候会自动周期性的释放内存,释放的过程就叫 "垃圾回收"。这个机制有好的一面,当然也也有不好的一面。一方面自动分配内存减轻了开发者的负担,开发者不用过多的去关注内存使用,但是另一方面,正是因为因为是自动回收,所以如果不清楚回收的机制,会很容易造成混乱,而混乱就很容易造成"内存泄漏".由于是自动回收,所以就存在一个 "内存是否需要被回收的" 的问题,但是这个问题的判定在程序中意味着无法通过某个算法去准确完整的解决,后面探讨的回收机制只能有限的去解决一般的问题。
回收算法
- 标记清除
垃圾回收器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记(闭包)。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾回收器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
目前主流浏览器都是使用标记清除式的垃圾回收策略,只不过收集的间隔有所不同
缺点:
1、回收后会形成内存碎片,影响后面申请大的连续内存空间
- 引用计数
引用计数策略相对而言不常用,因为弊端较多。其思路是对每个值记录它被引用的次数,通过最后对次数的判断(引用数为0)来决定是否保留,具体的规则有
1、声明一个变量,赋予它一个引用值时,计数+1;
2、同一个值被赋予另外一个变量时,引用+1;
3、保存对该值引用的变量被其他值覆盖,引用-1;
4、引用为0,回收内存;
缺点:
最主要的就是循环引用的问题
eg:
function refProblem () { let a = new Object(); let b = new Object(); a.c = b; b.c = a; //互相引用 }
Nodejs V8回收机制
首先先来了解V8的内存结构
- 新生代(New Space/Young Generation): 大多数新生对象被分配到这,分为两块空间,整体占据小块空间,垃圾回收的频率较高,采用的回收算法为 Scavenge 算法
- 老生代(Old Space/Old Generation):大多数在新生区存活一段时间后的对象会转移至此,采用的回收算法为标记清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)算法,内部再细分为两个空间
- 指针空间(Old pointer space): 存储的对象含有指向其他对象的指针
- 数据空间(Old data space):存储的对象仅包含数据,无指向其他对象的指针
- 大对象空间(Large Object Space):存放超过其他空间(Space)限制的大对象,垃圾回收器从不移动此空间中的对象
- 代码空间(Code Space): 代码对象,用于存放代码段,是唯一拥有执行权限的内存空间,需要注意的是如果代码对象太大而被移入大对象空间,这个代码对象在大对象空间内也是拥有执行权限的,但不能因此说大对象空间也有执行权限
- Cell空间、属性空间、Map空间 (Cell ,Property,Map Space): 这些区域存放Cell、属性Cell和Map,每个空间因为都是存放相同大小的元素,因此内存结构很简单。
Scavenge 算法
Scavenge 算法是新生代空间中的主要算法,该算法由 C.J. Cheney 在 1970 年在论文 A nonrecursive list compacting algorithm 提出。
Scavenge 主要采用了 Cheney算法,Cheney算法新生代空间的堆内存分为2块同样大小的空间,称为 Semi space,处于使用状态的成为 From空间 ,闲置的称为 To 空间。垃圾回收过程如下:
- 检查From空间,如果From空间被分配满了,则执行Scavenge算法进行垃圾回收
- 如果未分配满,则检查From空间的是否有存活对象,如果无存活对象,则直接释放未存活对象的空间
- 如果存活,将检查对象是否符合晋升条件,如果符合晋升条件,则移入老生代空间,否则将对象复制进To空间
- 完成复制后将From和To空间角色互换,然后再从第一步开始执行
晋升条件
- 经历过一次Scavenge 算法筛选;
- To空间内存使用超过25%;
标记清除 & 整理(Mark-Sweep & Mark-Compact,Major GC)算法
之前说过,标记清除策略会产生内存碎片,从而影响内存的使用,这里 标记整理算法(Mark-Compact)的出现就能很好的解决这个问题。标记整理算法是在 标记清除(Mark-Sweep )的基础上演变而来的,整理算法会将活跃的对象往边界移动,完成移动后,再清除不活跃的对象。
由于需要移动移动对象,所以在处理速度上,会慢于Mark-Sweep。
全停顿(Stop The World )
为了避免应用逻辑与垃圾回收器看到的逻辑不一样,垃圾回收器在执行回收时会停止应用逻辑,执行完回收任务后,再继续执行应用逻辑。这种行为就是 全停顿,停顿的时间取决与不同引擎执行一次垃圾回收的时间。这种停顿对新生代空间的影响较小,但对老生代空间可能会造成停顿的现象。
增量标记(Incremental Marking)
为了解决全停顿的现象,2011年V8推出了增量标记。V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直至标记完成。
那我们日常中如何监听内存使用情况
1、对于chrome浏览器(84.0.4147.105),F12打开开发者工具;
2、在More Tools中找到Performance monitor;
常见的内存泄漏场景
- 意外声明全局变量
function test() {x = new Array(100000);
}
test();
console.log(x);
未声明的对象会被绑定在全局对象上,就算不被使用了,也不会被回收,所以写代码的时候,一定要记得声明变量。
- 定时器
let name = 'Tom';setInterval(() => {
console.log(name);
}, 100);
定时器的回调通过闭包引用了外部变量,如果定时器不清除,name会一直占用着内存,所以用定时器的时候最好明白自己需要哪些变量,检查定时器内部的变量,另外如果不用定时器了,记得及时清除定时器。
- 闭包
let out = function() {let name = 'Tom';
return function () {
console.log(name);
}
}
由于闭包会常驻内存,在这个例子中,如果out一直存在,name就一直不会被清理,如果name值很大的时候,就会造成比较严重的内存泄漏。所以一定要慎重使用闭包。
- 事件监听
mounted() {
window.addEventListener("resize", () => {});
}
在页面初始化时绑定了事件监听,但是在页面离开的时候未清除事监听,就会导致内存泄漏。
- 缓存爆炸
通过 Object/Map 的内存缓存可以极大地提升程序性能,但是很有可能未控制好缓存的大小和过期时间,导致失效的数据仍缓存在内存中,导致内存泄漏:
const cache = {};function setCache() {
cache[Date.now()] = new Array(1000);
}
setInterval(setCache, 100);
上面这段代码中,会不断的设置缓存,但是没有释放缓存的代码,导致内存最终被撑爆。
参考资料: