1. 标记清除法
策略内容
标记清除(Mark-Sweep),目前在 JavaScript引擎里这种算法是最常见的,到目前为止大多数浏览器的 JavaScript 引擎 都在采用标记清除算法,只是各大浏览器厂商还对此算法进行了优化加工,且不同的浏览器的 Javascript 引擎 在运行垃圾回收的频率上有所差异。
此算法分为 标记 和 清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
如果给变量加标记的呢?
- 当变量进入执行环境时,反转某一位(通过一个二进制字符来表示标记);
- 维护进入环境变量和离开环境变量这样两个列表,可以自由的把变量从一个列表转移到另一个列表;
引擎在执行GC(使用标记清除算法)时,需要从出发点去遍历内存总所有的对象去打标记,而这个出发点有很多,我们称之为一组 根对象,而所谓的根对象,其实在浏览器环境中包括又不止于 全局Window对象,文档DOM树等。
整个标记清除算法大致过程就像下面这样:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0;
- 然后从各个根对象开始遍历,把不是垃圾节点改成1;
- 清理所有标记为0的垃圾,销毁并回收它们所占有的内存空间;
- 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收;
优点
实现比较简单,打标记也无非与不打两种情况,这使得一位二进制位(0和1)就可以为其标记,非常简单。
缺点
在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片,并且由于剩余空间内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题:
假设我们新建对象分配内存时需要大小为size,由于空间内存是间断的、不连续的,则需要对空闲内存列表进行一次单向遍历找出大于等于size 的块才能为其分配:
常见包括三种分配策略找到合适的块内存:
- First-fit ,找到大于等于 size 的块立即返回;
- Best-fit,遍历整个空间列表,返回大于等于 size 的最小分块;
- Worst-fit,遍历整个空闲列表,找到最大的分块,然后切成两部分,一部分分 size 大小,并将该部分返回;
这三种策略里面 Worst-fit 的空间利用率看起来时最合理,但实际上切分之后会造成更多的小块,形成内存碎片,所以不推荐使用,对于 First-fit 和 Best-fit 来说,考虑到分配的速度和效率 First-fit 是更为明智的选择。
综上所述,标记清除算法或者说策略就有两个明显的缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现更多的空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块;
- 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大堆象的分配效率会更慢;
归根结底,标记清除算法的缺点在于清除之后剩余的对象位置不变而导致的空闲内存不连续,所以只要解决这一点,两个缺点都可以完美解决了。
而 标记整理(Mark-Compact)算法就可以有效的解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存;
2. 引用计数算法
策略内容
引用计数(Reference Counting),是很早的一种垃圾回收算法,它把对象是否不再需要简化定义为对象有没有其他对象引用它,如果没有引用指向该对象(零引用),对象将被回收。但是它有很多问题,目前很少使用这种算法了。
它的策略就跟踪记录每个变量值被使用的次数
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为1;
- 如果同一个值又被赋给另一个变量,那么引用数加1;
- 如果该变量的值被其他的值覆盖了,则引用次数减1;
- 当这个值的引用次数变为0时,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为0的值占用的内存;
这种方式很简单,但是也有一个问题。两个对象相互引用了对方——循环引用。这样对象在函数执行结束后被清理后,引用数也不会变成0,那么就会造成大量的内存不会被释放。
优点
- 引用计数在引用值为0时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾;
- 标记清除算法需要每隔一段时间进行一次,那在应用程序中运行过程中线程就必须要暂停去执行一段时间的GC,另外,标记清除算法需要遍历堆里的活动以及非活动对象来清除,而引用计数则只需要在引用时计数就可以了;
缺点
- 需要一个计数器,而此计数器需要占很大的位置,因为我们也不知道被引用数量的上限;
- 无法解决循环引用无法回收的问题;