轻松掌握V8引擎是如何回收内存的奥秘

作者: 贺鹏飞 分类: 浏览器 发布时间: 2021-01-16 12:46

一、前言

为什么要关注内存:防止页面占用内存过大,引起客户端卡顿,甚至无响应。Node使用的也是V8,内存对于后端服务的性能至关重要。因为服务的持久性,后端更容易造成内存的溢出。

V8的垃圾回收机制:JavaScript使用垃圾回收机制来自动管理内存。垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带来的内存泄露问题。但使用了垃圾回收即意味着程序员将无法掌控内存。ECMAScript没有暴露任何垃圾回收器的接口。我们无法强迫其进 行垃圾回收,更无法干预内存管理。

内存管理问题:在浏览器中,Chrome V8引擎实例的生命周期不会很长(谁没事一个页面开着几天几个月不关),而且运行在用户的机器上。如果不幸发生内存泄露等问题,仅仅会影响到一个终端用户。且无论这个V8实例占用了多少内存,最终在关闭页面时内存都会被释放,几乎没有太多管理的必要(当然并不代表一些大型Web应用不需 要管理内存)。但如果使用Node作为服务器,就需要关注内存问题了,一旦内存发生泄漏,久而久之整个服务将会瘫痪(服务器不会频繁的重启)。

二、V8引擎是如何分配内存的

V8实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。

1、内存大小限制

内存大小和操作系统有关,64位为1.4G,32位为0.7G;64位下新生代的空间为64M,老生代为1400M;32位下新生代为16M,老生代为700M。为什么是1.4G呢?1、js最初设计是在浏览器上跑的,浏览器上的js不持久,1.4G安全够用,2、js回收垃圾的时候,会暂停所有代码的执行,如果内存过大,回收时占用的时间会更多,300mb 回收0.5秒。为什么新生代内存空间划分成两块呢(一个from,一个to)?轻松掌握V8引擎是如何回收内存的奥秘

新生代老生代内存划分

先说一下垃圾回收算法:新生代回收算法简单的说就是复制:新生代内存空间,会标记活着的变量,典型的牺牲空间获取时间的方式,该内存空间中主要存一些新的变量、存活时间较短的变量,会频繁的进行垃圾回收。老生代回收算法就是标记删除整理:老生代标记死了的变量,需要整理磁盘碎片,新生代不需要整理,效率更高。接下来详细介绍一下新生代算法和老生代算法。

2、新生代算法

新生代对象主要通过Scavenge算法进行垃圾回收,在Scavenge的具体实现中,主要采用了Cheney算法。Cheney算法是一种采用复制的方式实现的垃圾回收算法,它将堆内存一分为二,每一部分空间称为semispace。在这两个semispace空间中。只有一个处于使用中另一个处于闲置状态,处于使用状态的空间称为From空间,处于闲置状态的称为To空间。当我们分配对象时,先是在From空间中进行分配,当开始进行垃圾回收时,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,而非存活对象将被释放。完成复制后,From空间和To空间的角色发生交换。Scavenge算法的缺点:只能使用堆内存的一半,但由于新生代中对象的生命周期较短所以很适合这个算法。当一个对象经过多次复制依然存活时(检查内存地址),它将被认为是生命周期较长的对象,所以随后会被移动到老生代中。

3、老生代算法

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。在讲算法前,先来说下什么情况下对象会出现在老生代空间中:1、新生代中的对象是否已经经历过一次Scavenge算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。2、To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。老生代中的空间很复杂,有如下几个空间:

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,       // 不变的对象空间
  NEW_SPACE,      // 新生代用于 GC 复制算法的空间
  OLD_SPACE,      // 老生代常驻对象空间
  CODE_SPACE,     // 老生代代码对象空间
  MAP_SPACE,      // 老生代 map 对象
  LO_SPACE,       // 老生代大空间对象
  NEW_LO_SPACE,   // 新生代大空间对象
  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

