V8 引擎是如何给 JS"打扫房间"的 ?
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
JS 语言不像 C/C++, 让程序员自己去开辟或者释放内存,而是类似Java,采用自己的一套垃圾回收算法进行自动的内存管理。今天就从内存结构说起,一步步聊聊 V8 的垃圾回收机制。 先搞懂JS 的内存都存在哪里?JS 的内存存储分两块:栈(Stack) 和堆(Heap) ,就像家里的 "鞋柜" 和 "储物间"—— 常用的小东西放鞋柜,大件杂物放储物间。 栈栈是一块连续的内存空间,就像排队的抽屉,每个抽屉大小固定。它主要存两种东西:
栈的回收特别简单:函数执行时会创建 " 堆堆是一块不连续的内存空间,大小不固定,就像开放式储物间,专门存引用类型(对象、数组、函数等)。比如创建一个 堆的麻烦在于:对象不会像栈里的变量那样 "用完就走"。比如全局对象 V8 的内存结构V8 引擎把堆内存又细分了两块:
这就是 V8 的 "分代回收" 思路:不同生命周期的对象,用不同的方式回收,效率更高。 栈内存的回收栈的回收几乎不用我们操心,全靠 JS 引擎的 "执行上下文管理"。举个例子:
这种回收方式效率极高,几乎不消耗额外性能,所以栈内存很少出问题。 新生代内存的回收新生代存的都是 "短命对象",比如循环里创建的临时对象:
这些对象的回收,V8 用的是Scavenge 算法,核心是 "复制存活对象,清空剩余空间"。具体步骤可以脑补成这样:
为什么要这么折腾?主要是为了避免 " 而复制到 To 空间时按顺序排列,存活对象会挤在一起,剩下的空间是一整块连续区域,下次分配新对象就很方便。 当然,这种方式也有代价:新生代内存实际只能用一半(总有一个空间闲置)。但好在新生代对象存活时间短,复制成本低,总体算下来比标记清除快得多。 老生代内存的回收当新生代的对象 "活过" 多次回收(比如被全局变量引用,或者在闭包里被长期持有),就会被 "
老生代的对象要么体积大,要么存活久,用 Scavenge 算法复制太费时间,所以 V8 换了套思路:标记 - 清除+标记 - 整理。 第一步:标记 - 清除
这种方式解决了引用计数法的 "循环引用" 问题。比如两个对象互相引用,但都不再被全局访问,标记阶段它们不会被标记,清除阶段会被回收 —— 而引用计数法会因为它们互相引用,计数不为 0,永远不回收,导致内存泄漏。 第二步:标记 - 整理标记 - 清除后,堆内存会像被挖过的地一样坑坑洼洼:存活对象零散分布,中间夹杂着被回收的空白区域(内存碎片)。下次想分配一个大对象,可能找不到连续的空间,明明总内存够,却分配失败。 所以 V8 会紧接着做 "标记 - 整理":把所有存活对象往内存的一端 "挤",让空白区域集中到另一端,形成一整块连续的空闲内存。就像把衣柜里的衣服都推到左边,右边留出一大块空地放新衣服。 老生代的"增量标记"JS 是单线程的,一旦开始垃圾回收,JS 代码就会暂停(称为 "Stop-The-World")。如果老生代内存很大,一次完整的标记 - 清除可能要卡 1 秒以上 —— 用户点按钮没反应,页面像死机了一样。 为了解决这个问题,V8 用了增量标记:把原本一口气完成的标记阶段,拆成一小块一小块,穿插在 JS 代码执行间隙。比如标记 10ms,就让 JS 执行 20ms,再标记 10ms... ,如果循环,直到标记阶段完成才进入内存碎片的整理上面来,不耽误JS代码的正常执行。 这样一来,单次垃圾回收的阻塞时间从几百毫秒降到几十毫秒,用户几乎感觉不到卡顿。数据显示,增量标记能把垃圾回收的阻塞时间减少到原来的 1/6,对大型应用来说太重要了。 最后搞懂 V8 的回收机制后,我总结了几个对开发有用的点:
转自https://juejin.cn/post/7524812060761489418 该文章在 2025/7/10 9:53:21 编辑过 |
相关文章
正在查询... |