背景:为什么需要垃圾回收?
一些专业术语词:
– 新生代/老生代
– 标记清除/标记整理
– 全停顿
JS内存管理
当创建变量时,系统会自动给对象分配对应的内存。当系统发现这些变量不被使用的时候,会自动释放这些变量的内存,作为开发者不用过多关心这些内存问题。
JS中的数据类型分为两类
– 简单类型:内存是保存在栈空间中,按值访问;
– 引用类型:内存保存在堆空间中,栈内存中存放地址指向堆内存中的对象,按引用访问;
栈的内存空间,大小固定,由操作系统自动分配和自动释放。而堆空间中的内存,大小不固定,内存无法进行自动释放,就需要JS引擎手动释放这些内存。所以当我们的代码书写不规范时会使JS引擎的垃圾回收无法正确对内存进行释放,浏览器占用的内存不断增加,导致性能下降,内存泄漏等问题。
V8中的垃圾回收(主要针对堆)
V8将堆分为两类
– 新生代:存放生存时间短的对象,使用副垃圾回收器
– 老生代:存放生存时间久的对象,使用主垃圾回收器
新生代垃圾回收器
主要使用Scavenge算法进行垃圾回收(广度优先的复制算法)
工作流程
新生代堆又拆为两部分,from空间和to空间。新分配的对象放入from空间中,当from空间被占满,会进行GC。算法会标记活动对象和非活动对象,将from空间中活动对象复制到to空间中,并进行排序,释放from空间中的非活动对象内存,from空间和to空间角色互换。
新生代中的对象还分两种等级:初级(nursery)代,中级(intermediate)代;第一次分配内存时是初级代,如果在第一轮垃圾回收后幸存下来会升级到中级代,如果后一轮垃圾回收它还幸存下来就会从中级代移动到老生代,这个过程叫
晋升。
老生代垃圾回收(两种策略)
- Mark-sweep(标记清除):标记活动对象和非活动对象后直接清除非活动对象。但这会产生很多内存碎片,这会导致之后要分配一个大对象无法进行分配。
- Mark-compact(标记整理):在标记清除的基础上将活动对象往前移动,清理掉边界外的内存,解决内存碎片的问题。
> 内存碎片:空闲内存小且不连续,分配算法无法分配使这些内存导致无法使用。
全停顿
执行垃圾回收算法前需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑。对于新生代来说,内存较小,全停顿的影响不是很大。对于老生代来说,内存较大,如果遍历太多,会明显感到页面卡顿,体验极差。
? 所以V8针对老生代对这种情况进行了优化—— Orinoco~
1. 增量标记:把大暂停换成多个小暂停,每个暂停只持续最多几十毫秒,穿插在JS应用逻辑之间执行
2. 并行回收:主线程和辅助线程同时执行GC任务,辅助线程分担主线程的GC工作,减少垃圾回收所耗费的时间
3. 并发回收:主线程一直执行JS,辅助线程执行垃圾回收