内存回收
内存回收:全局变量在关闭页面之前一般不会回收,局部变量会在使用完毕之后自动回收,若本该回收的变量无法回收或未回收叫内存泄露
内存泄漏:内存泄漏有多种情况
-
非严格模式下变量忘记加var/let/const,导致隐式创建全局变量,挂载在window上,因为全局对象是根,不会被回收,严格模式下会报错
注意: 最外层的变量即使加了var/let/const也是全局变量,但是let/const创建的全局变量不会挂载到window上
此外,隐式的全局变量可通过delete删除,但var创建的全局变量不能用delete删除,let/const也不能用不过他们是因为根本不在window对象上
-
定时器未回收(setInterval没有clear)
-
闭包中引用了不再需要的变量或全局变量引用闭包内的数据
-
事件监听未移除
-
Map/Set强引用
JS的垃圾回收机制
内存分为栈内存和堆内存,栈内存放基本类型的值和引用类型的指针,堆内存放指针指向的对象(基本类型就直接放在栈内存因为不是对象),栈内存的变量在使用完后会由操作系统自动分配释放,堆内存由程序员释放或垃圾回收机制回收,垃圾回收机制分为引用计数和标记清除
-
引用计数:被引用一次记录次数加一,减少一个引用次数减一,次数为0的时候释放内存,但是可能会因为循环引用导致内存泄漏,比如两个对象a.other=b,b.other=a,在结束的时候a和b两个变量被释放但是b.other仍然指向对象a,即对象a的引用次数仍为1而不是0,b同理
-
标记清除:现在常用,能从根部(在JS中就是全局对象)到达的对象就是有用的,否则清除,这样就能避免内存泄漏
注意这里的能到达,是从栈内存上找根集,然后往堆内存上找,然后比如栈内存上一个变量存的是堆内存的一个map的地址,那从栈上这个变量找到堆内存这个map,这个map就是可达的,然后递归找,比如map中存的是另一个对象的引用那就能找到另一个对象,那这个对象就是可达的,最终From区标记完成后没被标记的就是不可达的
-
引用计数垃圾回收是在整个过程中的所以比较平缓,但因为需要不停的更新计数器,所以有持续的CPU开销,标记清除没有,并且由于垃圾回收是某个时段集中回收所以平时速度快,但回收时候会有较大卡顿
-
标记清除优化: 标记整理,垃圾回收完只会会出现很多碎片空间影响大对象分配空间,在标记之后,先把存活的对象挪到一边,再把另一边的清除
v8引擎的标记-清除优化
分层优化,JS堆内存分为新生代和老生代
新生代: 存放短期存活的对象(比如临时变量)
-
内存空间小,回收频率高
-
使用Scavenge算法,把新生代内存分为From区(使用中,新对象存在这里),To区(空闲,里面是空的)
- 遍历From区存活对象,复制到To区并整理内存
- 交换From/To区,释放原From区内存(相当于互换From/To)
- 若对象多次复制仍然存活(超过阈值),则晋升为老生代
老生代:
-
内存空间大,回收频率低
-
主要使用标记-清除+标记-整理,并优化为增量标记+并发回收(注意标记和回收是两个动作)
- 增量标记: 把整个标记过程拆分成多个小步骤,穿插再Js执行间隙完成,大幅度降低单次停顿
- 并发回收: 垃圾回收线程与JS执行线程并行运行,垃圾回收不影响主线程运行
- 标记是JS主线程执行的,回收是子线程做的
浏览器不会实时回收垃圾,回收时机:
- 新生代From区内存不足
- 老生代内存不足,达到阈值
- 浏览器空闲
- 页面卸载