从JS算法出发:如何优化v8引擎

目录

  • V8引擎如何回收垃圾

  • 如何查看V8内存使用情况

  • 内存优化实例

什么是V8引擎?

大家都知道,js其实就是运行在浏览器上的一种语言。而作为一个解释形脚本语言,它是在v8引擎上运行的,就好比java是在jvm上运行一样,这个大家有个简单的了解就好。

为什么我们要关注内存?

  • Node使用的也是v8,内存对于后端服务器性能至关重要
      首先node也是js,而且是运行于服务端的,大家都知道,作为服务器,需要持久连接,这时候内存就至关重要。如果内存没有及时释放的话,很容易造成内存溢出,导致服务器的奔溃。
  • 对前端浏览器的影响
      不知道大家有没有过一种经历,就是打开一个网页的时候,或者在浏览过程中,页面突然卡死,甚至出现崩溃的情况?这时候如果你打开任务管理器,就会发现浏览器的内存占用率太高,而导致的崩溃。显然这个体验对于用户来说是非常不友好的。

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的空间不够的话,那么就会从新生代空间,移到老生代空间,这就是晋升过程。

利用node来查看内存使用情况

  • 通过process.memoryUsage(); 方法
    我们用node执行process.memoryUsage();函数,可以得到返回对象结果如下:
    从JS算法出发:如何优化v8引擎_第1张图片
    其中,rss为v8引擎申请到的内存总空间,heapTotal就是一个堆内存,然后heapUsed就是我们已经使用的内存空间。而external,这个解释一下,由于node的底层是C++,所以使用node的话,会比浏览器上,多申请到一块而外的空间,这个空间实际上是C++申请来的。
    如果想在浏览器上查看内存使用情况的话,可以在浏览器打印窗口执行函数window.performance,得到结果:
    从JS算法出发:如何优化v8引擎_第2张图片
    大家看,其中的memory就是内存使用情况。
    jsHeapSizeLimit: 总内存
    totalJSHeapSize: 总的堆内存
    usedJSHeapSize:已经使用的

除此之外还可以看到执行时间等等,这个大家可以自行研究。

知道这个方法后,我们就可以自己定义一个方法来查看内存使用情况, 在这个方法里,我把内存转换成了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)
		);
}

来看看node里运行的结果
在这里插入图片描述

接下来说说,js中的变量,是如何判定可以回收的。

首先js分为两大块作用域,分别是全局作用域和局部作用域,其中局部作用域中的变量,会随着函数执行完毕而被内存判定已经可以回收。代码如下:

function f() {
	var a = '';
}
f();

如上,在f函数调用完毕之后,a变量就会被释放,而不会存活在内存中。但是如果出现这么一种情况:

function f() {
	var a = '';
	return a
}
var b = f();

如果函数有返回值,并且在全局定义了一个变量接收这个值,那么就算函数执行完毕,也会在内存中留下b这个变量。而全局变量,会始终在内存中存活,直到程序结束。

所以我们总结一下:

优化内存的技巧

  • 尽量不要定义全局变量
  • 全局变量记得销毁掉
  • 用匿名函数自调用变全局为局部
  • 尽量避免使用闭包

接着我来测试下node中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();

我在全局中定义了9个大数组,然后来执行下看看结果:

从JS算法出发:如何优化v8引擎_第3张图片
大家可以看到,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引擎都不会崩溃。

以上就是我总结的对v8引擎的理解和优化的一些方法,供大家参考。如果大家觉得有用的话,请给我点个赞吧!

你可能感兴趣的:(前端技术,js技术,vue技术)