Mark-Sweep是标记清除的意思,它分为标记和清除两个阶段。Mark-Sweep在标记阶段遍历堆中所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有标记的对象。Scavenge中只复制活着的对象,而Mark-Sweep只清除死亡对象。Mark-Sweep的问题: 在进行一次标记清除回收站后,内存空间会出现不连续的状态。当分配一个大对象时,所有的碎片空间都无法完成此次分配,就会提前出发垃圾回收,而这次垃圾回收是不必要的。Mark-Compact:当对象被标记为死亡对象后,在整理的过程中,将活着的对象往一端移动,移动完成后直接清理掉外界外的内存。Incremental Marking为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致情况,垃圾回收的3种基本算法都需要将应用逻辑停下来,待执行完成垃圾回收后再恢复执行应用逻辑,这种行为被成为“停顿”。为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记,也就是拆分为多个小“步”,没完成一”步“就让JavaScript应用逻辑执行一小会,垃圾回收与应用逻辑交替进行直到标记阶段完成。在老生代中,以下情况会先启动标记清除算法:1、某一个空间没有分块的时候;2、空间中被对象超过一定限制;3、空间不能保证新生代中的对象移动到老生代中。Mark-Sweep是将需要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间,Mark Compact的思想有点像新生代垃圾回收时采取的 Cheney 算法:将存活的对象移动到一边,将需要被回收的对象移动到另一边,然后对需要被回收的对象区域进行整体的垃圾回收。在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。

三、v8是如何处理变量的

1、浏览器查看内存使用情况

F12调试工具查看;window.performance轻松掌握V8引擎是如何回收内存的奥秘

浏览器查看内存使用情况

memory字段代表JavaScript对内存的占用:

memory:{
    jsHeapSizeLimit: 3760000000 // 内存大小限制
    totalJSHeapSize: 10000000   // 可使用的内存
    usedJSHeapSize: 10000000    //JS对象占用的内存,一定小于 totalJSHeapSize
}

其中,usedJSHeapSize一定要小于totalJSHeapSize值,否则出现内存泄漏的问题。

2、node查看内存使用情况

通过node来查看内存使用情况:process.memoryUsage();在nodejs环境下查看内存使用的情况:

edzdeMacBook-Pro-5:V8引擎内存分配 edz$ node
Welcome to Node.js v12.14.1.
Type ".help" for more information.
> process.memoryUsage()
{
  rss: 24498176,      //V8申请到的总占用空间
  heapTotal: 4608000, //堆总内存
  heapUsed: 2412240,  //已经使用了的内存
  external: 1344872   // node底层是C++,他可以申请到一些C++的内存
}
}
> 

heapTotal 和 heapUsed 代表 V8 的内存使用情况。 external 代表 V8 管理的,绑定到 Javascript 的 C++ 对象的内存使用情况。 rss 是驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分),这些物理内存中包含堆、代码段、以及栈。

对象、字符串、闭包等存于堆内存。 变量存于栈内存,实际的 JavaScript 源代码存于代码段内存。

使用 Worker 线程时, rss 将会是一个对整个进程有效的值,而其他字段只指向当前线程。

Node在启动时,可以通过设置参数来调整内存限制的大小。

node --max-old-space-size=1700 test.js //设置老生代内存空间最大值,单位为MB
node --max-new-space-size=1024 test.js //设置新生代内存空间最大值,单位为KB

process.memoryUsage() 方法返回 Node.js 进程的内存使用情况的对象,该对象每个属性值的单位为字节(Btye)。
Btye转化MB函数:

var format = function(bytes) {
    return (bytes/1024/1024).toFixed(2)+'MB';
};

自己写个方法来查看效果:

function getme(){
    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) + '  rss  ' + format(mem.rss));
};

四、吃透JS内存回收

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

1、全局变量使用

var size=20*1024*1024; //每个是20MB
var arr = new Array(size);
getme();
var arr1 = new Array(size);
getme(); 
var arr2 = new Array(size);
getme();
var arr3 = new Array(size);
getme();
var arr4 = new Array(size);
getme();
var arr5 = new Array(size);
getme();
var arr6 = new Array(size);
getme();
var arr7 = new Array(size);
getme();
var arr8 = new Array(size);
getme();
var arr9 = new Array(size);
getme();
var arr10 = new Array(size);
getme();
var arr11 = new Array(size);
getme();
var arr12 = new Array(size);
getme();
var arr13 = new Array(size);
getme();
var arr14 = new Array(size);
getme();
var arr15 = new Array(size);
getme();
console.log("输出的内容");

