Node.js学习笔记——GC

2019 / 12 / 02

V8

在node.js中javascript代码是通过v8来解释执行的,v8和node.js的关系就像jvm和java一样,所以node.js中的GC,其实就是v8的GC。

查看使用量

process.memoryUsage() 会返回当前的内存使用量,有四个字段,分别是

rss(resident set size) 常驻内存集,表示给该进程分配的内存大小。

heapTotal 堆中总共申请到的内存量

heapUsage 堆中目前用到的内存量

external v8引擎内部的C++对象用到的内存量

function format(size) {
return `${(size / 1024 / 1024).toFixed(2)}MB`
}

function printMemoryUsage() {
const memory = process.memoryUsage()

for (const key in memory) {
console.log(`${key}: ${format(memory[key])}`)
}
}

printMemoryUsage()

输出结果为

rss: 16.59MB
heapTotal: 4.02MB
heapUsed: 1.96MB
external: 0.62MB

如果创建两个很大的对象的话

function createBigArray() {
const arr = new Array(1024 * 1024 * 5)
return arr
}

const a = createBigArray()
const b = createBigArray()

printMemoryUsage()

创建两个很大的对象之后内存使用会变得很大

rss: 97.16MB
heapTotal: 84.77MB
heapUsed: 81.92MB
external: 0.62MB

内存大小

V8对内存占用进行了限制,64位机器的限制是1.4G,32位机器的限制是0.7G,所以对于非常大的数据操作的时候会有风险,可能会由于超出内存限制而进程退出。

垃圾回收的内存可以通过

--max-old-space-size=2048
--max-new-space-size=2048

这两个参数来控制老生代和新生代的空间大小。

内存给的越大不一定越好,即使给非常大的内存,v8进行一个1.5G的堆内存进行回收需要50ms以上的时间,而内存回收会造成线程的阻塞,所以这也是非常影响性能的。

垃圾回收

不断声明的数据会耗费大量的内存,需要及时的释放不再使用的数据的内存,V8会自动回收不再使用的数据所占的内存,而不需要手动的释放内存。那什么样的数据是无法被回收的呢?

一般情况下只有全局变量才无法被垃圾回收机制回收,这个全局变量指的是挂载到window或者global上的变量,而局部作用域中的变量在函数或者块执行完毕之后就会被销毁。在node中某个模块执行完毕的时候模块中声明的变量也会被销毁。所以node中的大多数的对象存活周期只是在一个函数作用域或者一个模块的作用域中(模块也是一个函数作用域)。

V8把内存堆分为新生代和老生代。

新生代采用一种Scavenge的复制算法,因为新生代的垃圾回收会非常频繁,一般刚刚创建的数据会放在新生代中,新生代的数据在触发一些机制后会被放到老生代里,一般情况下是一轮或者多轮都没有被回收,而且占用的比例较大,那么这个数据就会被放到老生代。

老生代的数据不能使用Scavenge复制算法,因为这个算法比较耗费空间,老生代一般都是存放那些又大又难以被回收的数据,所以它的更新策略和新生代也不一样。对于老生代V8采用了标记清除,标记整理和增量标记的方法来进行更新。

标记清除是在变量进入环境的时候标记为进入,出去的时候标记为出去,然后等自动被回收就好了。标记整理是对标记清除后的内存进行处理,让其变得连续,不会产生内存碎片造成内存浪费。增量标记是v8优化老生代的内存回收速度的方法。

内存泄漏

比较老的IE浏览器使用引用计数来判断一个变量是否已经不再使用,而循环引用会让标记一直无法变成0,所以浏览器就会认为内存一直再被占用,从而无法回收内存,在标记清除中不会发生这个问题。

全局变量是无法被回收的,下边的代码第一个执行会把变量挂到全局造成内存泄漏,第二个如果没有设置调用者也会把变量挂载到全局。所以实现的时候,使用严格模式,防止使用未声明的变量,函数调用的时候注意this的指向。尽量把代码写到块作用域或者函数作用域中也能保证内存被更好的回收

function foo2(arg) {
bar = "some text";
}

function foo2() {
this.bar = "potential accidental global";
}

DOM的引用处理不当也会造成内存泄漏,比如下边的代码在dom被移除之后仍然存在在对象elements的引用中就会造成无法被回收。解决这个问题最好的方法就是使用weakMap或者weakSet,这两个数据类型的引用不会被标记,所以dom移除了之后内存就能回收DOM的内存了。

const elements = {
image: document.getElementById('image')
}
elements.image.src = 'https://test.com/123.png'
document.body.removeChild(document.getElementById('image'))

另外提及的一个东西是尾调用,大概意思就是一个函数的最后一个操作是返回了另外一个函数。比如,尾调用有什么用呢,其实很简单,大概就是说函数调用函数会形成调用栈,而尾调用的话,这个栈就不会保留外层的栈了,当前前提是内部作用域没有引用外层的作用域的变量,也就是不是闭包。

function bar() {
// ...
}

function foo() {
return bar()
}

尾调用可以减少调用栈从而优化内存,在写递归的时候用尾调用优化可以提升性能。当然凡是可以用递归实现的,都能用循环实现,递归最好的优化方式还是用循环。

参考文章

JavaScript 内存管理 & 垃圾回收机制

浅谈V8引擎中的垃圾回收机制

嗨,请先 登录

加载中...
(๑>ω<๑) 又是元气满满的一天哟