18 | 垃圾回收:释放内存,提升浏览器页面性能

13 | 垃圾回收:垃圾数据是如何自动回收的?

JavaScript 的内存管理

简而言之,基本就是说明以下两点。

  • 基本类型:这些类型在内存中会占据固定的内存空间,它们的值都保存在栈空间中,直接可以通过值来访问这些;
  • 引用类型:由于引用类型值大小不固定(比如上面的对象可以添加属性等),栈内存中存放地址指向堆内存中的对象,是通过引用来访问的。

因此总结来说:栈内存中的基本类型,可以通过操作系统直接处理;而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理

Chrome 内存回收机制

在 Chrome 浏览器中,JavaScript 的 V8 引擎被限制了内存的使用,根据不同的操作系统(操作系统有 64 位和 32 位的)内存大小会不同,大的可以到 1.4G 的空间,小的只能到 0.7G 的空间。
为什么要去限制内存使用呢?大致是两个原因:V8 最开始是为浏览器而设计的引擎,早些年由于 Web 应用都比较简单,其实并未考虑占据过多的内存空间;另外又由于被 V8 的垃圾回收机制所限制,比如清理大量的内存时会耗费很多时间,这样会引起 JavaScript 执行的线程被挂起,会影响当前执行的页面应用的性能。

新生代内存回收

image.png
图中左边部分表示正在使用的内存空间,右边是目前闲置的内存空间。当浏览器开始进行内存的垃圾回收时,JavaScript 的 V8 引擎会将左边的对象检查一遍。如果引擎检测是存活对象,那么会复制到右边的内存空间去;如果不是存活的对象,则直接进行系统回收。当所有左边的内存里的对象没有了的时候,等再有新生代的对象产生时,上面的部分左右对调,这样来循环处理。
image.png
Scavenge
image.png

老生代内存回收

新生代中的变量如果经过回收之后依然一直存在,那么就会被放入到老生代内存中。
Mark-Sweep(标记清除) 和 Mark-Compact(标记整理)的策略

Mark-Sweep(标记清除)

通过名字你就可以理解,标记清除分为两个阶段:标记阶段和清除阶段。
首先它会遍历堆上的所有的对象,分别对它们打上标记;然后在代码执行过程结束之后,对使用过的变量取消标记。那么没取消标记的就是没有使用过的变量,因此在清除阶段,就会把还有标记的进行整体清除,从而释放内存空间。
但是其实通过标记清除之后,还是会出现上面图中的内存碎片的问题。内存碎片多了之后,如果要新来一个较大的内存对象需要存储,会造成影响。对于通过标记清除产生的内存碎片,还是需要通过另外一种方式进行解决,因此这里就不得不提到标记整理策略(Mark-Compact)了。

Mark-Compact(标记整理)

经过标记清除策略调整之后,老生代的内存中因此产生了很多内存碎片,若不清理这些内存碎片,之后会对存储造成影响
和标记清除来对比来看,标记整理添加了活动对象整理阶段,处理过程中会将所有的活动对象往一端靠拢,整体移动完成后,直接清理掉边界外的内存。其操作效果如下图所示。
image.png可以看到,老生代内存的管理方式和新生代的内存管理方式区别还是比较大的。Scavenge 算法比较适合内存较小的情况处理;而对于老生代内存较大、变量较多的时候,还是需要采用“标记-清除”结合“标记-整理”这样的方式处理内存问题,并尽量避免内存碎片的产生。

内存泄漏与优化

内存泄漏是指 JavaScript 中,已经分配堆内存地址的对象由于长时间未释放或者无法释放,造成了长期占用内存,使内存浪费,最终会导致运行的应用响应速度变慢以及最终崩溃的情况。
内存泄漏的场景:

  1. 过多的缓存未释放;
  2. 闭包太多未释放;
  3. 定时器或者回调太多未释放;
  4. 太多无效的 DOM 未释放;
  5. 全局变量太多未被发现。

    优化

  6. 减少不必要的全局变量,使用严格模式避免意外创建全局变量

  7. 在你使用完数据后,及时解除引用(闭包中的变量,DOM 引用,定时器清除)
  8. 组织好你的代码逻辑,避免死循环等造成浏览器卡顿、崩溃的问题。例如,对于一些比较占用内存的对象提供手工释放内存的方法