运行这段代码的结果,当运行完变量arr13时停止,此时内存已满,不能运行arr14、arr15。全局变量程序执行完才能回收,arr14、arr15还没有执行,所以内存已经使用完,这里我们可以看到程序如何一步一步走到报错。

edzdeMacBook-Pro-5:V8引擎内存分配 edz$ node test.js
Process: heapTotal:164.02MB  heapUsed:161.98MB  rss:181.55MB
Process: heapTotal:325.27MB  heapUsed:322.18MB  rss:342.80MB
Process: heapTotal:487.53MB  heapUsed:482.18MB  rss:503.10MB
Process: heapTotal:651.53MB  heapUsed:642.18MB  rss:663.22MB
Process: heapTotal:819.54MB  heapUsed:802.18MB  rss:823.50MB
Process: heapTotal:995.54MB  heapUsed:962.18MB  rss:984.01MB
Process: heapTotal:1155.54MB  heapUsed:1122.18MB  rss:1144.16MB
Process: heapTotal:1315.55MB  heapUsed:1282.18MB  rss:1304.30MB
Process: heapTotal:1475.55MB  heapUsed:1442.18MB  rss:1464.38MB
Process: heapTotal:1635.55MB  heapUsed:1602.18MB  rss:1624.39MB
Process: heapTotal:1795.56MB  heapUsed:1762.19MB  rss:1784.40MB
Process: heapTotal:1955.56MB  heapUsed:1922.19MB  rss:1944.41MB
Process: heapTotal:2115.57MB  heapUsed:2081.71MB  rss:2104.54MB

<--- Last few GCs --->

[33023:0x1028cd000]     3829 ms: Mark-sweep 2081.7 (2114.8) -> 2081.6 (2083.8) MB, 701.3 / 0.0 ms  (average mu = 0.033, current mu = 0.000) last resort GC in old space requested
[33023:0x1028cd000]     5004 ms: Mark-sweep 2081.6 (2083.8) -> 2081.6 (2083.8) MB, 1174.6 / 0.0 ms  (average mu = 0.013, current mu = 0.000) last resort GC in old space requested

<--- JS stacktrace --->

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x1009311f9]
    1: ConstructFrame [pc: 0x1008ad1da]
    2: StubFrame [pc: 0x100991960]
Security context: 0x3bec359c08a1 <JSObject>
    3: /* anonymous */ [0x3bec39ad69a9] [/Users/edz/Desktop/V8??????/test.js:37] [bytecode=0x3beceb81b341 offset=243](this=0x3bec39ad6ad9 <Object map = 0x3becfd940431>,0x3bec39ad6ad9 <Object map = 0x3becfd940431>,0x3bec39ad6a99 <JSFunction require (sfi = 0x3beceb81b921)>,0x3bec39ad6a09 <...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: 0x10007f231 node::Abort() [/usr/local/bin/node]
 2: 0x10007f3b5 node::OnFatalError(char const*, char const*) [/usr/local/bin/node]
 3: 0x100176887 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
 4: 0x100176823 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/bin/node]
 5: 0x1002fa9d5 v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/usr/local/bin/node]
 6: 0x100302788 v8::internal::Heap::AllocateRawWithRetryOrFail(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/usr/local/bin/node]
 7: 0x1002ce0c9 v8::internal::Factory::NewFixedArrayWithFiller(v8::internal::RootIndex, int, v8::internal::Object, v8::internal::AllocationType) [/usr/local/bin/node]
 8: 0x1004357ea v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastHoleySmiElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)1> >::ConvertElementsWithCapacity(v8::internal::Handle<v8::internal::JSObject>, v8::internal::Handle<v8::internal::FixedArrayBase>, v8::internal::ElementsKind, unsigned int, unsigned int, unsigned int) [/usr/local/bin/node]
 9: 0x1004356b8 v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastHoleySmiElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)1> >::GrowCapacityAndConvertImpl(v8::internal::Handle<v8::internal::JSObject>, unsigned int) [/usr/local/bin/node]
