v8引擎如何回收内存

「JavaScript基础原理」- v8引擎如何回收内存

Posted by wendy on January 9, 2020
  • V8引擎如何回收垃圾
  • 如何查看v8内存使用情况
  • 内存优化实例

V8引擎如何回收垃圾

思考:打开页面,页面卡顿,卡死,提示页面无响应 ———— 内存过大

为什么我们要关注内存? (如何处理变量)
  • 防止页面占用内存过大, 引起客户端卡顿,甚至无响应
  • Node使用的也是V8, 内存对于后端服务的性能至关重要,因为服务的持久性,后端很容易造成内存泄漏
V8的垃圾回收机制-内存分配

在V8中,主要将内存分为新生代和老生代两代。

  • 新生代中的对象为存活时间较短的对象
  • 老生代中的对象,为存活时间较长或常驻内存的对象.
1
2
3
4
5
6
---------------------------------------------
|         |                                   |
| 新生代的 |      老生代的内存空间                |
| 内存空间 |                                   |
|         |                                   |
---------------------------------------------
内存大小
  • 和操作系统有关64位为1.4G,32位为0.7G
  • 64位下新生代的空间是64MB,老生代为1400MB
  • 32位下新生代的空间是16MB,老生代为700MB

为什么不把内存设置大些呢?

  • 1 前端的特点是不持久化,执行一遍就全部会回收了(js设计之处是为了浏览器),1.4G完全足够用了。
  • 2 js回收内存的时候,会暂停执行(回收一次100mb,大概需要6ms, 太大回收会卡顿)

垃圾回收算法:

  • 新生代回收是复制(from、to)—— 新生代放一些临时变量
  • 老生代标记删除回收

总结:
新生代存放一些新产生的变量,存在比较小,存在时间比较短的变量。 分成了From空间和To空间,刚开始新生代的变量,放到from空间里,当我们满足一定条件,新生代要进行回收了,活着的(还有用的)变量复制到To空间,然后清空From空间,下次再发生回收, From空间和To空间的角色发生互。新生代为什么要用怎么设置,牺牲空间换取时间,节省了时间,有一半空间用不上。 老生代是新生代变量已符合老生代,才放到老生代。标记删除回收

Scavenge 算法(新生代的垃圾回收)

在分代的基础上,新生代中的对象主要通过Scavenge算法进行垃圾回收.在Scavenge算法的具体实现中,主要采用Cheney算法.Cheney算法是一种采用复制方式实现的垃圾回收.它将堆内存一分为二,每一部分空间称为semispace. 只有一个处于使用中,另一个处于闲置状态

  • From空间: 处于使用状态的semispace空间, 当开始进行垃圾回收时, 会检查From空间中的存活对象,
  • To空间: 处于闲置状态的空间, 将From空间存活对象被复制到To空间, 而非存活空间对象占用的空间将被释放
  • 完成复制后, From空间和To空间的角色发生互换

简而言之, 在垃圾回收的过程中,就是通过将存活对象在两个semispace空间之间进行复制. Scavenge中之复制活着的对象
Scavenge算法是典型的牺牲空间换取时间的算法, 非常适合在新生代中, 因为新生代中对象的生命周期较短。

1
2
3
4
5
6
7
---------------------------------------------------------------
|        |        |                                           |
| semi   | semi   |                                           |
| space  | space  |          老生代空间                         |
| (From) | (To)   |                                           |
|        |        |                                           |
---------------------------------------------------------------

对象晋升(晋升到老生代中)的条件主要有两个:

  • 对象是否经历Scavenge回收(对象从From空间中复制到To空间时,检测内存地址判断是否经历Scavenge回收。是则复制到老生代空间中)
  • To空间的内存占用比超过限制(没有经历Scavenge回收, To空间已经使用超过了25%(8M), 则这个对象直接晋升到老生代中)

总结流程:

  • semi space(From) => 是否经历Scavenge回收 => 是则晋升老生代(第二次回收,还存活),否则到semi space(To)
  • semi space(From) => To空间是否已经使用了25% => 是则晋升老生代,否则到semi space(To)

设置25%这个限制值的原因是当次Scavenge回收完成后,这个To空间将变成From空间, 接下来的内存分配将在这个空间进行.如果占比过高会影响后来的内存分配.(为什么不实时回收,回收内存会暂停执行,回收一次100mb,需要6ms)

思考: 对象晋升后,将会在老生代空间作为存活周期较长的对象对待,接受新的回收算法处理.

Mark-Sweep & Mark-Compact(老生代垃圾回收)

Mark-Sweep标记清除,它分为标记和清除两个阶段。Mark-Sweep在标记阶段遍历堆中所有对象.并标记活着的对象,在随后的清除阶段中,之清除没有被标记的对象.
缺陷:内存空间会出现不连续的状态(内存碎片会对后续的内存分配造成问题)

Mark-Compact标记整理, 是在Mark-Sweep基础上演变而来.它们的差别在于对象标记死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存

为什么要整理?
内存碎片会对后续的内存分配造成问题。比如数组必须是连续内存空间

可以看出,Scavenge中之复制活着的对象,而Mark-Sweep只清理死亡对象. 活对象在新生代中只占较小部分,死对象在老生代中只占较小部分,这是两种回收方式能高效处理的原因.

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

内存查看
  • 浏览器:window.performance;
  • node-process.memoryUsage();
