前端为什么要关注内存
- 防止占用内存过大,造成页面卡顿,甚至无响应
- Node.js 使用 V8 引擎,内存管理对于服务端至关重要,因为服务端的持久性,内存更容易积累造成内存溢出
js 垃圾回收机制
js 使用垃圾回收机制自动管理内存,这种方式的利弊都很明显。
- 优势: 可以大幅简化程序中都内存管理代码,减轻开发者的负担,同时也减少长时间运转造成的内存泄漏问题
- 劣势: 意味着开发者无法掌控内存管理,我们无法强迫其进行垃圾回收,进行管理
下面简单介绍一下 js 的几种垃圾回收策略:
引用计数
主要是IE8 以下的浏览器使用,现代浏览器都弃用了这种方式,这里只做简单介绍。
基本原理就是,记录跟踪每个值被引用的次数,被引用一次被引用次数就加一,被释放就减一,为零时,就释放改值所占内存。
标记清除
主流浏览器使用垃圾回收机制。
当变量进入环境(例如,在函 数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。
而当变量离开环境时,则将其 标记为“离开环境”。 可以使用任何方式来标记变量。比如,可以通过翻转某个特殊的位来记录一个变量何时进入环境, 或者使用一个“进入环境的”变量列表及一个“离开环境的”变量列表来跟踪哪个变量发生了变化。
function test(){
var a = 10; //被标记"进入环境"
var b = "hello"; //被标记"进入环境"
}
test(); // 执行完毕后之后,a和b又被标记"离开环境",被回收
说到底,如何标记变量其实并不重要,关键在于采取什么策略。 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(当然,可以使用任何标记方式)。
然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记 的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。
最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
环境可以理解为我们的作用域,但是全局作用域的变量只会在页面关闭才会销毁。
V8 内存控制
V8 的内存限制
在 Node 中只能使用部分内存,64位系统下约为1.4GB,32位系统下约为0.7GB,在这种限制下,将会导致 Node 无法操作大内存对象,比如将一个 2GB 的文件读入内存,即使物理内存有64 GB,也没有办法完成,这个时候我们可以使用 Buffer 类,来完成大内存文件的读取。
造成这个问题的主要原因: Node 基于 V8 构建,而 V8 的这套内存管理机制主要是在浏览器中使用,完全可以满足前端页面中的所有需求,但是在 Node 中却限制了开发者随心所欲使用大内存的想法。
V8 的垃圾回收机制
V8 的垃圾回收策略主要基于分代式垃圾回收机制。主要将内存分为新生代和老生代两代。
新生代空间(Young Generaion)
特点:
- 管理对象存活时间较短
- 占用空间比老生代空间小很多
- 垃圾回特别频繁
新生代空间的垃圾回收采用Scavenge 算法,其工作原理如下:
- 将新生代空间分为两个空间,称为semispace,处于使用状态的叫做 From 空间,处于闲置的叫 To 空间,当我们分配对象时,先是在 From 空间中进行分配。
- 开始垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,然后释放 From 空间中的内存。
- From 空间与 To 空间对换
从上面的过程我们可以看到,Scavenge 算法是典型的牺牲空间换取时间的算法。缺点是只能使用堆内存中的一半,优点是在时间效率上有优异的表现。
老生代空间( OldGeneraion)
在新生代空间中生命周期较长的对象会被复制到老生代空间中,这个过程叫晋升。对象晋升的条件主要有两个:
- 对象是否经历过一次 Scavenge 回收。 对象从 From 空间复制到 To 空间时,会检查它的内存地址来判断这个对象是否已经经历过一次 Scavenge 回收。如果经历过,就直接复制到老生代空间中,而不是 To 空间。
- To 空间的内存使用占比是否超过 To 空间的 25%。 对象从 From 空间复制到 To 空间时,发现 To 空间的内存占比已经超过限制。因为To 空间将会变成 From空间,为了不影响后续的内存分配,会直接晋升到老生代空间中。
对于老生代空间,由于存活对象占比较大,再采用Scavenge的方式会有两个问题:
- 存活对象比较多,复制存活对象的效率会很低
- 要拆分两个 semispace 空间,比较浪费
为此,老生代中主要采用标记清除(Mark-Sweep)和标记整理(Mark-Compact)相结合的方式进行垃圾回收,其工作原理如下:
- 在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。标记后的示意图如下:
黄色部分为标记活跃的对象,深灰色部分为标记死亡的对象。
标记清除(Mark-Sweep)最大的问题在于,清除标记死亡的对象后,内存不连续,这种碎片空间会对后续的内存分配造成问题。
- 为了解决内存碎片的问题,标记整理(Mark-Compact)被提出来。它会在标记完成后,将存活的对象移动到一端,然后释放存活对象这一端之外的空间。
增量标记(Incremental Marking)
在进行上面 V8垃圾回收操作的时候,需要将应用逻辑暂停,但是由于老生代空间很大,且存活对象很多,为了避免长时间的停顿,将原本一次性完成的操作改为增量标记,即拆分为许多小“步进”,没做完一次“步进”,让应用逻辑执行一会儿,交替执行,直到垃圾回收执行完成。
参考文章:
- 《Javascript 高级程序设计》
- 《深入浅出 Node.js》