10: 0x1004353b2 v8::internal::(anonymous namespace)::ElementsAccessorBase<v8::internal::(anonymous namespace)::FastHoleySmiElementsAccessor, v8::internal::(anonymous namespace)::ElementsKindTraits<(v8::internal::ElementsKind)1> >::SetLengthImpl(v8::internal::Isolate*, v8::internal::Handle<v8::internal::JSArray>, unsigned int, v8::internal::Handle<v8::internal::FixedArrayBase>) [/usr/local/bin/node]
11: 0x100421cd0 v8::internal::ArrayConstructInitializeElements(v8::internal::Handle<v8::internal::JSArray>, v8::internal::Arguments*) [/usr/local/bin/node]
12: 0x1005d236d v8::internal::Runtime_NewArray(int, unsigned long*, v8::internal::Isolate*) [/usr/local/bin/node]
13: 0x1009311f9 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_NoBuiltinExit [/usr/local/bin/node]
14: 0x1008ad1da Builtins_JSBuiltinsConstructStub [/usr/local/bin/node]
Abort trap: 6

2、局部变量

局部变量当程序执行结束,并不是引用的时候就会随之消失

var size = 20*1024*1024;  //内存是20MB
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();
//局部变量可以回收,只是说可以回收,并不是用完就回收
for(var i=0;i<16;i++){
  a.push(new Array(size));
  getme();
}

看看输出的结果:

edzdeMacBook-Pro-5:V8引擎内存分配 edz$ node test.js
Process: heapTotal:835.29MB  heapUsed:801.93MB  rss:822.85MB
Process: heapTotal:995.04MB  heapUsed:961.78MB  rss:983.56MB
Process: heapTotal:1155.04MB  heapUsed:1121.79MB  rss:1143.58MB
Process: heapTotal:1315.05MB  heapUsed:1281.78MB  rss:1303.64MB
Process: heapTotal:1475.05MB  heapUsed:1441.78MB  rss:1463.66MB
Process: heapTotal:1635.05MB  heapUsed:1601.78MB  rss:1623.67MB
Process: heapTotal:1795.06MB  heapUsed:1761.78MB  rss:1783.69MB
Process: heapTotal:1955.06MB  heapUsed:1921.78MB  rss:1943.72MB
Process: heapTotal:1475.05MB  heapUsed:1441.70MB  rss:1463.71MB //这一行显示了变量部分被回收
Process: heapTotal:1635.05MB  heapUsed:1601.70MB  rss:1623.84MB
Process: heapTotal:1795.06MB  heapUsed:1761.70MB  rss:1783.85MB
Process: heapTotal:1955.06MB  heapUsed:1921.76MB  rss:1943.86MB
Process: heapTotal:2115.07MB  heapUsed:2081.70MB  rss:2103.88MB

五、注意内存的使用

1、优化内存的技巧

  • 尽量不要定义全局变量。
  • 全局变量记得销毁掉。
    不推荐大家在开发时写delete,delete在严格模式下有Bug。js里null是一个保留字,undefined 其实是个变量
var size = 20*1024*1024;
var arr = new Array(size);
arr=undefined; //arr对象会被V8垃圾回收

用匿名自执行函数变全局为局部。

(function(){ //自执行函数
  var arr = new Array(size);
  ...
})();

尽量避免闭包引用。

function a(){
    var size = 20*1024*1024;
    var arr1 = new Array(size);
    return arr1;
}
a(); //引用多次不会存在问题  
var b=a(); 
var c=a(); 
var d=a();
...
//这样多次引用会出现问题,会占用大量内存 没执行一次就会初始化一次,后边调用的时候执行的是里边的匿名函数
b=null,c=null, d=null ... //应及时解除引用,否则会占用更多内存

2、防止内存泄漏

  • 滥用缓存
    缓存通常放在全局,尽量不要用V8缓存大的变量,服务端可以用Redis,前端可以用Localstore,如果一定要使用,如果非要用V8缓存,需要加一道”锁“:
var size = 20*1024*1024;  //内存是20MB
var a=[];
for(var i=0;i<16;i++){
  if(a.length>4){  //锁
     a.shift();
  }
  a.push(new Array(size));
}

大内存量操作
nodejs注意的地方:

import {createReadtream,write} form "fs";
// node操作文件
// api是一次性读取文件到buffer
//读取小文件:
fs.readFile();
//读取大文件
createReadtream().pipe(write);

六、我们为什么要了解底层

1、如果一个东西谁都会,那么这个东西就会变得不值钱。

2、了解底层才能不是一个简单的搬砖工。

3、除了底层,视野,技术的全面性也很重要。

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注