1
2
3
4
5
6
7
8
9
10
11
// 获取内存的大小的方法
function getme(){
  var mem = process.memoryUsage();
  var format = function(bytes){
    return (bytes/1024/1024).toFixed(2)+'MB';
  }
  // 总的内存,和使用内存
  console.log("heapTotal:" + format(mem.heapTotal) +
   'heapUsed:' + format(mem.heapUsed));
}
getme();

扩展知识:node是c++编写的, 可以扩展c++内存。

变量:全局变量和局部变量
  • 内存主要就是存储变量等数据的
  • 局部变量当程序执行结束,且没有引用的时候就会随着消失
  • 全局对象会始终存活到程序运行结束

扩展:闭包会不会消失,看使用,只是进行闭包是会消失,如果被引用则不会消失。闭包不是具体的写法,是一种思想。让我的变量,通过指定的方式,外部可以访问。这就是闭包。

1
2
3
4
5
6
function a(){
  var b = 123;
}
a(); // a执行完。b在外部没有引用,b局部变量伴随a的作用域一起回收

var a = 123; // 整个程序完了,才会结束回收

用node运行以下代码,内存不够进行变量回收(发现都是全局变量,无法回收)=> 内存不够,导致内存溢出,中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var size = 20 * 1024 * 1024;
var arr = 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);
var arr10 = new Array(size);
var arr11 = new Array(size);
var arr12 = new Array(size); 
// 到这里内存不够了
var arr13 = new Array(size); 
console.log(1);

局部会回收,只是说可以回收,并不是用完就回收。 运行的时候卡了一下,因为这里内存接近满的时候,进行回收会暂停执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var size = 20 * 1024 * 1024;
var a = []
function b(){
  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);
}
b(); // 标记b方法里变量可以被回收了
for(var i = 0; i < 13; i++){
  a.push(new Array(size));
  getme();
}

如何注意内存的使用

一、优化内存的技巧
  • 尽量不要定义全局变量
  • 全局变量记得及时销毁掉
  • 用匿名自执行函数把全局变为局部
  • 尽量避免闭包(是错的,应该是尽量避免闭包引用)

全局变量记得及时销毁掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 不推荐在开发时写delete && delete在严格模式下有bug
var size = 20 * 1024 * 1024;
var arr1 = new Array(size);
arr1 = undefined; // 及时回收 undefined | null 
var arr2 = new Array(size);
arr2 = undefined;
var arr3 = new Array(size);
arr3 = null;
var arr4 = new Array(size);
arr4 = null;
var arr5 = new Array(size);
arr5 = null;
var arr6 = new Array(size);
var arr7 = new Array(size);
var arr8 = new Array(size);
var arr9 = new Array(size);
var arr10 = new Array(size);
var arr11 = new Array(size);
var arr12 = new Array(size); 
// 到这里内存不够了
var arr13 = new Array(size); 
var arr14 = new Array(size); 
var arr15 = new Array(size); 
console.log(1);

扩展:null表示没有对象,即该处不应该有值。 undefined表示缺少值,即此处应该有值,但没有定义。null是一个保留字, undefined其实是一个变量。 比如null = 123; 会报错。但undefined不会报错

用匿名子执行函数把全局变为局部

1
2
3
4
// 比如jquery等
(function(){

})();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function a(){
  var size = 20 * 1024 * 1024;
  var arr1 = new Array(size);
  // 局部变量,return。被外部引用,又不释放。导致内存溢出
  return arr1;
}
// 闭包,被外部引用
var b1 = a();
var b2 = a();
var b3 = a();
var b4 = a();
var b5 = a();
var b6 = a();
var b7 = a();
var b8 = a();
var b9 = a();
var b10 = a();
var b11 = a();
var b12 = a();
// 内存溢出
var b13 = a();
防止内存泄漏
  • 不要滥用缓存,尽量不要用v8来缓存
  • 大内存量操作

缓存,全局变量a超出内存大小限制,内存泄漏。尽量不要用v8来缓存。
解决方案:

  • 方案1. node用redis缓存;
  • 方案2. 给缓存加个锁限制
1
2
3
4
5
6
7
8
9
10
var size = 20 * 1024 * 1024;
// 缓存通常都是在全局
var a = []; // 缓存
for(var i = 0; i < 16; i++){
  // 给缓存加个限制
  if(a.length > 4){
    a.shift();
  }
  a.push(new Array(size));
}

尤其是node服务,不断访问,访问数量很多,会很容易造成内存泄漏。如果用node服务,全局变量,只要服务开着,全局变量不会被回收

大内存量操作 —— node
比如:主要在node操作文件, fs.readFile这个api是一次性读取文件到buffer

1
2
fs.readFile(); // 大文件不可取
createReadStream().pipe(write) // 大文件

大内存量操作 —— 浏览器
比如:大文件上传,可以用切片上传。file blog slice

1
2
3
// 就像字符串分割
file.slice(0, 1000);
file.slice(1000, 2000);

收尾:为什么我们要了解底层。如果一个东西谁都会,那个这个东西就会变得不值钱;了解底层才能不是一个简单搬砖工;除了底层外,视野,技术的全面性也是格外重要。
底层类, 一些稀奇古怪的js题,或者直接某个底层技巧(JavaScript异步机制、JavaScript回收机制,JavaScript变量机制)
源码类, Vue(比如axios、vuex、vue-route源码)、React
项目,项目里面做了什么事情,或者介绍一些项目。(一些操作,同构、移动解决方案、组件库、架构、 一些优化操作(比如管理keep-alive, 如何分模块管理页面、缓存操作))