深入理解JS内存管理机制

0

一、前言

在相当长一段时间里,JS运行时的内存问题都不被前端开发人员所关注。
一方面,日常开发中基本不会遇上需要对内存精准控制的场景,另一方面,写JS不需要像写 C/C++ 那样在开发过程中随时关注内存的分配和释放问题。

随着生态的逐渐完善,JS的执行环境也不再局限于浏览器中。目前,JS主要的执行场景包括服务端(NodeJSDeno)、桌面端(Electron)、浏览器(ChromeMicrosoft Edge)。

其中,JS执行引擎 V8 因其优异的性能表现,已经成为主流。因此,本文对JS内存管理模型的研究也将基于V8展开。

二、内存结构

1

上图展示了V8引擎的内存结构,整体上分成两部分:

2.1 堆内存

这里是存储对象或动态数据的地方,也是占比最大的内存区域。堆内可以细分以下区域:

  • 新生代(Young generation) 新生代是新对象存在的地方,这些对象中的大多数都是短暂存在的。
    这部分空间很小(默认情况下 16~32M),并且拆分成了两个空间。
    空间使用 Minor GC (Scavenger) 进行垃圾回收。
  • 老生代(Old generation) 新生代中经历了两个Minor GC 周期的对象会被转移到老生代中存放。这里占据着大量的内存空间(默认情况下 700~1400M
    空间使用 Major GC(Mark-Sweep & Mark-Compact) 进行垃圾回收。
  • 大对象区(LARGE OBJECT SPACE) 超过一定大小的对象会直接在大对象区中被创建,并在不被使用时将其直接回收。
  • 代码区(CODE SPACE) 这是 即时编译器(JIT) 存储编译代码块的地方。
  • 其他区(CELL, PROPERTY CELL,MAP SPACE) 这些空间存放大小相同的对象,并且对它们指向的对象类型有一些限制。
    比如MAP SPACE里存放的是hidden class信息,这能让V8快速定位到对象值所在的内存区。
2.2 栈内存

栈是用来存储静态数据的地方,内容主要包括:

  • 基本类型(Number, Boolean, String, Null, Undefined, Symbol, BigInt) 对于基本类型,系统会为新的变量在栈内存中分配一个新值。
  • 引用类型 系统会为新的变量在栈内存中分配一个值,这个值是一个对象的引用。
  • 调用栈 解释器创建了调用栈来记录函数的调用过程。
    每调用一个函数,解释器就把该函数添加进调用栈,解释器会为被添加进来的函数创建一个栈帧(用来保存函数的局部变量以及执行语句)并立即执行。
    如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。

三、内存回收

栈内存 其实是由操作系统进行自动管理的,本文不做讨论。

堆内存 由V8进行管理,它占据最大的内存空间,并且随着程序运行时间的增加可能会持续增长,最终耗尽内存。它也会变得碎片化,影响程序运行的速度。这时内存回收的重要性就体现出来了。

要进行内存回收,需要先明确一个问题:什么样的数据可以被回收。

V8通过回收 不可达对象 来释放堆内存,整个回收过程总体可以分为 标记回收 两个阶段,涉及到的原理是 三色标记分代回收

3.1 新生代

2.1堆内存 小节中我们知道,新生代被拆分成两个小空间,使用 Minor GC (Scavenger) 进行垃圾回收。Minor GC (Scavenger) 我们可以简称为次要GC

两个拆分出来的空间分别称之为 to-spacefrom-space。新加入的对象都会存放到from-space,当from-space被填满时,会触发次要GC。

GC过程

  1. 标记 从堆栈指针开始递归遍历 from-space 中的对象图查找 活跃对象
  2. 复制 将这些对象复制到 to-space 中(包括被这些对象引用的所有对象)。
    重复此操作,直到扫描 from-space 中的所有对象。另外,to-space 会分配连续的内存块,以减少碎片。
  3. 清除 清空 from-space,因为此时剩余的对象都是可回收的。
  4. 交换to-spacefrom-space 互换,即 to-space 变成 from-space。
image

回收的最后一步是更新引用已移动的原始对象的指针。每个被复制的对象都会留下一个新地址,用于更新原始指针以指向新位置。

此时还存在一个问题:随着活跃对象的累积,from-space 很快会被填满。

这时就轮到老生代出场了,在新生代中经历两次GC并存活下来的对象,会被转移到老生代,这个过程被称为晋升。如下图:

image

至此,新生代一次完整的垃圾回收就完成了。

3.2 老生代

在老生代中,垃圾回收为主要GC(Major GC),包含了 标记清除(Mark-Sweep)标记整理(Mark-Compact)

GC过程

  1. 标记 垃圾收集器识别哪些对象正在使用,哪些对象未使用。正在使用或可从GC根域递归访问的对象被标记为活跃对象
  2. 清除 清除未被标记为活跃对象的数据。
  3. 整理 如果碎片较多,会将存活的对象移动到一起,减少碎片提高内存使用率。
2
3.3 三色标记

之前提到,垃圾回收时会先标记 活跃对象 来区分对象是否应该被回收,这里就涉及到了三色标记算法。

标记位有三种颜色:

  • 白色 对象未被标记。
  • 灰色 对象已经被标记,但对象内属性还未遍历完成。
  • 黑色 对象已经被标记,且对象内的属性也已完成遍历(活跃对象)。

标记过程:

  1. 开始标记
    初始所有对象都是白色,当收集器发现白色对象并将其推送到标记工作列表时,会将其标记成灰色。
    01.jpg
  1. 标记完成对象
    当收集器访问目标对象的所有字段后,会将对象的颜色由灰色变为黑色。
    02.jpg
  1. 标记结束
    当没有灰色对象时,代表标记结束。此时剩余的白色对象表示无法访问,可以被回收。
    03.jpg

经历过以上三步,一次完整的标记过程就完成了。

3.4 回收优化

现在我们知道,一次垃圾回收总需要经历 标记回收整理 等阶段。

事实上,整个垃圾回收的过程是非常耗时的,比如光是标记整个堆内存的活跃对象可能就要花费数百毫秒,并且期间会阻塞程序的正常的执行。

[图片上传失败...(image-ad8178-1632313567745)]

对此,V8也在持续优化,目前主要的优化方式有:

  • 增量标记
    通过将标记任务拆分成一系列小任务,确保每次标记任务的持续时间在 5~10 毫秒。
    当堆的占用空间达到某个阈值大小时,开始激活标记任务,此后每分配一定量的内存,就会执行增量标记。
    增量标记与常规标记一样,本质上都是深度优先搜索,同样使用的是三色标记算法。

  • 并行 & 并发

    • 并行 V8会创建辅助线程,与主线程同时处理GC任务。这样GC时间就约等于总时间除以协同的线程数了。
    • 并发 这里的并发是指主线程不再处理GC任务,而是由辅助线程来执行。这样的好处是垃圾回收不再阻塞正常任务的执行。
  • 惰性清除
    当所有对象都被标记完成,此时已经可以进行垃圾清除的工作。但实际上这部分工作可以延迟执行,尤其是当内存足够的时候。
    V8会在合适的时间点执行清除工作,比如工作线程空闲,或者内存不足的时候。

总结

内存管理是一件非常复杂的事情,本文主要从 内存结构内存回收 两个方面进行了介绍,并且隐藏了其中的一些细节。

在V8的内存管理模型中,其实能学习到一些通用的内存管理思想和性能优化方法。
比如,垃圾回收总是会围绕标记清除整理展开。
在标记算法上,V8JAVA,Golang,PHP 等编程语言一样,使用了三色标记
在性能优化上,多进程/多线程分步异步延迟 等方式也总能发挥作用。


参考资料

原文已在玩物得志技术公众号上发布,链接:https://mp.weixin.qq.com/s/7mvP5jv5sBGNfnThap6JSQ

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

推荐阅读更多精彩内容