V8的内存管理与垃圾回收(一)

大多数时候,js开发者其实根本无须接触垃圾回收机制或内存管理机制等问题,因为曾经的js仅仅应用于客户端浏览器(现在的绝大多数前端开发场景同样也是),浏览器端几乎绝少出现垃圾回收对我们的网站性能构成较大影响的情况,即时发生内存泄漏的情况,比如早期的低版本IE与DOM交互可能发生的内存泄漏问题,页面的卡顿估计也迫使用户进行刷新操作了,且浏览器端的应用运行时间也比较短,进程一旦退出,内存也会自动释放,几乎没有内存管理的必要了。
然而,2008年Chrome开源了V8引擎,这款性能极高的JavaScript引擎促使了Node.js的诞生并且得到了快速的普及(事实上,V8在接下来的性能跑分中一直处于领先地位,一改以往JavaScript的性能低下的形象),如今Node.js的使用覆盖了各个端,各种应用,Node.js成为了前端开发的必学技术。现在撇开短时间运行的CLI工具或是桌面应用不谈,考虑服务器上部署的Node.js应用,此时,内存资源“寸土寸金”,在海量的用户请求和长时间的运行场景下,即时是很小的内存使用不恰当,也可能会造成非常严重的性能问题,这时候就需要Node.js开发者们熟悉V8的内存管理模式和垃圾回收机制了。

V8的内存限制

首先需要说明的是,V8限制了所能使用的内存极限(64位系统下约为1.4GB,32位系统下约为0.7GB),所以在使用nodejs进行服务端开发的时候不能直接进行大内存对象的操作。V8限制内存使用上限的原因,表面上看是因为V8最初是为浏览器的js引擎设计,如前言中所说,浏览器端对内存的使用需求很小,V8设计的内存使用大小在浏览器端上运行起来绰绰有余,但是其更深层次的原因还是受V8的垃圾回收机制的限制,在后文中会具体说明。

V8内存使用策略

V8的内存配置和JVM一样,都是分配在堆内存中的,可以使用process.memoryUsage()方法查看V8的内存使用情况(单位:字节),下面将单位转换后输出查看:

function memUsage() {
  const memory = process.memoryUsage();
  console.log('=============================');
  console.log(`heapTotal:${(memory.heapTotal / 1024 / 1024).toFixed(2)}MB`);
  console.log(`heapUsed: ${(memory.heapUsed / 1024 / 1024).toFixed(2)}MB`);
  console.log(`rss: ${(memory.rss / 1024 / 1024).toFixed(2)}MB`);
}
memUsage();

可以在你自己的电脑上输出查看一下,我的如下:


1.jpg

如上图所示,各个字段的含义为:

  • heapTotal:V8已申请到的堆内存
  • heapUsed:当前内存使用量
  • rss:官网解释:驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分) 这些物理内存中包含堆,栈,和代码段。简单说:进程的常驻内存(node所占的内存)

可以看到当前V8申请到的堆内存很小,只有7MB不到,现在尝试扩大内存花销,看看扩大过程中堆内存使用情况的变化,尝试执行以下代码:

function memoryAnalyse() {
  const setArray = () => {
    // 设置一个超大长度的数组
    let size = 30 * 1024 * 1024;
    let array = new Array(size);
    // 不停地往数组中塞值,这将引起内存花销的迅速增大
    for (let i = 0; i < size; i ++) {
      array[i] = 0;
    }
    return array;
  }
  // 连续设置8个上面所示的超大数组
  for (let j = 0; j < 8; j++) {
    setArray();
    memUsage(); // 输出内存使用情况
  }
}

ok,执行以上代码得到以下结果


2.jpg

可以看到每次设置一个超大数组时,heapTotal都增加了大概240MB,并且heapUsed基本等于heapTotal,heapTotal最大时达到了1GB以上了,再往后heapTotal反而减小了。上述过程意味着:V8并不是一开始就申请到其内存上限的大小的,而是在当前堆内存使用已满时再申请更多的堆内存,直至V8的堆内存使用上限,当达到上限之后内存溢出了。这里竟然出现了内存中数据被销毁的问题!(在不同的V8和node版本中,内存溢出的处理情况可能不一致,某些情况下会出现内存溢出后,内存因无法继续分配导致循环都无法继续执行),总之:千万注意内存使用情况,避免内存溢出!

当然,V8并没有对内存的限制进行完全封死,它提供了扩大内存上限的选项,网上查看的资料所示的命令都是如下两条:

node --max-old-space-size=2048 memory.js // 设置老生代内存最大限制,单位:MB
node --max-new-space-size=1024 memory.js // 设置新生代内存最大上限,单位:MB

也许是不同系统或node版本下命令不同,我的两条命令如下所示:

--max_old_space_size // 老生代内存最大限制
--min_semi_space_size // 新生代中单个semi-space的内存最大上限(一共两个semi-space)

具体可以使用以下命令查看自己电脑的版本下命令是什么:

node --v8-options

可以看到终端显示了大量关于V8的选项及其含义:


3.jpg

上面提到了新生代内存老生代内存以及semi-space的概念,接下来详细解释这些东西。

V8的垃圾回收机制

V8的堆其实并不只是由老生代和新生代两部分构成,可以将堆分为以下几个不同的区域:

  • 新生代内存区:存储存活时间较短的对象,这个区域很小但是垃圾回收特别频繁
  • 老生代指针区:属于老生代,这里包含了大多数可能存在指向其他对象的指针的对象,大多数从新生代晋升的对象会被移动到这里
  • 老生代数据区:属于老生代,这里只保存原始数据对象,这些对象没有指向其他对象的指针
  • 大对象区:这里存放体积超越其他区大小的对象,每个对象有自己的内存,垃圾回收其不会移动大对象
  • 代码区:代码对象,也就是包含JIT之后指令的对象,会被分配在这里。唯一拥有执行权限的内存区
  • Cell区、属性Cell区、Map区:存放Cell、属性Cell和Map,每个区域都是存放相同大小的元素,结构简单

垃圾回收器只会针对新生代内存区、老生代指针区以及老生代数据区进行垃圾回收。所以本文的讨论中可以将V8的内存划分简化为以下所示:

4.jpg

上文提到的--max-old-space-size就是设置老生代内存区最大尺寸的命令,--max-new-space-size为设置新生代内存最大尺寸的命令,但是这两个值只能在启动进程时就设置好,并不能在应用运行过程中实时调控。
V8的垃圾回收器对新生代和老生代采取了两种不同的回收算法:


新生代垃圾回收策略

新生代中的对象主要通过Scavenge算法进行回收,在Scavenge算法的实现中,主要采用了Cheney算法。
Cheney算法是一种采用复制的方式实现的垃圾回收算法。它将内存一分为二,每一部分空间称为semi-space。在这两个semi-space中,一个处于使用状态,另一个处于闲置状态。处于使用状态的semi-space空间称为From空间,处于闲置状态的空间称为To空间。当我们分配对象时,先是在From空间中进行分配;当开始进行垃圾回收算法时,会检查From空间中的存活对象,这些存活对象将会被复制到To空间中(复制完成后会进行紧缩),而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间的角色发生对换(称为翻转)。简而言之,在垃圾回收的过程中,就是通过将存活对象在两个semi-space之间进行复制。

很明显可以看出:Scavenge算法只能使用堆内存的一半,但是由于新生代中的对象的存活时间一般较短,所以存活对象占新生代所有对象的较小部分,这样复制所需要的开销就很小,这是一种典型的牺牲空间换取时间的算法。

现在V8的内存划分大致是如下情况了:


5.jpg
如何判断对象是否存活?

如何判断一个对象是否存活是垃圾回收中最根本的问题。存活对象的条件为:当且仅当它被一个根对象或另一个活对象所指向。根对象永远是活对象,它是被浏览器或V8所引用的对象。被局部变量所指向的对象也属于根对象,因为它们所在的作用域对象被视为根对象。全局对象(Node中为global,浏览器中为window)自然是根对象。浏览器中的DOM元素也属于根对象。

对象复制过程

首先将From区的根对象直接指向的对象复制进To区中,To区有两个指针:scanPtr和allocationPtr。scanPtr指向即将扫描的存活对象 ,allocationPtr指向即将为新对象分配内存的地方。scanPtr循环扫描To中的对象,判断对象是否有别的指向并且确定指向哪里,若其对象有指向并且指向的是From区对象,则从From区复制这个被指向的对象进To空间的allocationPtr位置,scanPtr移向下一个存活对象,allocationPtr移向下一个空闲位置。对象复制过程采用的广度优先算法,从根对象出发,遍历所有能达到的对象。我们先假设有一个程序的对象引用情况如下所示:

6.jpg

其复制过程大致如下所示:

  1. From区的对象存储为:


    7.jpg
  2. From区中根对象直接指向的对象复制进To区,此时To区为:


    8.jpg
  3. scanPtr扫描A对象,发现有指向对象D,且D在From区中,则复制D进To区的allocationPtr所在位置,如下所示:


    9.jpg
  4. scanPtr扫描B对象,发现指向E和F,且都在From区,则依次复制进To区中:


    10.jpg
  5. scanPtr扫描D对象,没有发现指向,继续扫描E,发现指向G,且G在From区,则复制G进To区中:


    11.jpg
  6. scanPtr扫描F对象,没有发现指向,继续扫描G,也没有发现指向,scanPtr继续下移,于是此时和allocationPtr指向了同一块位置,则此轮复制已结束:


    12.jpg
  7. 此时From区和To区的情况如下所示,由于C对象没有任何指向,也不被任何对象指向,所以垃圾回收器将视其为非存活对象进行回收:

From区:
7.jpg

To区:
13.jpg
写屏障

如果新生代中的一个对象只有一个指向它的指针,而这个指针在老生代中,我们如何判断这个新生代的对象是否存活?为了解决这个问题,需要建立一个列表用来记录所有老生代对象指向新生代对象的情况。每当有老生代对象指向新生代对象的时候,我们就记录下来

对象晋升

当一个对象经过多次Scavenge算法进行复制后,还处于存活状态,则说明这个对象存活时间较长,应该被移至老生代内存中。对象从新生代内存中被移至老生代内存中的过程称为晋升。对象晋升有两种情况:

  1. From区复制某对象时,会检查它的内存地址来判断这个对象是否已经经历过一次Scavenge的回收,若是,则进行晋升,否则复制到To区中;
  2. 对象在被复制到To时,如果To区的空间已经被使用了超过25%,那么这个对象直接进行晋升。

至此,新生代的垃圾回收过程已完成。老生代的垃圾回收策略在下一篇中进行讨论。

PS. 若本文有任何错误之处,欢迎指出!


参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容