JavaScript工作原理:内存管理

堆栈

js和其他语言一样,是将数据存储在堆栈结构中的。

  • 栈(stack):结构类似于数据结构中的栈,先入后出。存储了函数参数值、所有变量名包括对象的引用、基础类型(String、Number、Boolean、Null、Undefined、Symbol)。

  • 堆(heap):结构类似于数据结构中的队列,先入先出,和操作系统中的堆(一种树)是完全的两码事。存储复杂对象。

    这里特别补充一点是池,又叫做常量池,顾名思义就是存储常量的地方,自从ES6引入const后,常量就被存储在池中。池是一块特殊的栈,结构和栈一样除了池中数据不可变之外。

js的内存空间中的存储如下图

栈中的运算比堆中快,为什么要将Object存储在堆中是因为对象可拓展,单独存储不会影响栈的效率。

操作系统分配

静态分配:从静态存储区域分配内存。程序编译的时候内存已经分配好了,并且在程序的整个运行期间都存在,如静态变量和全局变量。

自动分配:在栈中为局部变量分配内存的方法,栈中的内存可以随着代码块退出时的出栈操作被自动释放。例如在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数结束时这些存储单元自动被释放。

动态分配:从静态存储区域分配内存。程序编译的时候内存已经分配好了,并且在程序的整个运行期间都存在,如静态变量和全局变量。

栈在底堆在上,function的执行都是通过入栈出栈,执行的时候去堆取到数据推入执行栈,成员变量在堆,局部变量在栈,全局变量也在堆;引用类型的引用变量存储在栈中,指向于实际存储在堆中的实际对象。

node.js中V8垃圾回收算法

新生代与老生代内存

新生代:存活时间较短的对象,会被GC自动回收的对象及作用域,比如不被引用的对象及调用完毕的函数等。默认吗32M。基本只包括抽象语法树中使用了的变量会被放在新生代中。

老生代:存活时间较长或常驻内存的对象,比如闭包因为外部仍在引用内部作用域的变量而不会被自动回收,故会被放在常驻内存中,这种就属于在新生代中持续存活,所以被移到了老生代中,还有一些核心模块也会被存在老生代中,例如文件系统(fs)、加密模块(crypto)等。可以依靠node –max-old-space-size修改,最大1.7G,默认1.4G。

具体内部的分类如下图:

新生代的内存回收算法——Cheney算法

它将现有的空间分半,一个作为 To 空间,一个作为 From 空间,当开始垃圾回收时会检查 from 空间中存活的对象并赋复制入 To 空间中,而非存活就会被直接释放,完成复制后,两者职责互换,下一轮回收时重复操作。

算法效率很低,但占用空间大,但因为新生代本身内存限制不大,因此采用这样的算法对整个系统的优化是利大于弊的。

老生代的内存回收算法——标记清楚

当变量进度环境,将这个变量标记为“进入环境”,状态为“进入环境”的变量不会被释放,当变量离开环境,就标记为“离开环境”,并且进行回收。具体实现一般是翻转一个特定的位来实现这个标记。

被淘汰的内存回收算法——引用计数

引用计数在JavaScript内存回收中用的较少,现在只有KDE下的KJS引擎的游览器在采用,而最早是网景3游览器采用了这种方式。
引用计数的含义是跟踪记录每个值被引用的次数,当声明一个变量并将一个引用类型赋值给变量时,引用次数是1.如果值又赋给另外一个变量,引用次数加1,如果包含这个值引用的变量取得了其他值,引用次数减1,当引用次数为0,表示不能再访问到,即可回收。
值得注意的是,虽然基本没有游览器上JavaScript使用引用计数了,但ie9一下游览器很多对象不是原生JavaScript对象,比如DOM与BOM中的对象就是c++以COM(组件对象模型)实现的,而COM的垃圾回收采用的是引用计数策略,因此即使游览器采用标记清楚实现垃圾回收,但只要在游览器设计COM对象,就使用了引用计数。这会引起循环引用的问题。如;

var ele = document.getElementById('ele');
var myObj = new Object();
myObj.ele = ele;  //新对象一个属性引用了com对象
ele.someObj = myObj;  ///com对象一个属性引用了JavaScript对象

在上述情况下,即使将DOM从页面移除,其内存也不会被回收。为了避免出现,要在使用完毕后手工回收,赋值null来手工解除引用。
ie9以上的游览器上,BOM与DOM变成了JavaScript对象,不会再出现这个问题,但做兼容性处理的时候这种内存泄漏是要考虑的。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

😉😐😡😈🙂😯🙁🙄😛😳😮:mrgreen:😆💡😀👿😥😎😕