掘金链接
往往我们意识里觉得内存管理是后端的事情,单并非如此,前端也需要关注内存使用情况。前端为什么也需要关注内存呢?一方面防止内存占用过大导致页面卡顿,甚至没有响应;另一方面Node.js使用V8引擎,内存管理对于服务端至关重要,因为服务端的持久性,内存更容易积累造成内存溢出。
js 垃圾回收机制
js是通过垃圾回收机制来自动管理内存的,这种方式有自己的利弊:
- 好处:大幅简化程序中都内存管理代码,减轻开发者的负担;同时减少长时间运转造成的内存泄漏问题
- 坏处:开发者无法掌控内存管理,我们无法强迫其进行垃圾回收,进行管理
我们来一起了解js 的几种简单的垃圾回收策略:
1. 引用计数
目前主要是IE8 以下的浏览器使用,现代浏览器都弃用了这种方式。基本原理:记录跟踪每个值被引用的次数,被引用一次,次数就加一;被释放就减一;为零时就释放当前值所占内存。
2. 标记清除
当前国内外主流浏览器都是使用标记清除作为垃圾回收策略。其策略可以简单的理解为:
- 当变量进入环境(例如,在函 数中声明一个变量)时,将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
- 当变量离开环境时,则将其标记为“离开环境”。
我们可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境, 或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。如下例子简单的反映了标记清除的定义:
function fn(){
let a = 'hello'; // 被标记"进入环境"
let b = 'world'; // 被标记"进入环境"
}
fn(); // 执行完毕后之后,a和b又被标记"离开环境",被回收
整个流程其实可以有如下的概括:
- 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
- 它会去掉环境中的变量,以及被环境中的变量引用的变量的标记。
- 此后再被加上标记的变量将被视为准备删除的变量(因为环境中的变量已经无法访问到这些变量了)。
- 最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
V8 内存控制
Node 基于 V8 构建,而 V8 的内存管理机制主要是在浏览器中使用,完全可以满足前端页面中的所有需求,但是在 Node 中却限制了开发者随心所欲使用大内存的想法。Node 中只能使用部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB,在这种限制下,将会导致 Node 无法操作大内存对象,比如将一个 2GB 的文件读入内存,即使物理内存有64 GB,也没有办法完成,这个时候我们可以使用 Buffer 类,来完成大内存文件的读取。V8 的垃圾回收策略主要基于一种分代式的机制,将内存空间分为新生代和老生代。
1. 新生代内存
新生代内存有如下特点:
- 管理对象的存活时间较短
- 占用空间比老生代空间小很多
- 垃圾回收频繁
新生代内存的垃圾回收采用的是 Scavenge 算法,在Scavenge的具体实现中,主要采用了Cheney算法,其工作原理是典型的牺牲空间换取时间的算法: - 新生代空间分为两个空间,称为semispace。处于使用状态的叫做 From 空间,处于闲置的叫 To 空间,当我们分配对象时,先是在 From 空间中进行分配。
- 开始垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,然后释放 From 空间中的内存。
- 完成复制之后,From 空间与 To 空间对换
2. 老生代内存
新生代内存空间中,如果有生命周期较长的对象就会被复制到老生待内存空间中,能够被复制到老生代内存空间的对象需要满足以下条件:
- 对象是否经历过一次 Scavenge 回收。如果经历过,就直接复制到老生代空间中,而不是 To 空间。
- To 空间的内存使用占比是否超过 To 空间的 25%。 对象从 From 空间复制到 To 空间时,发现 To 空间的内存占比已经超过限制。因为To 空间将会变成 From空间,为了不影响后续的内存分配,会直接晋升到老生代空间中。
对于老生代内存的回收,主要采用标记清除(Mark-Sweep)和标记整理(Mark-Compact)相结合的方式进行垃圾回收,其工作原理如下: - 在标记阶段遍历所有对象,并标记活着的对象;在随后的清除阶段中,只清除没有被标记的对象。
- 为了解决内存碎片的问题,标记整理在标记完成后,将存活的对象移动到一端,然后释放存活对象这一端之外的空间。
3. 增量标记
为了避免出现 JavaScript 应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为“全停顿"。长时间的"全停顿"垃圾回收会让用户感受到明显的卡顿,带来体验的影响。
为了降低垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记(incremental marking),也就是拆分为许多小“步进”,每做完一“步进” 就让 JavaScript 应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直到标记阶段完成。
参考文章:
《深入浅出 Node.js》
http://www.ruanyifeng.com/blog/2017/04/memory-leak.html