javascript清除map所占内存_【原创.54期】 JavaScript的V8引擎初探

本文主要就下面三块内容展开

  • 栈和堆空间

  • 垃圾回收

  • 解释器和编译器

一.栈空间和堆空间

先回顾下基本知识

1.静态语言就是强类型?二者的关系

(1-1)静态语言, 使用前 要确定数据类型
(1-2)动态语言, 运行中 检查数据类型;
(2-1)弱类型语言,支持隐式类型转换
(2-2)强类型,不支持 隐式转换;
C是静态语言,但是它支持隐式转换,是弱类型

6a111b9d8ed91e0f687cfcd8c01649f5.png

2.JS的内存空间

(1)分三块,代码空间,栈stack空间,堆heap空间;
(2) 原始 类型的数据值都是直接保存在“栈”中的, 引用 类型的值是存放在“堆”中的
栈空间比较小,堆空间比较大;

95a7fd39543cb45e8b71cfb69dafba9a.png

(3)为什么不放在一起?
放在一起,会影响执行上下文切换和执行效率;
(4)闭包的内存模型,Closure
比如下面这段代码,foo中有个闭包对象,有两个属性值 myName和test1,存到堆中;

1234567891011121314
function foo() {  var myName = "悬笔e绝"  let test1 = 1  const test2 = 2  var innerBar = {    setName:function(newName){ myName = newName },    getName:function(){ console.log(test1); return myName }  }  return innerBar}var bar = foo()bar.setName("张三")bar.getName()console.log(bar.getName())

执行到foo 函数中“return innerBar” 的调用栈情况,如下图

7c705eb9a5748a0438df8166d1183b4c.png

二.垃圾回收机制

1.常用的垃圾回收机制如下:

6ffffa3dff4309d477af4c03d7a895e4.png

1-1.标记清除法;

给存储在内存中的变量都加上标记,判断哪些变量没有在执行环境中引用,进行删除;

1-2.引用计数法;

(1)跟踪记录每个值被引用的次数
(2)当声明变量并将一个引用类型的值赋值给该变量时,则这个值的引用次数加1,
(3)同一值被赋予另一个变量,该值的引用计数加1 。
(4)当引用该值的变量被另一个值所取代,则引用计数减1,
(5)当计数为 0 的时候,说明无法在访问这个值了,系统将会收回该值所占用的内存空间。

缺点 :循环引用的时候,引用次数不为0,不会被释放;

调用栈中的数据的GC

1.调用栈还有一个记录当前执行状态的 指针 (称为 ESP)
JS引擎通过下移ESP指针来销毁 栈顶 某个执行上下文的过程。
如下图,上面按个已经是无效的,有新的会直接覆盖;

46dcf245250769f0b5b39ea7922a3486.png

堆空间中数据的GC

1.代际假说和分代收集~新生代,老生代;

(0)提高垃圾回收的效率,V8将堆分为 新生代 和 老生代 两个部分,
(1)其中新生代为存活时间较短的对象(需要经常进行垃圾回收),内存占用小,GC频繁;
只支持1-8M容量;使用副垃圾回收器;
(2)而老生代为存活时间较长的对象(垃圾回收的频率较低),内存占用多,GC不频繁;
使用主垃圾回收器;

2.新生代的GC算法

新生代的对象通过 Scavenge 算法进行GC。在 Scavenge 的具体实现中,主要采用了 Cheney 算法。
(1)Cheney 算法是一种采用 停止复制(stop-copy) 的方式实现的垃圾回收算法。
(2)它将堆内存一分为二,每一部分空间成为 semispace-半空间。
(3)在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置中。
(4)处于使用中的 semispace 空间成为 From 对象空间,处于闲置状态的空间成为 To 空闲空间。
(5)当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,同时还会将这些对象有序的排列起来~~相当于内存整理,所以没有内存碎片,而非存活对象占用的空间将被释放。
完成复制后,From空间和To空间的角色发生对换。
(6)Scavenge 是典型的 空间换取时间 的算法,而且复制需要时间成本,无法大规模地应用到所有的垃圾回收中,但非常适合应用在新生代中进行快速频繁清理。

3.对象晋升策略

(1)对象从新生代中移动到老生代中的过程称为晋升。
(2)晋升条件主要有两个:
(1)对象是否经历过 两次 Scavenge 回收都未清除,则移动到老生代
(2)To 空间已经使用超过 25% ,To 空间对象移动到老生代
因为这次 Scavenge 回收完成后,这个 To 空间将变成 From 空间,接下来的内存分配将在这个空间中进行,如果占比过高,会影响后续的内存分配

4.写屏障

写缓冲区中有一个列表(CrossRefList),列表中记录了所有老生区对象指向新生区的情况
这样可以快速找到指向新生代该对象的老生代对象,根据他是否活跃,来清理这个新生代对象;

5.老生代的GC

(1)老生代的内存空间较大且存活对象较多,使用新生代的Scavenge 复制算法,会耗费很多时间,效率不高;而且还会浪费一半的空间;
(2)为此V8使用了 标记-清除算法 (Mark-Sweep) 进行垃圾回收,并使用 标记-压缩算法 (Mark-Compact) 整理内存碎片,提高内存的利用率。步骤如下:

1) 对老生代进行第一遍扫描,标记存活的对象
从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据;
2) 对老生代进行第二次扫描,清除未被标记的对象

16afe703cd8a9847c2f9b39ad097aa5a.png

3) 标记-整理算法,标记阶段一样是递归遍历元素,整理阶段是将存活对象往内存的一端移动

34416971ff0437df6f77221aeb1c6564.png

4) 清除掉存活对象边界外的内存
注意,不管哪种,整理内存后只要地址有变化,需要及时更新到调用栈的;

6.全停顿Stop-The-World

(1)JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World);
(2)新生代因为内存小,活动对象少,全停顿影响不大,但是老生代可能会造成卡顿明显;
(3)解决办法~ 增量标记算法Incremental Marking
V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成;

92126e0920fd604cc58cb3827edf14fe.png

三.编译器和解释器

1.编译器Compiler和解释器Interpreter

按语言的执行流程,可以把语言划分为编译型语言和解释型语言。

1-1.编译型语言

在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如 C/C++、GO 等都是编译型语言。

233a3c186c9fb76e4d2b9212b9049baf.png

1-2.解释型语言

在每次运行时都需要通过解释器对程序进行动态解释和执行。比如 Python、JavaScript 等都属于解释型语言。

1b35dcafff6e0fd99dfaee7534c71f9a.png

2.V8执行一段代码流程图

2ac3d61916b038f51a0a3508ab037e72.png

2-1.生成抽象语法树(AST)和执行上下文

2-1-1.AST

(1)编译器或解释器,他们不理解高级语言,只可以理解AST;
(2)简单demo,在线AST网站
https://resources.jointjs.com/demos/javascript-ast
(3)可以把 AST 看成代码的结构化表示;
(4)应用非常广泛
1)Babel:
Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码;
2)ESLint:
利用 AST 来检查代码规范化的问题。

2-1-2.AST生成过程

(1)分词 tokenize ,或词法分析
将一行行的源码拆解成一个个 token。
token,指的是语法上不可能再分的、最小的单个字符或字符串;
比如:

1
var myName = "悬笔e绝";

关键字“var” ~ keyword、
标识符“myName” ~ identifier、
赋值运算符“=” ~ assignment、
字符串“悬笔e绝” ~ Literal
四个都是 token,而且它们代表的属性还不一样;

(2)解释 parse ,或语法分析
上一步生成的 token 数据,根据语法规则转为 AST。如果源码符合语法规则,这一步就会顺利完成。但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。

2-1-3.执行上下文

前面介绍过,主要是代码在执行过程中的环境信息

93060b3a5f60656caf9739c83a03a585.png

2-2.生成字节码

(1)解释器Ignition
根据AST生成 字节码 ,并解释执行字节码;
(2)历史,一开始直接把AST转成机器码,
效率性能很高,但是机器码占用内存很大,小内存手机上内存占用问题明显,
V8团队画了快4年时间,引入字节码,抛弃编译器,才有现在的架构;
(3)字节码
字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。

2-3.执行代码

(1)如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。
解释器 Ignition 除了负责生成字节码之外,还有一个作用,就是解释执行字节码。
(2)在 Ignition 执行字节码的过程中,如果发现有 热点代码(HotSpot) ,比如一段代码被重复执行多次,这种就称为热点代码,后台的编译器 TurboFan 就会把它编译为高效的 机器码 ,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
(3)V8 的解释器和编译器的取名
解释器Ignition ~ 点火器;
编译器TurboFan ~ 涡轮增压发动机;
寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率;
(4)即时编译 JIT
字节码配合解释器和编译器的技术;
Java 和 Python 的虚拟机,苹果的 SquirrelFish Extreme 和 Mozilla 的 SpiderMonkey 也是基于JIT这个技术;

eadde90c06c3d9321e30562789479782.png

3.V8做的性能优化措施

(1)脚本流
一边下载,一边解析,节省时间;
(2)字节码缓存
访问同一个页面时直接复用之前的字节码,不再重新编译生成;
(3)内联
将主函数中调用的函数,直接换成要执行的语句;加快执行速度;
(4)隐藏类
通过隐藏类快速定位到动态加入的属性;注意:动态加入的属性顺序不一样,会造成生成不同的隐藏类,我们动态赋值同一个构造函数对象的时候,尽量保证顺序也是一致的。
(5)JIT即时编译,将热点代码编译成机器码
执行越久,越多的代码编译成机器码,速度越快;
常用的函数传入的类型保持固定。并且对象的属性越稳定,越有利于性能。

总结

这篇只是V8引擎的入门介绍,关于V8的其他知识,后续会补充其他的文章
未完待续……

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值