前面的话
js具有自动回收机制,它会定期的找出那些不在继续使用的变量,然后释放其内存;而c、c++语言必须手动释放内存,程序员负责内存管理。
回收方法
-
标记清除
标记清除是最常用的垃圾回收方式。 当变量进入执行环境时,就标记这个变量为
"进入环境"。
当变量离开环境时,则将其标记为"离开环境"
。被标记为离开环境的变量,在下一次垃圾回收启动时,就会被释放掉占用的空间。举个例子:
var m = 0 ,n = 4;// 把 m, n,add() 标记为进入环境 add(m ,n);// 把 a,b,c标记进入环境。 console.log(n);// add() 函数执行完之后,将a,b,c标记为离开环境,等待垃圾回收 function add(a, b) { a++; var c = a + b; return c; }
-
引用计数
跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1。如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为0的值所占用的内存。
缺点:无法解决循环引用:
function func() { let obj1 = {}; let obj2 = {}; obj1.a = obj2; // obj1 引用 obj2 obj2.a = obj1; // obj2 引用 obj1 }
当函数func执行结束之后,返回值undefined,所以整个函数以及内部的变量都应该被回收,但是,引用计数方法,obj1和obj2的引用次数都不为0,所以他们不会被回收。
要解决循环引用的问题,最好是在不使用它们的时候将它们手动设为空。上面的例子可以这样:
obj1 = null; obj2 = null;
哪些会引起内存泄漏
内存泄漏:不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
- 意外的全局变量:比如在函数内部,没用使用var声明的变量。
- 被遗忘的计时器:计时器没有被清除
- 闭包
- 没有被清理的DOM元素引用
内存泄漏的识别方法
在新版本的Chrome中的performance中查看
图中Heap对应的部分就是内存在周期性的回落。
如何避免泄漏
- 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收
- 避免死循环
- 避免创建过多的对象
总之:不用了的东西要及时归还。
V8引擎回收机制
内存申请:
- 64位系统:1.4GB
- 32位系统:700MB
在node环境中可以通过:process.memoryUsage()
来查看内存分配。
- rss: 所有内存占用
- heapTotal:v8引擎可以分配的最大堆内存
- heapUsed:v8引擎分配使用的堆内存
- external:v8管理c++对象绑定到JavaScript对象的内存
以上单位都是字节。
Node的整体架构
-
第一层: Node标准库,由js编写,是我们使用过程中直接能调用的API。在源码中的lib目录下。比如常用的http模块
-
第二层:Node bindings,这一层是js与底层c/c++能够沟通的关键,前者通过bindings调用后者,相互交换数据。
-
第三层:是支撑Node运行的关键,由c/c++实现。
1、v8是Google开发的JavaScript引擎,提供JavaScript运行环境,可以说 它就是Node.js的发动机。 2、libuv是专门为Node.js开发的一个封装库,提供跨平台,线程池、 事件池、异步I/O能力,是Node.js如此强大的关键。 3、C-ares提供了异步处理DNS相关的能力 4、http_parser、 OpenSSL、zlib等提供http解析,ssl、数据压缩等其他能力
v8垃圾回收策略
我们不可能只用一种回收策略来解决问题,这样效率会很低。所以,v8采用了分代回收
的策略,将内存分为两个带:新生代
和老生代
- 新生代中的对象:为存活时间较短的对象
- 老生代中的对象:为存活较长或常驻内存的对象
分代内存:
- 32位中,新生代内存大小16MB,老生代内存大小700MB
- 64位中,新生代内存大小32MB,老生代内存大小1.4GB
- 新生代平均分为两块相等的内存空间,叫做
semispace
,每块内存大小8MB(32位)或者16MB(64位)
新生代
新生代采用Scavenge
垃圾回收算法,在算法实现时主要采用Cheney
算法。
Cheney算法将新生代的内存分为两块semispace
,一块处于使用状态,称为From空间,一块使用闲置状态,称为To空间。
绘图说明:
上图中,一直将From和To交换,就是为了让活跃对象始终在From中,To始终保持闲置。
这种算法的缺点是:只能使用堆内存的一半。
由于Scavenge是典型的牺牲空间换取时间的算法,所以无法大规模的应用到所有的垃圾回收中。但我们可以看到,Scavenge非常适合应用在新生代中,因为新生代中对象的生命周期较短,恰恰适合这个算法。
当对象经过多次复制任然存活,它就会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中。采用新的算法进行管理。
对象从新生代移到老生代的过程叫作晋升。
晋升的条件:
- 如果一个对象是第二次经历从From空间复制到To空间,那么这个对象会被移动到老生代中。
- 当要从From空间复制一个对象到To空间时,如果To空间已经使用了超过25%,则这个对象直接晋升到老生代中。设置25%这个阈值的原因是当这次Scavenge回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。
老生代
由于老生代中的对象存活时间长,使用上面的方法将不在适用。老生代将使用Mark-Sweep
和Mark-Compact
相结合的方式进行垃圾回收。
1、Mark-Sweep:
Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。Mark-Sweep在标记阶段遍历堆内存中的所有对象,并标记活着的对象,在随后的清除阶段,只清除没有被标记的对象。
注意: js中的标记是标记所有的变量,清除掉被标记为离开状态的变量;而老生代中的标记使标记存活的变量,清除没有被标记的变量。
缺点: 从图中可以看出,在进行一次清除回收之后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。如果出现需要分配一个大内存的情况,由于剩余的碎片空间不足以完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。
2、Mark-Compact
为了解决Mark-Sweep的内存碎片问题,Mark-Compact就被提出来了。
Mark-Compact是标记整理的意思,会将标记存活对象以后,将活着的对象向内存空间的一端移动,移动完成后,直接清理掉边界外的内存。
3、两者结合
在V8的回收策略中,Mark-Sweep和Mark-Conpact两者是结合使用的。由于Mark-Conpact需要移动对象,所以它的执行速度不可能很快,在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代中晋升过来的对象进行分配时,才使用Mark-Compact。
参考文章: