V8内存管理及垃圾回收机制
JavaScript引擎的内存空间主要分为栈和堆。
栈
栈是临时存储空间,主要存储局部变量和函数调用。
基本类型数据(Number, Boolean, String, Null, Undefined, Symbol, BigInt)保存在在栈内存中。引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。
基本类型赋值,系统会为新的变量在栈内存中分配一个新值,这个很好理解。引用类型赋值,系统会为新的变量在栈内存中分配一个值,这个值仅仅是指向同一个对象的引用,和原对象指向的都是堆内存中的同一个对象。
对于函数,解释器创建了”调用栈“来记录函数的调用过程。每调用一个函数,解释器就可以把该函数添加进调用栈,解释器会为被添加进来的函数创建一个栈帧(用来保存函数的局部变量以及执行语句)并立即执行。如果正在执行的函数还调用了其他函数,新函数会继续被添加进入调用栈。函数执行完成,对应的栈帧立即被销毁。
两种查看调用栈的方法
-
使用 console.trace() [1] 向Web控制台输出一个堆栈跟踪. -
浏览器开发者工具进行断点调试
栈虽然很轻量,在使用时创建,使用结束后销毁,但是不是可以无限增长的,被分配的调用栈空间被占满时,就会引起”栈溢出“的错误。
(function foo() {
foo()
})()
为什么基本数据类型存储在栈中,引用数据类型存储在堆中?
JavaScript引擎需要用栈来维护程序执行期间的上下文的状态,如果栈空间大了的话,所有数据都存放在栈空间里面,会影响到上下文切换的效率,进而影响整个程序的执行效率。
堆
堆空间存储的数据比较复杂,大致可以划分为下面 5 个区域:代码区(Code Space)、Map 区(Map Space)、大对象区(Large Object Space)、新生代(New Space)、老生代(Old Space)。本篇文章主要讨论新生代和老生代的内存回收算法。
新生代内存是临时分配的内存,存活时间段,老生代内存是常驻内存,存活时间长。
新生代内存回收
新生代中用 Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域(from),一半是空闲区域 (to)。
新的对象会首先被分配到 from 空间,当进行垃圾回收的时候,会先将 from 空间中的 存活的对象复制到 to 空间进行保存,对未存活的对象的空间进行回收。复制完成后, from 空间和 to 空间进行调换,to 空间会变成新的 from 空间,原来的 from 空间则变成 to 空间。这种算法称之为 ”Scavenge“。
新生代内存回收频率很高,速度也很快,但是空间利用率很低,因为有一半的内存空间处于"闲置"状态。
老生代内存回收
新生代中多次进行回收仍然存活的对象会被转移到空间较大的老生代内存中,这种现象称为晋升。以下两种情况
-
在垃圾回收过程中,发现某个对象之前被清理过,那么将会晋升到老生代的内存空间中 -
在 from 空间和 to 空间进行反转的过程中,如果 to 空间中的使用量已经超过了 25% ,那么就讲 from 中的对象直接晋升到老生代内存空间中。
因为老生代空间较大,如果仍然用 Scavenge 算法来频繁复制对象,那么性能开销就太大了。
标记-清除(Mark-Sweep)
老生代采用的是”标记清除“来回收未存活的对象。
分为标记和清除两个阶段。标记阶段会遍历堆中所有的对象,并对存活的对象进行标记,清除阶段则是对未标记的对象进行清除。
标记-整理(Mark-Compact)
标记清除不会对内存一分为二,所以不会浪费空间。但是经过标记清除之后的内存空间会生产很多不连续的碎片空间,这种不连续的碎片空间中,在遇到较大的对象时可能会由于空间不足而导致无法存储。为了解决内存碎片的问题,需要使用另外一种算法 - 标记-整理(Mark-Compact)。标记整理对待未存活对象不是立即回收,而是将存活对象移动到一边,然后直接清掉端边界以外的内存。
增量标记
为了避免出现JavaScript应用程序与垃圾回收器看到的不一致的情况,进行垃圾回收的时候,都需要将正在运行的程序停下来,等待垃圾回收执行完成之后再回复程序的执行,这种现象称为“全停顿”。如果需要回收的数据过多,那么全停顿的时候就会比较长,会影响其他程序的正常执行。
为了避免垃圾回收时间过长影响其他程序的执行,V8将标记过程分成一个个小的子标记过程,同时让垃圾回收和JavaScript应用逻辑代码交替执行,直到标记阶段完成。我们称这个过程为增量标记算法。通俗理解,就是把垃圾回收这个大的任务分成一个个小任务,穿插在 JavaScript任务中间执行,这个过程其实跟 React Fiber 的设计思路类似。
参考
-
V8 引擎垃圾内存回收原理解析 [2] -
JavaScript中V8引擎内存问题 [3] -
浅谈V8引擎中的垃圾回收机制 [4] -
《深入浅出Node.js》
参考资料
console.trace(): https://developer.mozilla.org/zh-CN/docs/Web/API/Console/trace
[2]V8 引擎垃圾内存回收原理解析: https://juejin.im/post/5dcb7f706fb9a04aad01615a
[3]JavaScript中V8引擎内存问题: https://www.cnblogs.com/cangqinglang/p/12668374.html
[4]浅谈V8引擎中的垃圾回收机制: https://segmentfault.com/a/1190000000440270
本文分享自微信公众号 - 牧码的星星(gh_0d71d9e8b1c3)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
浅谈常见数据结构和算法的应用系列(一)
近来有小伙伴问我:刷leetcode真的有用吗,觉得收益很小,越刷越迷茫了… 诚然每个人刷题的目的不一样,233酱还不是为了能水几篇文章… 当然不止。我觉得刷题是一件有意思的事,就像小猫小狗咬自己尾巴,玩弄的不亦乐乎。比喻可能不太恰当,是有种沉迷小游戏的感觉。 可是在艰难打野的过程中,我们不要忘了,最重要的是: 了解每种技能包的特点,适合解决的问题和场景。在特定实战场景下能够使用特定的技能包,自创技能包。这才是武功的至高境界。 装X结束,浅谈开始。。 数据结构是指:一种数据组织、管理和存储的格式,它可以帮助我们实现对数据高效的访问和修改。 数据结构 = 数据元素 + 元素之间的结构。 如果说数据结构是造大楼的骨架,算法就是具体的造楼流程。流程不同,效率资源不同。我会两者结合简单探讨下他们的特点和应用。 常见的数据结构可分为:线性结构、树形结构 和 图状结构。 常见的算法有:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法等。 本文从 线性数据结构、递归 和 排序算法 谈起。 线性结构 线性结构:是指数据排成像一条线一样的结构。每个元素结点最多...
- 下一篇
JavaScript代码是如何被执行的
基本概念 编译器,解释器 抽象语法树 字节码和机器码 编译器和解释器 计算机不能直接理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言编写的程序。根据语言的执行流程,可以把语言分成编译型语言和解释型语言。 编译型语言:程序在执行之前需要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、go等. 解释型语言: 程序不需要编译,程序在运行时才翻译成机器语言(所以执行前需要环境中安装了解释器),每执行一次都要翻译一次。因此效率比较低。效率比较低,依赖解释器,跨平台性好。 编译型与解释型,两者各有利弊, 不能一概而论。前者由于程序执行速度快,同等条件下对系统要求较低,因此像开发操作系统、大型应用程序、数据库系统等时都采用它,像C/C++、Pascal/Object Pascal(Delphi)等都是编译语言,而一些网页脚本、服务器脚本及辅助开发接口这样的对速度要求不高、对不同系统平台间的兼容性有一定要求的程序则通常使用解释性语言,如JavaScr...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Hadoop3单机部署,实现最简伪集群
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7设置SWAP分区,小内存服务器的救世主
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS7,CentOS8安装Elasticsearch6.8.6