1. 初识回收机制
代码执行完成后,变量不再被使用,这些变量如果没有被及时回收,易造成内存占用过满,无法为新的变量分配内存,从而发生内存泄漏。
回收机制需要解决的点:
- (1)回收不再使用的变量
- (2)引用环的变量如何判断是否是不再使用的变量
- (3)如何将碎片化内存转化成大块的内存,供分配大变量
2. 常见的回收机制
如JVM、PHP引擎等内存回收会用到以下的一些回收机制
-
(1)引用计数器:当该对象被其他对象使用一次,计数器加1,反之不被引用时,计数器减1,当计数器为0,表示不被使用,该变量可以被回收。
优点:计算简单、效率高
缺点:存在引用环的时候,无法判断变量是否可以被回收
-
(2)标记-清除算法:分为标记阶段和清除阶段,标记阶段从根节点出发,根据引用关系链,在链上面的变量被认为是在使用的,不再链上面的变量被认为可以回收。清除阶段将不再引用链上面的变量清除
优点:计算简单、效率高; 可以解决找到不被使用的引用闭环,并被回收
缺点:
- 垃圾回收过程中的停顿:标记-清除算法会暂停程序的执行,进行垃圾回收操作。当堆中对象较多时,可能会导致明显的停顿,影响用户体验。
- 内存碎片化:标记-清除算法会在回收过程中产生大量的不连续的、碎片化的内存空间。这可能导致后续的内存分配难以找到足够大的连续内存块,从而使得内存的利用率降低。
-
(3)标记-整理(或标记-整理-清除):可以看作是标记-清除的增强版本,他在标记阶段的操作和标记清除一致,但是清除阶段之前会先执行整理,移动对象位置,对内存空间进行压缩,目的是为将碎片化内存合并成更大的内存
优点:解决了上述内存碎片化的问题
缺点:垃圾回收过程中的停顿问题依旧存在
2. V8回收机制
V8是开源的js执行引擎,主要用于浏览器和node.js。由google团队开发,被用于google chrome浏览器,V8采用即时编译技术,负责将js代码转化成机器码,供计算机直接执行,提升js执行速度。
在Chrome中,v8被限制了内存的使用(64位约1.4G/1464MB , 32位约0.7G/732MB),为什么要限制呢?
- 表层原因是,V8最初为浏览器而设计,不太可能遇到用大量内存的场景
- 深层原因是,V8的垃圾回收机制的限制(如果清理大量的内存垃圾是很耗时间,这样回引起JavaScript线程暂停执行的时间,那么性能和应用直线下降)
在JavaScript中,其实绝大多数的对象存活周期都很短,大部分在经过一次的垃圾回收之后,内存就会被释放掉,而少部分的对象存活周期将会很长,一直是活跃的对象,不需要被回收。为了提高回收效率,V8 将堆分为两类新生代和老生代,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
新生区通常只支持 1~8M 的容量,而老生区支持的容量就大很多了。对于这两块区域,V8 分别使用两个不同的垃圾回收器,以便更高效地实施垃圾回收。
- 副垃圾(新生代)回收器 - Scavenge:主要负责新生代的垃圾回收。
- 主垃圾(老生代)回收器 - Mark-Sweep(标记-清除) & Mark-Compact(标记-整理):主要负责老生代的垃圾回收。
此策略中,将堆内存中的区域划分成两大块,一个叫新生代(Young Generation),一个叫老年代(Young Generation)两个区域,在新生代中每次垃圾收集时都发现有大量对象死去,然后每次回收后存活的少量对象逐步晋升到老年代中存放。
用完就能丢弃(使用周期短)的对象就放在新生代中,长时间使用的对象就放在老年代中。这样就可以根据生命周期的不同特点使用不同的垃圾回收策略,老年代的垃圾回收很久才一次,新生代就会经常进行垃圾回收。
2.1 新生代回收算法:Scavenge算法
Scavenge算法具体也用到了Mark-Sweep(标记-清除) & Mark-Compact(标记-整理)算法。看上图V8堆空间的管理,新建的对象会被分配到新生区的对象区,当对象区分配满了之后,则会进行一次垃圾回收,由此来看,新生区垃圾回收的频率跟对象区的大小有关(注意:对象区和空闲区均是新生区的1/2大小)。回收的过程如下:
(1)标记阶段:利用上述的“标记-清除”或“标记-整理”方法中的标记方式,找到所有被引用的变量,这里称“活跃对象”
(2)复制阶段(个人认为可理解为“整理阶段”):类似于“标记-整理”方法,用于将在使用的内存都整理成连续的内存,减少碎片内存,方便后续的内存分配。 具体是通过将对象区的活动对象一一复制到空闲区(并且排序),从而空闲区中占用了连续的内存空间
(3)清除阶段:这时候“对象区“的内存可以释放掉 (4)空间交换:对象区和空闲区的内存空间进行交换后,这时候对象区中的活动对象占用分配的内存都是连续的,剩下的可分配内存也是连续的。
优缺点:
- 优点:算法简单
- 缺点:新生代回收算法,在回收阶段(含清除、空间交换)是依次在复制阶段之后的,因此可以认为是串行的,也就是说回收会存在“停顿”,同时进入回收过程(4个阶段)的时候,与JS执行是互斥的,JS执行会“停顿”,综合以上2点,性能相对差,但由于新生代的变量都是小变量、内存空间小,因此影响并不明显。
2.2 老生代回收算法:Mark-Sweep & Mark-Compact算法
上面讲到新生代中每次垃圾收集时都发现有大量对象回收,然后每次回收后会有部分对象逐步晋升到老年代中存放,这里的晋升机制是:
(1)内存占用达到阈值:当对象的内存占用大于一定的比例(一般是25%-50%),该变量自动存放到老生区,并从新生区移除(若已存在于新生区)
(2)年龄达到阈值:当对象长时间占用新生区(也就是回收多次后仍存在,一般5次),该变量自动存放到老生区,并从新生区移除
介于新生代的停顿问题,由于老生代的变量大、内存空间大,回收时间长,因此若采用与新生代一样的算法,停顿问题将放大(成为严重性能瓶颈),因此老生代会配合并行、增量、并发机制结合标记-清除&标记-整理算法进行回收。
2.2.1 并行&增量回收
并行机制就不多说了,就是利用多线程的机制,辅助线程并行执行的任务GC(回收)。增量回收,可以理解为任务JS(执行JS&分配内存)与任务GC(回收)交替执行,这样卡顿(阻塞)就能很大程度的缓解。
2.2.2 并发回收
对于增量回收,主线程在执行JS任务,辅助线程在后台完成GC任务时,可能会同时更改或使用同一个变量,这就需要使用类似“锁机制”来保证数据的完整性和可靠性,这种机制属于“并发机制”的范畴。
并行垃圾回收和增量垃圾回收依然会阻塞主线程,并发回收就可以解决这个问题。并行回收,将GC任务颗粒度细化成更小的任务,如整理、标记,则并发性更高。
并行和并发的区别: (1)并行是多个任务同时执行 (2)并发是多个任务之间存在资源竞争(如这里的对内存变量的读写),需要用锁机制
2.2.3 Mark-Sweep(标记-清除) & Mark-Compact(标记-整理)算法
这里主要列出V8老回收区的回收机制中采用的一些特殊的机制,用来提升性能的。
新标记算法(三色标记法)
在没有采用增量算法之前,老生代的垃圾回收是采用标记清理算法和标记整理算法,单纯使用黑色和白色来标记数据,在一次执行完整的标记前,垃圾回收会将所有数据设置为白色,然后从根开始深度遍历,将所有能访问到的数据标记为黑色,标记为黑色的数据对象就是活动对象,剩余的白色数据对象也就是待清理的垃圾对象。
增加灰色的标记,是方便在下一次增量GC任务开始的时候,快速找到需要回收的变量,具体说明参考:juejin.cn/post/727414…
惰性清理
采用这种延迟清理的原因是因为在增量标记之后,要进行清理非活动对象的时候,垃圾回收器发现了其实就算是不清理,剩余的空间也足以让JS代码跑起来,所以就延迟了清理,让JS代码先执行,或者只清理部分垃圾,而不清理全部。
3. 总结
关于回收机制,主要掌握“标记-清除”和“标记-整理”方法,回收的过程万变不离其宗:
(1)找到可以回收的变量,定期回收 (2)回收前进行整理,有效整合碎片化内存 (3)内存回收
并行、并发、增量都是常用的“多线程”或“多进程”性能提升的机制,了解即可。