18 | 垃圾回收:释放内存,提升浏览器页面性能
JavaScript 的内存管理
简而言之,基本就是说明以下两点。
- 基本类型:这些类型在内存中会占据固定的内存空间,它们的值都保存在栈空间中,直接可以通过值来访问这些;
- 引用类型:由于引用类型值大小不固定(比如上面的对象可以添加属性等),栈内存中存放地址指向堆内存中的对象,是通过引用来访问的。
因此总结来说:栈内存中的基本类型,可以通过操作系统直接处理;而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理。
Chrome 内存回收机制
在 Chrome 浏览器中,JavaScript 的 V8 引擎被限制了内存的使用,根据不同的操作系统(操作系统有 64 位和 32 位的)内存大小会不同,大的可以到 1.4G 的空间,小的只能到 0.7G 的空间。
为什么要去限制内存使用呢?大致是两个原因:V8 最开始是为浏览器而设计的引擎,早些年由于 Web 应用都比较简单,其实并未考虑占据过多的内存空间;另外又由于被 V8 的垃圾回收机制所限制,比如清理大量的内存时会耗费很多时间,这样会引起 JavaScript 执行的线程被挂起,会影响当前执行的页面应用的性能。
新生代内存回收
图中左边部分表示正在使用的内存空间,右边是目前闲置的内存空间。当浏览器开始进行内存的垃圾回收时,JavaScript 的 V8 引擎会将左边的对象检查一遍。如果引擎检测是存活对象,那么会复制到右边的内存空间去;如果不是存活的对象,则直接进行系统回收。当所有左边的内存里的对象没有了的时候,等再有新生代的对象产生时,上面的部分左右对调,这样来循环处理。
Scavenge
老生代内存回收
新生代中的变量如果经过回收之后依然一直存在,那么就会被放入到老生代内存中。
Mark-Sweep(标记清除) 和 Mark-Compact(标记整理)的策略
Mark-Sweep(标记清除)
通过名字你就可以理解,标记清除分为两个阶段:标记阶段和清除阶段。
首先它会遍历堆上的所有的对象,分别对它们打上标记;然后在代码执行过程结束之后,对使用过的变量取消标记。那么没取消标记的就是没有使用过的变量,因此在清除阶段,就会把还有标记的进行整体清除,从而释放内存空间。
但是其实通过标记清除之后,还是会出现上面图中的内存碎片的问题。内存碎片多了之后,如果要新来一个较大的内存对象需要存储,会造成影响。对于通过标记清除产生的内存碎片,还是需要通过另外一种方式进行解决,因此这里就不得不提到标记整理策略(Mark-Compact)了。
Mark-Compact(标记整理)
经过标记清除策略调整之后,老生代的内存中因此产生了很多内存碎片,若不清理这些内存碎片,之后会对存储造成影响
和标记清除来对比来看,标记整理添加了活动对象整理阶段,处理过程中会将所有的活动对象往一端靠拢,整体移动完成后,直接清理掉边界外的内存。其操作效果如下图所示。
可以看到,老生代内存的管理方式和新生代的内存管理方式区别还是比较大的。Scavenge 算法比较适合内存较小的情况处理;而对于老生代内存较大、变量较多的时候,还是需要采用“标记-清除”结合“标记-整理”这样的方式处理内存问题,并尽量避免内存碎片的产生。
内存泄漏与优化
内存泄漏是指 JavaScript 中,已经分配堆内存地址的对象由于长时间未释放或者无法释放,造成了长期占用内存,使内存浪费,最终会导致运行的应用响应速度变慢以及最终崩溃的情况。
内存泄漏的场景: