目录
V8引擎如何回收垃圾
如何查看V8内存使用情况
内存优化实例
什么是V8引擎?
大家都知道,js其实就是运行在浏览器上的一种语言。而作为一个解释形脚本语言,它是在v8引擎上运行的,就好比java是在jvm上运行一样,这个大家有个简单的了解就好。
为什么我们要关注内存?
V8的内存分配
新生代内存空间 | 老生代内存空间 |
---|---|
(From) and (To) |
如上图所示,内存主要分为新生代内存空间和老生代内存空间,两者用的算法不同,而内存大小和操作系统有关,现在用户大多都是windows的系统,主要分为64位和32位。
64位操作系统下,新生代空间为64MB,老生代为1400MB。
32位操作系统下,新生代空间为16MB,老生代为700MB。
为什么v8引擎内存那么小?
其实是因为js的最初设计,只是在浏览器上运行,而浏览器上的js不持久,1.4G已经完全够用。
还有一个原因就是,js的回收机制,在它回收垃圾的过程中,会暂停所有代码的执行,一般来说,回收300M的内存,大约是0.5秒左右,而超过1.4G就会直接崩溃。如果把v8引擎的内存设置成3G或者4G甚至更大,那么大家想想,在内存全满的情况下,回收机制就会让浏览器卡顿5~6秒,这是大部分用户所无法接受的。
什么样的情况会造成内存占用?
var a = 123;
如上代码,在全局使用了一个变量a,绑定了数据123,这样就在内存中开辟了一块空间,变量多了后,就会造成内存的溢出。
var a = 123;
// 假设a已经被回收,只剩下b变量
var b = 456;
var c = 789;
如上代码,新生代算法仅为64M,分为from和to两块空间,各占32MB,这时候a变量已经使用完毕,仅剩下b和c变量存活,那么这时候就会把已经回收的a变量放在from处,还存活的b和c放在to里,然后再清空from里的所有变量,接着使用b和c的话,再放回from里,形成一个循环。这样做虽然把已经不大的内存一分为二,但是好处就是效率高,因为留在from里的都是要回收的变量。这也是典型的牺牲空间,换取时间的一种算法。
已回收 | 未回收 | 已回收 | 未回收 |
---|---|---|---|
a = 1001 | b = 1002 | c = 1003 | d = 1004 |
如上表格,假设内存中有这4块内存,地址是从1001到1004,绑定了4个变量abcd,其中a和c已经被回收了,那么被回收的变量,存储在老生代内存中,会被标记下来,之后需要再进行整理,才能够继续使用。如果不整理会出现什么情况呢?就是说如果这时候需要插入一个数组 a = [1, 2],那么会发现它无法插入。虽然当前回收了两个变量,有了两个空位,但是数组对内存的要求是需要一块连续的内存空间来存储数组,而表格中的内存空间明显不是连续的,所以无法插入数组。这也是老生代空间的一个缺陷,因为它需要整理,虽然内存上比新生代更大,但是效率上不如新生代内存空间。
新生代内存是如何晋升至老生代?
在使用内存的时候,v8引擎会默认先使用新生代,从from过渡到to,这时候会进行一个判断,看看to空间是否充足,如果to的空间不够的话,那么就会从新生代空间,移到老生代空间,这就是晋升过程。
除此之外还可以看到执行时间等等,这个大家可以自行研究。
知道这个方法后,我们就可以自己定义一个方法来查看内存使用情况, 在这个方法里,我把内存转换成了MB,来方便我们阅读。
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
首先js分为两大块作用域,分别是全局作用域和局部作用域,其中局部作用域中的变量,会随着函数执行完毕而被内存判定已经可以回收。代码如下:
function f() {
var a = '';
}
f();
如上,在f函数调用完毕之后,a变量就会被释放,而不会存活在内存中。但是如果出现这么一种情况:
function f() {
var a = '';
return a
}
var b = f();
如果函数有返回值,并且在全局定义了一个变量接收这个值,那么就算函数执行完毕,也会在内存中留下b这个变量。而全局变量,会始终在内存中存活,直到程序结束。
所以我们总结一下:
优化内存的技巧
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
var size = 20 * 1024 * 1024;
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
var arr6 = new Array(size);
var arr7 = new Array(size);
var arr8 = new Array(size);
var arr9 = new Array(size);
getRam();
我在全局中定义了9个大数组,然后来执行下看看结果:
大家可以看到,v8引擎这时候已经炸了,报了内存泄漏的错误。
接下来我减少一个大数组
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
var size = 20 * 1024 * 1024;
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
var arr6 = new Array(size);
var arr7 = new Array(size);
var arr8 = new Array(size);
//var arr9 = new Array(size);
getRam();
这里只有8个,我们再来执行下
8个大数组接近极限,但是还没有崩溃。由此大家可以得出,如果全局中不停的累加变量,终究会造成内存的泄漏。所以尽量不要定义全局变量,如果非要定义的话,记得及时回收,代码如下:
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
var size = 20 * 1024 * 1024;
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
var arr6 = new Array(size);
var arr7 = new Array(size);
var arr8 = new Array(size);
arr1 = undefined;
var arr9 = new Array(size);
getRam();
我在定义第九个数组之前,我手动释放掉了第一个大数组,然后再定义第九个数组,此时内存里等于只有8个大数组,就不会造成内存泄漏了。当然最好的办法还是定义一个函数自调用,让全局变量变成局部,用完后自动销毁,代码如下:
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
var size = 20 * 1024 * 1024;
(function(){
var arr1 = new Array(size);
var arr2 = new Array(size);
var arr3 = new Array(size);
var arr4 = new Array(size);
var arr5 = new Array(size);
var arr6 = new Array(size);
var arr7 = new Array(size);
var arr8 = new Array(size);
})()
var arr9 = new Array(size);
getRam();
滥用缓存
大内存操作
以上两点是造成内存泄漏的罪魁祸首。所以记得千万不要滥用缓存,不要把什么东西都放进全局变量里。举个滥用缓存的例子:
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
var size = 20 * 1024 * 1024;
var arr = [];
function add() {
arr.push(new Array(size));
}
add();
getRam();
假设你定义了一个全局变量数组,每次执行add函数的时候,都会往这个全局变量里,push进一个大数组,那么最多8次,第9次的时候,v8引擎就会崩溃了。那么我们怎么预防这种情况呢?我的建议就是加一把“锁”,用这把锁来控制这个全局变量,我们已知9个大数组就会使v8引擎崩溃,那么为了使它不超过临界点,我们可以在它超过4个大数组的时候,加上一把“锁”,代码如下:
function getRam() {
var mem = process.memoryUsage();
var format = function(bytes) {
return (bytes/1024/1024).toFixed(2) + 'MB';
}
console.log(
'Process: heapTotal ' + format(mem.heapTotal) +
' heapUsed ' + format(mem.heapUsed) +
' res ' + format(mem.rss)
);
}
var size = 20 * 1024 * 1024;
var arr = [];
function add() {
if (arr.length > 4) {
arr.shift();
}
arr.push(new Array(size));
}
add();
getRam();
大家看,我在add函数内加了一把锁,每次调用函数,在push进全局数组之前,做个判断,如果长度大于4,那么就把数组的第一个元素回收掉。这样一来,不管这个函数执行多少次,v8引擎都不会崩溃。