JS的垃圾回收和内存泄漏

101 阅读8分钟

什么是GC

GC 即 Garbage Collection ,程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不

会再用的内存空间,而 GC 就是负责回收垃圾的,因为他工作在引擎内部,所以对于我们前端来说,GC 过程是相对比较

无感的,这一套引擎执行而对我们又相对无感的操作也就是常说的垃圾回收机制

当然也不是所有语言都有 GC,一般的高级语言里面会自带 GC,比如 Java、Python、JavaScript 等,也有无 GC 的语

言,比如 C、C++ 等,那这种就需要我们程序员手动管理内存了,相对比较麻烦。

垃圾产生/为何回收

我们知道写代码创建一个基本数据类型,对象,函数等时都是需要占用内存的,但是我们并不关注这些,因为这是引擎为我们分配的,我们不需要显式手动的去分配内存

我们举个例子

let a={a:1}

a=[1,2,3]

我们知道JS的引用数据类型是保存在堆内存中的,然后在栈内存中保存一个对堆内存实际对象的引用,所有,JS中对引用数据类型的操作都是操作对象的引用而不是实际的对象。可以简单理解为,栈内存中保存了一个地址,这个地址和堆内存中的实际值是相关的。

那上面代码首先我们声明了一个变量 a,它引用了对象 {a:1},接着我们把这个变量重新赋值了一个数组对象,也就变成了该变量引用了一个数组,那么之前的对象引用关系就没有了。没有了引用关系后,这部分内存就不会被使用了,少量还好,多了内存就是爆栈,所以需要被清理(回收)。程序的运行是需要内存的,只要程序提出要求,操作系统就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,不然内存越来越高,轻则影响吸引性能,重则就会导致进程崩溃。

垃圾回收策略

在 JavaScript 内存管理中有一个概念叫做 可达性 ,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收。至于如何回收,其实就是怎样发现这些不可达的对象(垃圾)它并给予清理的问题, JavaScript 垃圾回收机制的原理就是定期找出那些不再用到的内存(变量),然后释放其内存。(不是实时的找出无用内存并释放的原因:实时开销太大了)。

标记清除法

标记清除( Mark-Sweep ),目前在 JavaScript引擎 里这种算法是最常用的,到目前为止的大多数浏览器的

JavaScript引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同浏览器的 JavaScript

引擎 在运行垃圾回收的频率上有所差异。

此算法分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对

象)销毁。

你可能会疑惑怎么给变量加标记?

1.当变量进入执行环境时,反转某一位(通过一个二进制字符来表示标记);

2.维护进入环境变量和离开环境变量这样两个列表,可以自由的把变量从一个列表转移到另一个列表;

其实,怎样标记对我们来说并不重要,重要的是策略。

引擎在执行 GC(使用标记清除算法)时,需要从出发点去遍历内存中所有的对象去打标记,而这个出发点有很多,我们

称之为一组 根 对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象、文档DOM树 等。

整个标记清除算法大致过程就像下面这样:

1.垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0;

2.然后从各个根对象开始遍历,把不是垃圾的节点改成1;

3.清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间;

4.最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收;

优点

实现比较容易,打标记也无非打与不打两种情况,这使得一位二进制0,1就可以标记了

缺点

在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片,并且由于剩余空间内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题

假设我们新建对象分配内存时需要大小为 size,由于空闲内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于 size 的块才能为其分配:

常见的三种分配方法找到合适的块内存:

1.first-fit 找到大于等于size的块立即返回

2.best-fit 遍历整个空闲列表,返回大于等于size的最小块

3. worst-fit 遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分 size 大小,并将该部分返回

这三种策略里面 Worst-fit 的空间利用率看起来是最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fit 和 Best-fit 来说,考虑到分配的速度和效率 First-fit 是更为明智的选择

标记清除算法有两个明显的缺点

1.内存碎片化:空间内存块是不连续的,容易出现很多空闲内存块,还可能出现分配所需内存过大的对象时找不到合适的块

2.分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,

同时因为碎片化,大对象的分配效率会更慢;

引用计数算法

引用计数( Reference Counting ),这其实是早先的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,但因为它的问题很多,目前很少使用这种算法了

它的策略是跟踪记录每个变量值被使用的次数

1.当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为1

2.如果同一个值又被赋值给其它变量,那么引用次数加1

3.如果该变量的值被其它的值覆盖了,则引用次数减1

4.当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存;

虽然这种方式很简单,但是在引用计数这种算法出现没多久,就遇到了一个很严重的问题——循环引用,即对象 A 有一个指针指向对象 B,而对象 B 也引用了对象 A:

function test(){

let A = new Object()

let B = new Object()

A.b = B

B.a = A

}

对象 A 和 B 通过各自的属性相互引用着,按照上文的引用计数策略,它们的引用数量都是 2,但是,在函数 test 执行完成之后,对象 A 和 B 是要被清理的,但使用引用计数则不会被清理,因为它们的引用数量不会变成 0,假如此函数在程序中被多次调用,那么就会造成大量的内存不会被释放。我们再用标记清除的角度看一下,当函数结束后,两个对象都不在作用域中,A 和 B 都会被当作非活动对象来清除掉,相比之下,引用计数则不会释放,也就会造成大量无用内存占用,这也是后来放弃引用计数,使用标记清除的原因之一。

优点:1.引用计数在引用值为0时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾2.标记清除算法需要每隔一段时间进行一次,那在应用程序(JS脚本)运行过程中线程就必须要暂停去执行一段时间的 GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了。

缺点:1.需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限2.无法解决循环引用回收的问题

以前就是js垃圾回收的部分说明